diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index d08a93a99..afe281f3d 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -1779,6 +1779,7 @@ async function checkWorldInfo(chat, maxContext) { const secondarySubstituted = substituteParams(keysecondary); console.debug(`WI UID:${entry.uid}: Filtering for secondary keyword - "${secondarySubstituted}".`); + // Simplified AND/NOT if statement. (Proper fix for PR#1356 by Bronya) if (selectiveLogic === 0 && secondarySubstituted && matchKeys(textToScan, secondarySubstituted.trim()) || selectiveLogic === 1 && !(secondarySubstituted && matchKeys(textToScan, secondarySubstituted.trim()))) { if (selectiveLogic === 0) { diff --git a/public/scripts/world-info.js.bak b/public/scripts/world-info.js.bak new file mode 100644 index 000000000..19626a44e --- /dev/null +++ b/public/scripts/world-info.js.bak @@ -0,0 +1,2494 @@ +import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId } from '../script.js'; +import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean } from './utils.js'; +import { extension_settings, getContext } from './extensions.js'; +import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js'; +import { registerSlashCommand } from './slash-commands.js'; +import { getDeviceInfo } from './RossAscends-mods.js'; +import { FILTER_TYPES, FilterHelper } from './filters.js'; +import { getTokenCount } from './tokenizers.js'; +import { power_user } from './power-user.js'; +import { getTagKeyForCharacter } from './tags.js'; +import { resolveVariable } from './variables.js'; + +export { + world_info, + world_info_budget, + world_info_depth, + world_info_min_activations, + world_info_min_activations_depth_max, + world_info_recursive, + world_info_overflow_alert, + world_info_case_sensitive, + world_info_match_whole_words, + world_info_character_strategy, + world_info_budget_cap, + world_names, + checkWorldInfo, + deleteWorldInfo, + setWorldInfoSettings, + getWorldInfoPrompt, +}; + +const world_info_insertion_strategy = { + evenly: 0, + character_first: 1, + global_first: 2, +}; + +let world_info = {}; +let selected_world_info = []; +let world_names; +let world_info_depth = 2; +let world_info_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) + +let world_info_budget = 25; +let world_info_recursive = false; +let world_info_overflow_alert = false; +let world_info_case_sensitive = false; +let world_info_match_whole_words = false; +let world_info_character_strategy = world_info_insertion_strategy.character_first; +let world_info_budget_cap = 0; +const saveWorldDebounced = debounce(async (name, data) => await _save(name, data), 1000); +const saveSettingsDebounced = debounce(() => { + Object.assign(world_info, { globalSelect: selected_world_info }); + saveSettings(); +}, 1000); +const sortFn = (a, b) => b.order - a.order; +let updateEditor = (navigation) => { navigation; }; + +// Do not optimize. updateEditor is a function that is updated by the displayWorldEntries with new data. +const worldInfoFilter = new FilterHelper(() => updateEditor()); +const SORT_ORDER_KEY = 'world_info_sort_order'; +const METADATA_KEY = 'world_info'; + +const DEFAULT_DEPTH = 4; + +export function getWorldInfoSettings() { + return { + world_info, + world_info_depth, + world_info_min_activations, + world_info_min_activations_depth_max, + world_info_budget, + world_info_recursive, + world_info_overflow_alert, + world_info_case_sensitive, + world_info_match_whole_words, + world_info_character_strategy, + world_info_budget_cap, + }; +} + +const world_info_position = { + before: 0, + after: 1, + ANTop: 2, + ANBottom: 3, + atDepth: 4, +}; + +const worldInfoCache = {}; + +async function getWorldInfoPrompt(chat2, maxContext) { + let worldInfoString = '', worldInfoBefore = '', worldInfoAfter = ''; + + const activatedWorldInfo = await checkWorldInfo(chat2, maxContext); + worldInfoBefore = activatedWorldInfo.worldInfoBefore; + worldInfoAfter = activatedWorldInfo.worldInfoAfter; + worldInfoString = worldInfoBefore + worldInfoAfter; + + return { + worldInfoString, + worldInfoBefore, + worldInfoAfter, + worldInfoDepth: activatedWorldInfo.WIDepthEntries, + }; +} + +function setWorldInfoSettings(settings, data) { + if (settings.world_info_depth !== undefined) + world_info_depth = Number(settings.world_info_depth); + if (settings.world_info_min_activations !== undefined) + world_info_min_activations = Number(settings.world_info_min_activations); + if (settings.world_info_min_activations_depth_max !== undefined) + world_info_min_activations_depth_max = Number(settings.world_info_min_activations_depth_max); + if (settings.world_info_budget !== undefined) + world_info_budget = Number(settings.world_info_budget); + if (settings.world_info_recursive !== undefined) + world_info_recursive = Boolean(settings.world_info_recursive); + if (settings.world_info_overflow_alert !== undefined) + world_info_overflow_alert = Boolean(settings.world_info_overflow_alert); + if (settings.world_info_case_sensitive !== undefined) + world_info_case_sensitive = Boolean(settings.world_info_case_sensitive); + if (settings.world_info_match_whole_words !== undefined) + world_info_match_whole_words = Boolean(settings.world_info_match_whole_words); + if (settings.world_info_character_strategy !== undefined) + world_info_character_strategy = Number(settings.world_info_character_strategy); + if (settings.world_info_budget_cap !== undefined) + world_info_budget_cap = Number(settings.world_info_budget_cap); + + // Migrate old settings + if (world_info_budget > 100) { + world_info_budget = 25; + } + + // Reset selected world from old string and delete old keys + // TODO: Remove next release + const existingWorldInfo = settings.world_info; + if (typeof existingWorldInfo === 'string') { + delete settings.world_info; + selected_world_info = [existingWorldInfo]; + } else if (Array.isArray(existingWorldInfo)) { + delete settings.world_info; + selected_world_info = existingWorldInfo; + } + + world_info = settings.world_info ?? {}; + + $('#world_info_depth_counter').val(world_info_depth); + $('#world_info_depth').val(world_info_depth); + + $('#world_info_min_activations_counter').val(world_info_min_activations); + $('#world_info_min_activations').val(world_info_min_activations); + + $('#world_info_min_activations_depth_max_counter').val(world_info_min_activations_depth_max); + $('#world_info_min_activations_depth_max').val(world_info_min_activations_depth_max); + + $('#world_info_budget_counter').val(world_info_budget); + $('#world_info_budget').val(world_info_budget); + + $('#world_info_recursive').prop('checked', world_info_recursive); + $('#world_info_overflow_alert').prop('checked', world_info_overflow_alert); + $('#world_info_case_sensitive').prop('checked', world_info_case_sensitive); + $('#world_info_match_whole_words').prop('checked', world_info_match_whole_words); + + $(`#world_info_character_strategy option[value='${world_info_character_strategy}']`).prop('selected', true); + $('#world_info_character_strategy').val(world_info_character_strategy); + + $('#world_info_budget_cap').val(world_info_budget_cap); + $('#world_info_budget_cap_counter').val(world_info_budget_cap); + + world_names = data.world_names?.length ? data.world_names : []; + + // Add to existing selected WI if it exists + selected_world_info = selected_world_info.concat(settings.world_info?.globalSelect?.filter((e) => world_names.includes(e)) ?? []); + + if (world_names.length > 0) { + $('#world_info').empty(); + } + + world_names.forEach((item, i) => { + $('#world_info').append(``); + $('#world_editor_select').append(``); + }); + + $('#world_info_sort_order').val(localStorage.getItem(SORT_ORDER_KEY) || '0'); + $('#world_editor_select').trigger('change'); + + eventSource.on(event_types.CHAT_CHANGED, () => { + const hasWorldInfo = !!chat_metadata[METADATA_KEY] && world_names.includes(chat_metadata[METADATA_KEY]); + $('.chat_lorebook_button').toggleClass('world_set', hasWorldInfo); + }); + + // Add slash commands + registerWorldInfoSlashCommands(); +} + +function registerWorldInfoSlashCommands() { + function reloadEditor(file) { + const selectedIndex = world_names.indexOf(file); + if (selectedIndex !== -1) { + $('#world_editor_select').val(selectedIndex).trigger('change'); + } + } + + async function getEntriesFromFile(file) { + if (!file || !world_names.includes(file)) { + toastr.warning('Valid World Info file name is required'); + return ''; + } + + const data = await loadWorldInfoData(file); + + if (!data || !('entries' in data)) { + toastr.warning('World Info file has an invalid format'); + return ''; + } + + const entries = Object.values(data.entries); + + if (!entries || entries.length === 0) { + toastr.warning('World Info file has no entries'); + return ''; + } + + return entries; + } + + async function getChatBookCallback() { + const chatId = getCurrentChatId(); + + if (!chatId) { + toastr.warning('Open a chat to get a name of the chat-bound lorebook'); + return ''; + } + + if (chat_metadata[METADATA_KEY] && world_names.includes(chat_metadata[METADATA_KEY])) { + return chat_metadata[METADATA_KEY]; + } + + // Replace non-alphanumeric characters with underscores, cut to 64 characters + const name = `Chat Book ${getCurrentChatId()}`.replace(/[^a-z0-9]/gi, '_').replace(/_{2,}/g, '_').substring(0, 64); + await createNewWorldInfo(name); + + chat_metadata[METADATA_KEY] = name; + await saveMetadata(); + $('.chat_lorebook_button').addClass('world_set'); + return name; + } + + async function findBookEntryCallback(args, value) { + const file = resolveVariable(args.file); + const field = args.field || 'key'; + + const entries = await getEntriesFromFile(file); + + if (!entries) { + return ''; + } + + const fuse = new Fuse(entries, { + keys: [{ name: field, weight: 1 }], + includeScore: true, + threshold: 0.3, + }); + + const results = fuse.search(value); + + if (!results || results.length === 0) { + return ''; + } + + const result = results[0]?.item?.uid; + + if (result === undefined) { + return ''; + } + + return result; + } + + async function getEntryFieldCallback(args, uid) { + const file = resolveVariable(args.file); + const field = args.field || 'content'; + + const entries = await getEntriesFromFile(file); + + if (!entries) { + return ''; + } + + const entry = entries.find(x => x.uid === uid); + + if (!entry) { + toastr.warning('Valid UID is required'); + return ''; + } + + if (newEntryTemplate[field] === undefined) { + toastr.warning('Valid field name is required'); + return ''; + } + + const fieldValue = entry[field]; + + if (fieldValue === undefined) { + return ''; + } + + if (Array.isArray(fieldValue)) { + return fieldValue.map(x => substituteParams(x)).join(', '); + } + + return substituteParams(String(fieldValue)); + } + + async function createEntryCallback(args, content) { + const file = resolveVariable(args.file); + const key = args.key; + + const data = await loadWorldInfoData(file); + + if (!data || !('entries' in data)) { + toastr.warning('Valid World Info file name is required'); + return ''; + } + + const entry = createWorldInfoEntry(file, data, true); + + if (key) { + entry.key.push(key); + entry.addMemo = true; + entry.comment = key; + } + + if (content) { + entry.content = content; + } + + await saveWorldInfo(file, data, true); + reloadEditor(file); + + return entry.uid; + } + + async function setEntryFieldCallback(args, value) { + const file = resolveVariable(args.file); + const uid = resolveVariable(args.uid); + const field = args.field || 'content'; + + if (value === undefined) { + toastr.warning('Value is required'); + return ''; + } + + const data = await loadWorldInfoData(file); + + if (!data || !('entries' in data)) { + toastr.warning('Valid World Info file name is required'); + return ''; + } + + const entry = data.entries[uid]; + + if (!entry) { + toastr.warning('Valid UID is required'); + return ''; + } + + if (newEntryTemplate[field] === undefined) { + toastr.warning('Valid field name is required'); + return ''; + } + + if (Array.isArray(entry[field])) { + entry[field] = value.split(',').map(x => x.trim()).filter(x => x); + } else if (typeof entry[field] === 'boolean') { + entry[field] = isTrueBoolean(value); + } else if (typeof entry[field] === 'number') { + entry[field] = Number(value); + } else { + entry[field] = value; + } + + if (originalDataKeyMap[field]) { + setOriginalDataValue(data, uid, originalDataKeyMap[field], entry[field]); + } + + await saveWorldInfo(file, data, true); + reloadEditor(file); + return ''; + } + + registerSlashCommand('getchatbook', getChatBookCallback, ['getchatlore', 'getchatwi'], '– get a name of the chat-bound lorebook or create a new one if was unbound, and pass it down the pipe', true, true); + registerSlashCommand('findentry', findBookEntryCallback, ['findlore', 'findwi'], '(file=bookName field=field [texts]) – find a UID of the record from the specified book using the fuzzy match of a field value (default: key) and pass it down the pipe, e.g. /findentry file=chatLore field=key Shadowfang', true, true); + registerSlashCommand('getentryfield', getEntryFieldCallback, ['getlorefield', 'getwifield'], '(file=bookName field=field [UID]) – get a field value (default: content) of the record with the UID from the specified book and pass it down the pipe, e.g. /getentryfield file=chatLore field=content 123', true, true); + registerSlashCommand('createentry', createEntryCallback, ['createlore', 'createwi'], '(file=bookName key=key [content]) – create a new record in the specified book with the key and content (both are optional) and pass the UID down the pipe, e.g. /createentry file=chatLore key=Shadowfang The sword of the king', true, true); + registerSlashCommand('setentryfield', setEntryFieldCallback, ['setlorefield', 'setwifield'], '(file=bookName uid=UID field=field [value]) – set a field value (default: content) of the record with the UID from the specified book. To set multiple values for key fields, use comma-delimited list as a value, e.g. /setentryfield file=chatLore uid=123 field=key Shadowfang,sword,weapon', true, true); +} + +// World Info Editor +async function showWorldEditor(name) { + if (!name) { + hideWorldEditor(); + return; + } + + const wiData = await loadWorldInfoData(name); + displayWorldEntries(name, wiData); +} + +async function loadWorldInfoData(name) { + if (!name) { + return; + } + + if (worldInfoCache[name]) { + return worldInfoCache[name]; + } + + const response = await fetch('/getworldinfo', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ name: name }), + cache: 'no-cache', + }); + + if (response.ok) { + const data = await response.json(); + worldInfoCache[name] = data; + return data; + } + + return null; +} + +async function updateWorldInfoList() { + const result = await fetch('/getsettings', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({}), + }); + + if (result.ok) { + var data = await result.json(); + world_names = data.world_names?.length ? data.world_names : []; + $('#world_info').find('option[value!=""]').remove(); + $('#world_editor_select').find('option[value!=""]').remove(); + + world_names.forEach((item, i) => { + $('#world_info').append(``); + $('#world_editor_select').append(``); + }); + } +} + +function hideWorldEditor() { + displayWorldEntries(null, null); +} + +function getWIElement(name) { + const wiElement = $('#world_info').children().filter(function () { + return $(this).text().toLowerCase() === name.toLowerCase(); + }); + + return wiElement; +} + +/** + * @param {any[]} data WI entries + * @returns {any[]} Sorted data + */ +function sortEntries(data) { + const option = $('#world_info_sort_order').find(':selected'); + const sortField = option.data('field'); + const sortOrder = option.data('order'); + const sortRule = option.data('rule'); + const orderSign = sortOrder === 'asc' ? 1 : -1; + + if (sortRule === 'custom') { + // First by display index, then by order, then by uid + data.sort((a, b) => { + const aValue = a.displayIndex; + const bValue = b.displayIndex; + + return (aValue - bValue || b.order - a.order || a.uid - b.uid); + }); + } else if (sortRule === 'priority') { + // First constant, then normal, then disabled. Then sort by order + data.sort((a, b) => { + const aValue = a.constant ? 0 : a.disable ? 2 : 1; + const bValue = b.constant ? 0 : b.disable ? 2 : 1; + + return (aValue - bValue || b.order - a.order); + }); + } else { + const primarySort = (a, b) => { + const aValue = a[sortField]; + const bValue = b[sortField]; + + // Sort strings + if (typeof aValue === 'string' && typeof bValue === 'string') { + if (sortRule === 'length') { + // Sort by string length + return orderSign * (aValue.length - bValue.length); + } else { + // Sort by A-Z ordinal + return orderSign * aValue.localeCompare(bValue); + } + } + + // Sort numbers + return orderSign * (Number(aValue) - Number(bValue)); + }; + const secondarySort = (a, b) => a.order - b.order; + const tertiarySort = (a, b) => a.uid - b.uid; + + data.sort((a, b) => { + const primary = primarySort(a, b); + + if (primary !== 0) { + return primary; + } + + const secondary = secondarySort(a, b); + + if (secondary !== 0) { + return secondary; + } + + return tertiarySort(a, b); + }); + } + + return data; +} + +function nullWorldInfo() { + toastr.info('Create or import a new World Info file first.', 'World Info is not set', { timeOut: 10000, preventDuplicates: true }); +} + +function displayWorldEntries(name, data, navigation = navigation_option.none) { + updateEditor = (navigation) => displayWorldEntries(name, data, navigation); + + $('#world_popup_entries_list').empty().show(); + + if (!data || !('entries' in data)) { + $('#world_popup_new').off('click').on('click', nullWorldInfo); + $('#world_popup_name_button').off('click').on('click', nullWorldInfo); + $('#world_popup_export').off('click').on('click', nullWorldInfo); + $('#world_popup_delete').off('click').on('click', nullWorldInfo); + $('#world_popup_entries_list').hide(); + $('#world_info_pagination').html(''); + return; + } + + function getDataArray(callback) { + // Convert the data.entries object into an array + let entriesArray = Object.keys(data.entries).map(uid => { + const entry = data.entries[uid]; + entry.displayIndex = entry.displayIndex ?? entry.uid; + return entry; + }); + + // Sort the entries array by displayIndex and uid + entriesArray.sort((a, b) => a.displayIndex - b.displayIndex || a.uid - b.uid); + entriesArray = sortEntries(entriesArray); + entriesArray = worldInfoFilter.applyFilters(entriesArray); + typeof callback === 'function' && callback(entriesArray); + return entriesArray; + } + + let startPage = 1; + + if (navigation === navigation_option.previous) { + startPage = $('#world_info_pagination').pagination('getCurrentPageNum'); + } + + const storageKey = 'WI_PerPage'; + const perPageDefault = 25; + $('#world_info_pagination').pagination({ + dataSource: getDataArray, + pageSize: Number(localStorage.getItem(storageKey)) || perPageDefault, + sizeChangerOptions: [10, 25, 50, 100], + showSizeChanger: true, + pageRange: 1, + pageNumber: startPage, + position: 'top', + showPageNumbers: false, + prevText: '<', + nextText: '>', + formatNavigator: PAGINATION_TEMPLATE, + showNavigator: true, + callback: function (/** @type {object[]} */ page) { + $('#world_popup_entries_list').empty(); + const keywordHeaders = ` +
+ + Title/Memo + + + Status + + + Position + + + Depth + + + Order + + + Trigger % + +
`; + const blocks = page.map(entry => getWorldEntry(name, data, entry)).filter(x => x); + const isCustomOrder = $('#world_info_sort_order').find(':selected').data('rule') === 'custom'; + if (!isCustomOrder) { + blocks.forEach(block => { + block.find('.drag-handle').remove(); + }); + } + $('#world_popup_entries_list').append(keywordHeaders); + $('#world_popup_entries_list').append(blocks); + }, + afterSizeSelectorChange: function (e) { + localStorage.setItem(storageKey, e.target.value); + }, + }); + + if (typeof navigation === 'number' && Number(navigation) >= 0) { + const selector = `#world_popup_entries_list [uid="${navigation}"]`; + const data = getDataArray(); + const uidIndex = data.findIndex(x => x.uid === navigation); + const perPage = Number(localStorage.getItem(storageKey)) || perPageDefault; + const page = Math.floor(uidIndex / perPage) + 1; + $('#world_info_pagination').pagination('go', page); + waitUntilCondition(() => document.querySelector(selector) !== null).finally(() => { + const element = $(selector); + + if (element.length === 0) { + console.log(`Could not find element for uid ${navigation}`); + return; + } + + const elementOffset = element.offset(); + const parentOffset = element.parent().offset(); + const scrollOffset = elementOffset.top - parentOffset.top; + $('#WorldInfo').scrollTop(scrollOffset); + element.addClass('flash animated'); + setTimeout(() => element.removeClass('flash animated'), 2000); + }); + } + + $('#world_popup_new').off('click').on('click', () => { + createWorldInfoEntry(name, data); + }); + + $('#world_popup_name_button').off('click').on('click', async () => { + await renameWorldInfo(name, data); + }); + + $('#world_backfill_memos').off('click').on('click', async () => { + let counter = 0; + for (const entry of Object.values(data.entries)) { + if (!entry.comment && Array.isArray(entry.key) && entry.key.length > 0) { + entry.comment = entry.key[0]; + setOriginalDataValue(data, entry.uid, 'comment', entry.comment); + counter++; + } + } + + if (counter > 0) { + toastr.info(`Backfilled ${counter} titles`); + await saveWorldInfo(name, data, true); + updateEditor(navigation_option.previous); + } + }); + + $('#world_popup_export').off('click').on('click', () => { + if (name && data) { + const jsonValue = JSON.stringify(data); + const fileName = `${name}.json`; + download(jsonValue, fileName, 'application/json'); + } + }); + + $('#world_popup_delete').off('click').on('click', async () => { + const confirmation = await callPopup(`

Delete the World/Lorebook: "${name}"?

This action is irreversible!`, 'confirm'); + + if (!confirmation) { + return; + } + + if (world_info.charLore) { + world_info.charLore.forEach((charLore, index) => { + if (charLore.extraBooks?.includes(name)) { + const tempCharLore = charLore.extraBooks.filter((e) => e !== name); + if (tempCharLore.length === 0) { + world_info.charLore.splice(index, 1); + } else { + charLore.extraBooks = tempCharLore; + } + } + }); + + saveSettingsDebounced(); + } + + // Selected world_info automatically refreshes + await deleteWorldInfo(name); + }); + + // Check if a sortable instance exists + if ($('#world_popup_entries_list').sortable('instance') !== undefined) { + // Destroy the instance + $('#world_popup_entries_list').sortable('destroy'); + } + + $('#world_popup_entries_list').sortable({ + delay: getSortableDelay(), + handle: '.drag-handle', + stop: async function (event, ui) { + const firstEntryUid = $('#world_popup_entries_list .world_entry').first().data('uid'); + const minDisplayIndex = data?.entries[firstEntryUid]?.displayIndex ?? 0; + $('#world_popup_entries_list .world_entry').each(function (index) { + const uid = $(this).data('uid'); + + // Update the display index in the data array + const item = data.entries[uid]; + + if (!item) { + console.debug(`Could not find entry with uid ${uid}`); + return; + } + + item.displayIndex = minDisplayIndex + index; + setOriginalDataValue(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 }))); + + await saveWorldInfo(name, data, true); + }, + }); + //$("#world_popup_entries_list").disableSelection(); +} + +const originalDataKeyMap = { + 'displayIndex': 'extensions.display_index', + 'excludeRecursion': 'extensions.exclude_recursion', + 'selectiveLogic': 'selectiveLogic', + 'comment': 'comment', + 'constant': 'constant', + 'order': 'insertion_order', + 'depth': 'extensions.depth', + 'probability': 'extensions.probability', + 'position': 'extensions.position', + 'content': 'content', + 'enabled': 'enabled', + 'key': 'keys', + 'keysecondary': 'secondary_keys', + 'selective': 'selective', +}; + +function setOriginalDataValue(data, uid, key, value) { + if (data.originalData && Array.isArray(data.originalData.entries)) { + let originalEntry = data.originalData.entries.find(x => x.uid === uid); + + if (!originalEntry) { + return; + } + + const keyParts = key.split('.'); + let currentObject = originalEntry; + + for (let i = 0; i < keyParts.length - 1; i++) { + const part = keyParts[i]; + + if (!Object.hasOwn(currentObject, part)) { + currentObject[part] = {}; + } + + currentObject = currentObject[part]; + } + + currentObject[keyParts[keyParts.length - 1]] = value; + } +} + +function deleteOriginalDataValue(data, uid) { + if (data.originalData && Array.isArray(data.originalData.entries)) { + const originalIndex = data.originalData.entries.findIndex(x => x.uid === uid); + + if (originalIndex >= 0) { + data.originalData.entries.splice(originalIndex, 1); + } + } +} + +function getWorldEntry(name, data, entry) { + if (!data.entries[entry.uid]) { + return; + } + + const template = $('#entry_edit_template .world_entry').clone(); + template.data('uid', entry.uid); + template.attr('uid', entry.uid); + + // key + const keyInput = template.find('textarea[name="key"]'); + keyInput.data('uid', entry.uid); + keyInput.on('click', function (event) { + // Prevent closing the drawer on clicking the input + event.stopPropagation(); + }); + + keyInput.on('input', function () { + const uid = $(this).data('uid'); + const value = String($(this).val()); + resetScrollHeight(this); + data.entries[uid].key = value + .split(',') + .map((x) => x.trim()) + .filter((x) => x); + + setOriginalDataValue(data, uid, 'keys', data.entries[uid].key); + saveWorldInfo(name, data); + }); + keyInput.val(entry.key.join(', ')).trigger('input'); + //initScrollHeight(keyInput); + + // logic AND/NOT + const selectiveLogicDropdown = template.find('select[name="entryLogicType"]'); + selectiveLogicDropdown.data('uid', entry.uid); + + selectiveLogicDropdown.on('click', function (event) { + event.stopPropagation(); + }); + + selectiveLogicDropdown.on('input', function () { + const uid = $(this).data('uid'); + const value = Number($(this).val()); + console.debug(`logic for ${entry.uid} set to ${value}`); + data.entries[uid].selectiveLogic = !isNaN(value) ? value : 0; + setOriginalDataValue(data, uid, 'selectiveLogic', data.entries[uid].selectiveLogic); + saveWorldInfo(name, data); + }); + + template + .find(`select[name="entryLogicType"] option[value=${entry.selectiveLogic}]`) + .prop('selected', true) + .trigger('input'); + + // Character filter + const characterFilterLabel = template.find('label[for="characterFilter"] > small'); + characterFilterLabel.text(entry.characterFilter?.isExclude ? 'Exclude Character(s)' : 'Filter to Character(s)'); + + // exclude characters checkbox + const characterExclusionInput = template.find('input[name="character_exclusion"]'); + characterExclusionInput.data('uid', entry.uid); + characterExclusionInput.on('input', function () { + const uid = $(this).data('uid'); + const value = $(this).prop('checked'); + characterFilterLabel.text(value ? 'Exclude Character(s)' : 'Filter to Character(s)'); + if (data.entries[uid].characterFilter) { + if (!value && data.entries[uid].characterFilter.names.length === 0 && data.entries[uid].characterFilter.tags.length === 0) { + delete data.entries[uid].characterFilter; + } else { + data.entries[uid].characterFilter.isExclude = value; + } + } else if (value) { + Object.assign( + data.entries[uid], + { + characterFilter: { + isExclude: true, + names: [], + tags: [], + }, + }, + ); + } + + setOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter); + saveWorldInfo(name, data); + }); + characterExclusionInput.prop('checked', entry.characterFilter?.isExclude ?? false).trigger('input'); + + const characterFilter = template.find('select[name="characterFilter"]'); + characterFilter.data('uid', entry.uid); + const deviceInfo = getDeviceInfo(); + if (deviceInfo && deviceInfo.device.type === 'desktop') { + $(characterFilter).select2({ + width: '100%', + placeholder: 'All characters will pull from this entry.', + allowClear: true, + closeOnSelect: false, + }); + } + + const characters = getContext().characters; + characters.forEach((character) => { + const option = document.createElement('option'); + const name = character.avatar.replace(/\.[^/.]+$/, '') ?? character.name; + option.innerText = name; + option.selected = entry.characterFilter?.names?.includes(name); + option.setAttribute('data-type', 'character'); + characterFilter.append(option); + }); + + const tags = getContext().tags; + tags.forEach((tag) => { + const option = document.createElement('option'); + option.innerText = `[Tag] ${tag.name}`; + option.selected = entry.characterFilter?.tags?.includes(tag.id); + option.value = tag.id; + option.setAttribute('data-type', 'tag'); + characterFilter.append(option); + }); + + characterFilter.on('mousedown change', async function (e) { + // If there's no world names, don't do anything + if (world_names.length === 0) { + e.preventDefault(); + return; + } + + const uid = $(this).data('uid'); + const selected = $(this).find(':selected'); + if ((!selected || selected?.length === 0) && !data.entries[uid].characterFilter?.isExclude) { + delete data.entries[uid].characterFilter; + } else { + const names = selected.filter('[data-type="character"]').map((_, e) => e instanceof HTMLOptionElement && e.innerText).toArray(); + const tags = selected.filter('[data-type="tag"]').map((_, e) => e instanceof HTMLOptionElement && e.value).toArray(); + Object.assign( + data.entries[uid], + { + characterFilter: { + isExclude: data.entries[uid].characterFilter?.isExclude ?? false, + names: names, + tags: tags, + }, + }, + ); + } + setOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter); + saveWorldInfo(name, data); + }); + + // keysecondary + const keySecondaryInput = template.find('textarea[name="keysecondary"]'); + keySecondaryInput.data('uid', entry.uid); + keySecondaryInput.on('input', function () { + const uid = $(this).data('uid'); + const value = String($(this).val()); + resetScrollHeight(this); + data.entries[uid].keysecondary = value + .split(',') + .map((x) => x.trim()) + .filter((x) => x); + + setOriginalDataValue(data, uid, 'secondary_keys', data.entries[uid].keysecondary); + saveWorldInfo(name, data); + }); + + keySecondaryInput.val(entry.keysecondary.join(', ')).trigger('input'); + initScrollHeight(keySecondaryInput); + + // comment + const commentInput = template.find('textarea[name="comment"]'); + const commentToggle = template.find('input[name="addMemo"]'); + commentInput.data('uid', entry.uid); + commentInput.on('input', function () { + const uid = $(this).data('uid'); + const value = $(this).val(); + resetScrollHeight(this); + data.entries[uid].comment = value; + + setOriginalDataValue(data, uid, 'comment', data.entries[uid].comment); + saveWorldInfo(name, data); + }); + commentToggle.data('uid', entry.uid); + commentToggle.on('input', function () { + const uid = $(this).data('uid'); + const value = $(this).prop('checked'); + //console.log(value) + const commentContainer = $(this) + .closest('.world_entry') + .find('.commentContainer'); + data.entries[uid].addMemo = value; + saveWorldInfo(name, data); + value ? commentContainer.show() : commentContainer.hide(); + }); + + commentInput.val(entry.comment).trigger('input'); + initScrollHeight(commentInput); + commentToggle.prop('checked', true /* entry.addMemo */).trigger('input'); + commentToggle.parent().hide(); + + // content + const counter = template.find('.world_entry_form_token_counter'); + const countTokensDebounced = debounce(function (counter, value) { + const numberOfTokens = getTokenCount(value); + $(counter).text(numberOfTokens); + }, 1000); + + const contentInput = template.find('textarea[name="content"]'); + contentInput.data('uid', entry.uid); + contentInput.on('input', function (_, { skipCount } = {}) { + const uid = $(this).data('uid'); + const value = $(this).val(); + data.entries[uid].content = value; + + setOriginalDataValue(data, uid, 'content', data.entries[uid].content); + saveWorldInfo(name, data); + + if (skipCount) { + return; + } + + // count tokens + countTokensDebounced(counter, value); + }); + contentInput.val(entry.content).trigger('input', { skipCount: true }); + //initScrollHeight(contentInput); + + template.find('.inline-drawer-toggle').on('click', function () { + if (counter.data('first-run')) { + counter.data('first-run', false); + countTokensDebounced(counter, contentInput.val()); + } + }); + + // selective + const selectiveInput = template.find('input[name="selective"]'); + selectiveInput.data('uid', entry.uid); + selectiveInput.on('input', function () { + const uid = $(this).data('uid'); + const value = $(this).prop('checked'); + data.entries[uid].selective = value; + + setOriginalDataValue(data, uid, 'selective', data.entries[uid].selective); + saveWorldInfo(name, data); + + const keysecondary = $(this) + .closest('.world_entry') + .find('.keysecondary'); + + const keysecondarytextpole = $(this) + .closest('.world_entry') + .find('.keysecondarytextpole'); + + const keyprimarytextpole = $(this) + .closest('.world_entry') + .find('.keyprimarytextpole'); + + const keyprimaryHeight = keyprimarytextpole.outerHeight(); + keysecondarytextpole.css('height', keyprimaryHeight + 'px'); + + value ? keysecondary.show() : keysecondary.hide(); + + }); + //forced on, ignored if empty + selectiveInput.prop('checked', true /* entry.selective */).trigger('input'); + selectiveInput.parent().hide(); + + + // constant + /* + const constantInput = template.find('input[name="constant"]'); + constantInput.data("uid", entry.uid); + constantInput.on("input", function () { + const uid = $(this).data("uid"); + const value = $(this).prop("checked"); + data.entries[uid].constant = value; + setOriginalDataValue(data, uid, "constant", data.entries[uid].constant); + saveWorldInfo(name, data); + }); + constantInput.prop("checked", entry.constant).trigger("input"); + */ + + // order + const orderInput = template.find('input[name="order"]'); + orderInput.data('uid', entry.uid); + orderInput.on('input', function () { + const uid = $(this).data('uid'); + const value = Number($(this).val()); + + data.entries[uid].order = !isNaN(value) ? value : 0; + updatePosOrdDisplay(uid); + setOriginalDataValue(data, uid, 'insertion_order', data.entries[uid].order); + saveWorldInfo(name, data); + }); + orderInput.val(entry.order).trigger('input'); + orderInput.css('width', 'calc(3em + 15px)'); + + // probability + if (entry.probability === undefined) { + entry.probability = null; + } + + // depth + const depthInput = template.find('input[name="depth"]'); + depthInput.data('uid', entry.uid); + + depthInput.on('input', function () { + const uid = $(this).data('uid'); + const value = Number($(this).val()); + + data.entries[uid].depth = !isNaN(value) ? value : 0; + updatePosOrdDisplay(uid); + setOriginalDataValue(data, uid, 'extensions.depth', data.entries[uid].depth); + saveWorldInfo(name, data); + }); + depthInput.val(entry.depth ?? DEFAULT_DEPTH).trigger('input'); + depthInput.css('width', 'calc(3em + 15px)'); + + // Hide by default unless depth is specified + if (entry.position === world_info_position.atDepth) { + //depthInput.parent().hide(); + } + + const probabilityInput = template.find('input[name="probability"]'); + probabilityInput.data('uid', entry.uid); + probabilityInput.on('input', function () { + const uid = $(this).data('uid'); + const value = parseInt($(this).val()); + + data.entries[uid].probability = !isNaN(value) ? value : null; + + // Clamp probability to 0-100 + if (data.entries[uid].probability !== null) { + data.entries[uid].probability = Math.min(100, Math.max(0, data.entries[uid].probability)); + + if (data.entries[uid].probability !== value) { + $(this).val(data.entries[uid].probability); + } + } + + setOriginalDataValue(data, uid, 'extensions.probability', data.entries[uid].probability); + saveWorldInfo(name, data); + }); + probabilityInput.val(entry.probability).trigger('input'); + probabilityInput.css('width', 'calc(3em + 15px)'); + + // probability toggle + if (entry.useProbability === undefined) { + entry.useProbability = false; + } + + const probabilityToggle = template.find('input[name="useProbability"]'); + probabilityToggle.data('uid', entry.uid); + probabilityToggle.on('input', function () { + const uid = $(this).data('uid'); + const value = $(this).prop('checked'); + data.entries[uid].useProbability = value; + const probabilityContainer = $(this) + .closest('.world_entry') + .find('.probabilityContainer'); + saveWorldInfo(name, data); + value ? probabilityContainer.show() : probabilityContainer.hide(); + + if (value && data.entries[uid].probability === null) { + data.entries[uid].probability = 100; + } + + if (!value) { + data.entries[uid].probability = null; + } + + probabilityInput.val(data.entries[uid].probability).trigger('input'); + }); + //forced on, 100% by default + probabilityToggle.prop('checked', true /* entry.useProbability */).trigger('input'); + probabilityToggle.parent().hide(); + + // position + if (entry.position === undefined) { + entry.position = 0; + } + + const positionInput = template.find('select[name="position"]'); + initScrollHeight(positionInput); + positionInput.data('uid', entry.uid); + positionInput.on('click', function (event) { + // Prevent closing the drawer on clicking the input + event.stopPropagation(); + }); + positionInput.on('input', function () { + const uid = $(this).data('uid'); + const value = Number($(this).val()); + data.entries[uid].position = !isNaN(value) ? value : 0; + if (value === world_info_position.atDepth) { + depthInput.prop('disabled', false); + depthInput.css('visibility', 'visible'); + //depthInput.parent().show(); + } else { + depthInput.prop('disabled', true); + depthInput.css('visibility', 'hidden'); + //depthInput.parent().hide(); + } + updatePosOrdDisplay(uid); + // Spec v2 only supports before_char and after_char + setOriginalDataValue(data, uid, 'position', data.entries[uid].position == 0 ? 'before_char' : 'after_char'); + // Write the original value as extensions field + setOriginalDataValue(data, uid, 'extensions.position', data.entries[uid].position); + saveWorldInfo(name, data); + }); + + template + .find(`select[name="position"] option[value=${entry.position}]`) + .prop('selected', true) + .trigger('input'); + + //add UID above content box (less important doesn't need to be always visible) + template.find('.world_entry_form_uid_value').text(`(UID: ${entry.uid})`); + + // disable + /* + const disableInput = template.find('input[name="disable"]'); + disableInput.data("uid", entry.uid); + disableInput.on("input", function () { + const uid = $(this).data("uid"); + const value = $(this).prop("checked"); + data.entries[uid].disable = value; + setOriginalDataValue(data, uid, "enabled", !data.entries[uid].disable); + saveWorldInfo(name, data); + }); + disableInput.prop("checked", entry.disable).trigger("input"); + */ + + //new tri-state selector for constant/normal/disabled + const entryStateSelector = template.find('select[name="entryStateSelector"]'); + entryStateSelector.data('uid', entry.uid); + console.log(entry.uid); + entryStateSelector.on('click', function (event) { + // Prevent closing the drawer on clicking the input + event.stopPropagation(); + }); + entryStateSelector.on('input', function () { + const uid = entry.uid; + const value = $(this).val(); + switch (value) { + case 'constant': + data.entries[uid].constant = true; + data.entries[uid].disable = false; + setOriginalDataValue(data, uid, 'enabled', true); + setOriginalDataValue(data, uid, 'constant', true); + template.removeClass('disabledWIEntry'); + console.debug('set to constant'); + break; + case 'normal': + data.entries[uid].constant = false; + data.entries[uid].disable = false; + setOriginalDataValue(data, uid, 'enabled', true); + setOriginalDataValue(data, uid, 'constant', false); + template.removeClass('disabledWIEntry'); + console.debug('set to normal'); + break; + case 'disabled': + data.entries[uid].constant = false; + data.entries[uid].disable = true; + setOriginalDataValue(data, uid, 'enabled', false); + setOriginalDataValue(data, uid, 'constant', false); + template.addClass('disabledWIEntry'); + console.debug('set to disabled'); + break; + } + saveWorldInfo(name, data); + + }); + + const entryState = function () { + + console.log(`constant: ${entry.constant}, disabled: ${entry.disable}`); + if (entry.constant === true) { + console.debug('found constant'); + return 'constant'; + } else if (entry.disable === true) { + console.debug('found disabled'); + return 'disabled'; + } else { + console.debug('found normal'); + return 'normal'; + } + + }; + template + .find(`select[name="entryStateSelector"] option[value=${entryState()}]`) + .prop('selected', true) + .trigger('input'); + + saveWorldInfo(name, data); + + // exclude recursion + const excludeRecursionInput = template.find('input[name="exclude_recursion"]'); + excludeRecursionInput.data('uid', entry.uid); + excludeRecursionInput.on('input', function () { + const uid = $(this).data('uid'); + const value = $(this).prop('checked'); + data.entries[uid].excludeRecursion = value; + setOriginalDataValue(data, uid, 'extensions.exclude_recursion', data.entries[uid].excludeRecursion); + saveWorldInfo(name, data); + }); + excludeRecursionInput.prop('checked', entry.excludeRecursion).trigger('input'); + + // delete button + const deleteButton = template.find('.delete_entry_button'); + deleteButton.data('uid', entry.uid); + deleteButton.on('click', function () { + const uid = $(this).data('uid'); + deleteWorldInfoEntry(data, uid); + deleteOriginalDataValue(data, uid); + saveWorldInfo(name, data); + updateEditor(navigation_option.previous); + }); + + template.find('.inline-drawer-content').css('display', 'none'); //entries start collapsed + + function updatePosOrdDisplay(uid) { + // display position/order info left of keyword box + let entry = data.entries[uid]; + let posText = entry.position; + switch (entry.position) { + case 0: + posText = '↑CD'; + break; + case 1: + posText = 'CD↓'; + break; + case 2: + posText = '↑AN'; + break; + case 3: + posText = 'AN↓'; + break; + case 4: + posText = `@D${entry.depth}`; + break; + } + template.find('.world_entry_form_position_value').text(`(${posText} ${entry.order})`); + } + + return template; +} + +async function deleteWorldInfoEntry(data, uid) { + if (!data || !('entries' in data)) { + return; + } + + if (!confirm(`Delete the entry with UID: ${uid}? This action is irreversible!`)) { + throw new Error('User cancelled deletion'); + } + + delete data.entries[uid]; +} + +const newEntryTemplate = { + key: [], + keysecondary: [], + comment: '', + content: '', + constant: false, + selective: true, + selectiveLogic: 0, + addMemo: false, + order: 100, + position: 0, + disable: false, + excludeRecursion: false, + probability: 100, + useProbability: true, + depth: DEFAULT_DEPTH, + group: '', +}; + +function createWorldInfoEntry(name, data, fromSlashCommand = false) { + const newUid = getFreeWorldEntryUid(data); + + if (!Number.isInteger(newUid)) { + console.error('Couldn\'t assign UID to a new entry'); + return; + } + + const newEntry = { uid: newUid, ...structuredClone(newEntryTemplate) }; + data.entries[newUid] = newEntry; + + if (!fromSlashCommand) { + updateEditor(newUid); + } + + return newEntry; +} + +async function _save(name, data) { + await fetch('/editworldinfo', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ name: name, data: data }), + }); +} + +async function saveWorldInfo(name, data, immediately) { + if (!name || !data) { + return; + } + + delete worldInfoCache[name]; + + if (immediately) { + return await _save(name, data); + } + + saveWorldDebounced(name, data); +} + +async function renameWorldInfo(name, data) { + const oldName = name; + const newName = await callPopup('

Rename World Info

Enter a new name:', 'input', oldName); + + if (oldName === newName || !newName) { + console.debug('World info rename cancelled'); + return; + } + + const entryPreviouslySelected = selected_world_info.findIndex((e) => e === oldName); + + await saveWorldInfo(newName, data, true); + await deleteWorldInfo(oldName); + + const existingCharLores = world_info.charLore?.filter((e) => e.extraBooks.includes(oldName)); + if (existingCharLores && existingCharLores.length > 0) { + existingCharLores.forEach((charLore) => { + const tempCharLore = charLore.extraBooks.filter((e) => e !== oldName); + tempCharLore.push(newName); + charLore.extraBooks = tempCharLore; + }); + saveSettingsDebounced(); + } + + if (entryPreviouslySelected !== -1) { + const wiElement = getWIElement(newName); + wiElement.prop('selected', true); + $('#world_info').trigger('change'); + } + + const selectedIndex = world_names.indexOf(newName); + if (selectedIndex !== -1) { + $('#world_editor_select').val(selectedIndex).trigger('change'); + } +} + +async function deleteWorldInfo(worldInfoName) { + if (!world_names.includes(worldInfoName)) { + return; + } + + const response = await fetch('/deleteworldinfo', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ name: worldInfoName }), + }); + + if (response.ok) { + const existingWorldIndex = selected_world_info.findIndex((e) => e === worldInfoName); + if (existingWorldIndex !== -1) { + selected_world_info.splice(existingWorldIndex, 1); + saveSettingsDebounced(); + } + + await updateWorldInfoList(); + $('#world_editor_select').trigger('change'); + + if ($('#character_world').val() === worldInfoName) { + $('#character_world').val('').trigger('change'); + setWorldInfoButtonClass(undefined, false); + if (menu_type != 'create') { + saveCharacterDebounced(); + } + } + } +} + +function getFreeWorldEntryUid(data) { + if (!data || !('entries' in data)) { + return null; + } + + const MAX_UID = 1_000_000; // <- should be safe enough :) + for (let uid = 0; uid < MAX_UID; uid++) { + if (uid in data.entries) { + continue; + } + return uid; + } + + return null; +} + +function getFreeWorldName() { + const MAX_FREE_NAME = 100_000; + for (let index = 1; index < MAX_FREE_NAME; index++) { + const newName = `New World (${index})`; + if (world_names.includes(newName)) { + continue; + } + return newName; + } + + return undefined; +} + +async function createNewWorldInfo(worldInfoName) { + const worldInfoTemplate = { entries: {} }; + + if (!worldInfoName) { + return; + } + + await saveWorldInfo(worldInfoName, worldInfoTemplate, true); + await updateWorldInfoList(); + + const selectedIndex = world_names.indexOf(worldInfoName); + if (selectedIndex !== -1) { + $('#world_editor_select').val(selectedIndex).trigger('change'); + } else { + hideWorldEditor(); + } +} + +// Gets a string that respects the case sensitivity setting +function transformString(str) { + return world_info_case_sensitive ? str : str.toLowerCase(); +} + +async function getCharacterLore() { + const character = characters[this_chid]; + const name = character?.name; + let worldsToSearch = new Set(); + + const baseWorldName = character?.data?.extensions?.world; + if (baseWorldName) { + worldsToSearch.add(baseWorldName); + } else { + console.debug(`Character ${name}'s base world could not be found or is empty! Skipping...`); + return []; + } + + // TODO: Maybe make the utility function not use the window context? + const fileName = getCharaFilename(this_chid); + const extraCharLore = world_info.charLore?.find((e) => e.name === fileName); + if (extraCharLore) { + worldsToSearch = new Set([...worldsToSearch, ...extraCharLore.extraBooks]); + } + + let entries = []; + for (const worldName of worldsToSearch) { + if (selected_world_info.includes(worldName)) { + console.debug(`Character ${name}'s world ${worldName} is already activated in global world info! Skipping...`); + continue; + } + + if (chat_metadata[METADATA_KEY] === worldName) { + console.debug(`Character ${name}'s world ${worldName} is already activated in chat lore! Skipping...`); + continue; + } + + const data = await loadWorldInfoData(worldName); + const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : []; + entries = entries.concat(newEntries); + } + + console.debug(`Character ${characters[this_chid]?.name} lore (${baseWorldName}) has ${entries.length} world info entries`); + return entries; +} + +async function getGlobalLore() { + if (!selected_world_info) { + return []; + } + + let entries = []; + for (const worldName of selected_world_info) { + const data = await loadWorldInfoData(worldName); + const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : []; + entries = entries.concat(newEntries); + } + + console.debug(`Global world info has ${entries.length} entries`); + + return entries; +} + +async function getChatLore() { + const chatWorld = chat_metadata[METADATA_KEY]; + + if (!chatWorld) { + return []; + } + + if (selected_world_info.includes(chatWorld)) { + console.debug(`Chat world ${chatWorld} is already activated in global world info! Skipping...`); + return []; + } + + const data = await loadWorldInfoData(chatWorld); + const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : []; + + console.debug(`Chat lore has ${entries.length} entries`); + + return entries; +} + +async function getSortedEntries() { + try { + const globalLore = await getGlobalLore(); + const characterLore = await getCharacterLore(); + const chatLore = await getChatLore(); + + let entries; + + switch (Number(world_info_character_strategy)) { + case world_info_insertion_strategy.evenly: + console.debug('WI using evenly'); + entries = [...globalLore, ...characterLore].sort(sortFn); + break; + case world_info_insertion_strategy.character_first: + console.debug('WI using char first'); + entries = [...characterLore.sort(sortFn), ...globalLore.sort(sortFn)]; + break; + case world_info_insertion_strategy.global_first: + console.debug('WI using global first'); + entries = [...globalLore.sort(sortFn), ...characterLore.sort(sortFn)]; + break; + default: + console.error('Unknown WI insertion strategy: ', world_info_character_strategy, 'defaulting to evenly'); + entries = [...globalLore, ...characterLore].sort(sortFn); + break; + } + + // Chat lore always goes first + entries = [...chatLore.sort(sortFn), ...entries]; + + console.debug(`Sorted ${entries.length} world lore entries using strategy ${world_info_character_strategy}`); + + // Need to deep clone the entries to avoid modifying the cached data + return structuredClone(entries); + } + catch (e) { + console.error(e); + return []; + } +} + +async function checkWorldInfo(chat, maxContext) { + const context = getContext(); + const messagesToLookBack = world_info_depth * 2 || 1; + + // Combine the chat + let textToScan = chat.slice(0, messagesToLookBack).join(''); + let minActivationMsgIndex = messagesToLookBack; // tracks chat index to satisfy `world_info_min_activations` + + // Add the depth or AN if enabled + // Put this code here since otherwise, the chat reference is modified + if (extension_settings.note.allowWIScan) { + for (const key of Object.keys(context.extensionPrompts)) { + if (key.startsWith('DEPTH_PROMPT')) { + const depthPrompt = getExtensionPromptByName(key); + if (depthPrompt) { + textToScan = `${depthPrompt}\n${textToScan}`; + } + } + } + + const anPrompt = getExtensionPromptByName(NOTE_MODULE_NAME); + if (anPrompt) { + textToScan = `${anPrompt}\n${textToScan}`; + } + } + + // Transform the resulting string + textToScan = transformString(textToScan); + + let needsToScan = true; + let token_budget_overflowed = false; + let count = 0; + let allActivatedEntries = new Set(); + let failedProbabilityChecks = new Set(); + let allActivatedText = ''; + + let budget = Math.round(world_info_budget * maxContext / 100) || 1; + + if (world_info_budget_cap > 0 && budget > world_info_budget_cap) { + console.debug(`Budget ${budget} exceeds cap ${world_info_budget_cap}, using cap`); + budget = world_info_budget_cap; + } + + console.debug(`Context size: ${maxContext}; WI budget: ${budget} (max% = ${world_info_budget}%, cap = ${world_info_budget_cap})`); + const sortedEntries = await getSortedEntries(); + + if (sortedEntries.length === 0) { + return { worldInfoBefore: '', worldInfoAfter: '' }; + } + + while (needsToScan) { + // Track how many times the loop has run + count++; + + let activatedNow = new Set(); + + for (let entry of sortedEntries) { + // Check if this entry applies to the character or if it's excluded + if (entry.characterFilter && entry.characterFilter?.names?.length > 0) { + const nameIncluded = entry.characterFilter.names.includes(getCharaFilename()); + const filtered = entry.characterFilter.isExclude ? nameIncluded : !nameIncluded; + + if (filtered) { + console.debug(`WI entry ${entry.uid} filtered out by character`); + continue; + } + } + + if (entry.characterFilter && entry.characterFilter?.tags?.length > 0) { + const tagKey = getTagKeyForCharacter(this_chid); + + if (tagKey) { + const tagMapEntry = context.tagMap[tagKey]; + + if (Array.isArray(tagMapEntry)) { + // If tag map intersects with the tag exclusion list, skip + const includesTag = tagMapEntry.some((tag) => entry.characterFilter.tags.includes(tag)); + const filtered = entry.characterFilter.isExclude ? includesTag : !includesTag; + + if (filtered) { + console.debug(`WI entry ${entry.uid} filtered out by tag`); + continue; + } + } + } + } + + if (failedProbabilityChecks.has(entry)) { + continue; + } + + if (allActivatedEntries.has(entry) || entry.disable == true || (count > 1 && world_info_recursive && entry.excludeRecursion)) { + continue; + } + + if (entry.constant) { + entry.content = substituteParams(entry.content); + activatedNow.add(entry); + continue; + } + + if (Array.isArray(entry.key) && entry.key.length) { //check for keywords existing + + // If selectiveLogic isn't found, assume it's AND, only do this once per entry + const selectiveLogic = entry.selectiveLogic ?? 0; + let causeWords = { + primary: "", + secondary: "" + }; + + primary: for (let key of entry.key) { + // For Debug Purposes + causeWords = { + primary: "", + secondary: "" + }; + const substituted = substituteParams(key); + + console.debug(`${entry.uid}: ${substituted}`); + + if (substituted && matchKeys(textToScan, substituted.trim())) { + console.debug(`${entry.uid}: got primary match`); + // Add Primary to Debug Dictionary + causeWords.primary = substituted; + + //selective logic begins + if ( + entry.selective && //all entries are selective now + Array.isArray(entry.keysecondary) && //always true + entry.keysecondary.length //ignore empties + ) { + console.debug(`uid:${entry.uid}: checking logic: ${entry.selectiveLogic}`); + secondary: for (let keysecondary of entry.keysecondary) { + const secondarySubstituted = substituteParams(keysecondary); + console.debug(`uid:${entry.uid}: filtering ${secondarySubstituted}`); + + //AND operator + if (selectiveLogic === 0) { + console.debug('Saw AND logic, checking...'); + if (secondarySubstituted && matchKeys(textToScan, secondarySubstituted.trim())) { + console.debug(`activating entry ${entry.uid} with AND found: ${substituted} ${secondarySubstituted}`); + // Add Secondary to Debug Dictionary + causeWords.secondary = secondarySubstituted; + + activatedNow.add(entry); + break secondary; + } + } + + //NOT operator + if (selectiveLogic === 1) { + console.debug('Saw NOT logic, checking...'); + // Since AND is opposite of NOT, this should be opposite too. + if (!(secondarySubstituted && matchKeys(textToScan, secondarySubstituted.trim()))) { + console.debug(`activating entry ${entry.uid} with NOT found: ${substituted} ${secondarySubstituted}`); + causeWords.secondary = secondarySubstituted; + + activatedNow.add(entry); + break secondary; + } + } + } + //handle cases where secondary is empty + } else { + console.debug(`uid ${entry.uid}: activated without filter logic`); + activatedNow.add(entry); + break primary; + } + } else { console.debug('no active entries for logic checks yet'); } + } + console.debug(`DEBUG Output: ${entry.uid}. Primary Word Trigger: ${causeWords.primary} | Secondary Word Trigger: ${causeWords.secondary}.`) + } + } + + needsToScan = world_info_recursive && activatedNow.size > 0; + const newEntries = [...activatedNow] + .sort((a, b) => sortedEntries.indexOf(a) - sortedEntries.indexOf(b)); + let newContent = ''; + const textToScanTokens = getTokenCount(allActivatedText); + const probabilityChecksBefore = failedProbabilityChecks.size; + console.debug('-- PROBABILITY CHECKS BEGIN --'); + for (const entry of newEntries) { + const rollValue = Math.random() * 100; + + + if (entry.useProbability && rollValue > entry.probability) { + console.debug(`WI entry ${entry.uid} ${entry.key} failed probability check, skipping`); + failedProbabilityChecks.add(entry); + continue; + } else { console.debug(`uid:${entry.uid} passed probability check, inserting to prompt`); } + + newContent += `${substituteParams(entry.content)}\n`; + + if (textToScanTokens + getTokenCount(newContent) >= budget) { + console.debug('WI budget reached, stopping'); + if (world_info_overflow_alert) { + console.log('Alerting'); + toastr.warning(`World info budget reached after ${allActivatedEntries.size} entries.`, 'World Info'); + } + needsToScan = false; + token_budget_overflowed = true; + break; + } + + allActivatedEntries.add(entry); + console.debug('WI entry activated:', entry); + } + + const probabilityChecksAfter = failedProbabilityChecks.size; + + if ((probabilityChecksAfter - probabilityChecksBefore) === activatedNow.size) { + console.debug('WI probability checks failed for all activated entries, stopping'); + needsToScan = false; + } + + if (needsToScan) { + const text = newEntries + .filter(x => !failedProbabilityChecks.has(x)) + .map(x => x.content).join('\n'); + const currentlyActivatedText = transformString(text); + textToScan = (currentlyActivatedText + '\n' + textToScan); + allActivatedText = (currentlyActivatedText + '\n' + allActivatedText); + } + + // world_info_min_activations + if (!needsToScan && !token_budget_overflowed) { + if (world_info_min_activations > 0 && (allActivatedEntries.size < world_info_min_activations)) { + let over_max = false; + over_max = ( + world_info_min_activations_depth_max > 0 && + minActivationMsgIndex > world_info_min_activations_depth_max + ) || (minActivationMsgIndex >= chat.length); + if (!over_max) { + needsToScan = true; + textToScan = transformString(chat.slice(minActivationMsgIndex, minActivationMsgIndex + 1).join('')); + minActivationMsgIndex += 1; + } + } + } + } + + // Forward-sorted list of entries for joining + const WIBeforeEntries = []; + const WIAfterEntries = []; + const ANTopEntries = []; + const ANBottomEntries = []; + const WIDepthEntries = []; + + // Appends from insertion order 999 to 1. Use unshift for this purpose + [...allActivatedEntries].sort(sortFn).forEach((entry) => { + switch (entry.position) { + case world_info_position.before: + WIBeforeEntries.unshift(substituteParams(entry.content)); + break; + case world_info_position.after: + WIAfterEntries.unshift(substituteParams(entry.content)); + break; + case world_info_position.ANTop: + ANTopEntries.unshift(entry.content); + break; + case world_info_position.ANBottom: + ANBottomEntries.unshift(entry.content); + break; + case world_info_position.atDepth: { + const existingDepthIndex = WIDepthEntries.findIndex((e) => e.depth === entry.depth ?? DEFAULT_DEPTH); + if (existingDepthIndex !== -1) { + WIDepthEntries[existingDepthIndex].entries.unshift(entry.content); + } else { + WIDepthEntries.push({ + depth: entry.depth, + entries: [entry.content], + }); + } + break; + } + default: + break; + } + }); + + const worldInfoBefore = WIBeforeEntries.length ? WIBeforeEntries.join('\n') : ''; + const worldInfoAfter = WIAfterEntries.length ? WIAfterEntries.join('\n') : ''; + + if (shouldWIAddPrompt) { + const originalAN = context.extensionPrompts[NOTE_MODULE_NAME].value; + const ANWithWI = `${ANTopEntries.join('\n')}\n${originalAN}\n${ANBottomEntries.join('\n')}`; + context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth]); + } + + return { worldInfoBefore, worldInfoAfter, WIDepthEntries }; +} + +function matchKeys(haystack, needle) { + const transformedString = transformString(needle); + + if (world_info_match_whole_words) { + const keyWords = transformedString.split(/\s+/); + + if (keyWords.length > 1) { + return haystack.includes(transformedString); + } + else { + const regex = new RegExp(`\\b${escapeRegex(transformedString)}\\b`); + if (regex.test(haystack)) { + return true; + } + } + + } else { + return haystack.includes(transformedString); + } + + return false; +} + +function convertAgnaiMemoryBook(inputObj) { + const outputObj = { entries: {} }; + + inputObj.entries.forEach((entry, index) => { + outputObj.entries[index] = { + uid: index, + key: entry.keywords, + keysecondary: [], + comment: entry.name, + content: entry.entry, + constant: false, + selective: false, + order: entry.weight, + position: 0, + disable: !entry.enabled, + addMemo: !!entry.name, + excludeRecursion: false, + displayIndex: index, + probability: null, + useProbability: false, + group: '', + }; + }); + + return outputObj; +} + +function convertRisuLorebook(inputObj) { + const outputObj = { entries: {} }; + + inputObj.data.forEach((entry, index) => { + outputObj.entries[index] = { + uid: index, + key: entry.key.split(',').map(x => x.trim()), + keysecondary: entry.secondkey ? entry.secondkey.split(',').map(x => x.trim()) : [], + comment: entry.comment, + content: entry.content, + constant: entry.alwaysActive, + selective: entry.selective, + order: entry.insertorder, + position: world_info_position.before, + disable: false, + addMemo: true, + excludeRecursion: false, + displayIndex: index, + probability: entry.activationPercent ?? null, + useProbability: entry.activationPercent ?? false, + group: '', + }; + }); + + return outputObj; +} + +function convertNovelLorebook(inputObj) { + const outputObj = { + entries: {}, + }; + + inputObj.entries.forEach((entry, index) => { + const displayName = entry.displayName; + const addMemo = displayName !== undefined && displayName.trim() !== ''; + + outputObj.entries[index] = { + uid: index, + key: entry.keys, + keysecondary: [], + comment: displayName || '', + content: entry.text, + constant: false, + selective: false, + order: entry.contextConfig?.budgetPriority ?? 0, + position: 0, + disable: !entry.enabled, + addMemo: addMemo, + excludeRecursion: false, + displayIndex: index, + probability: null, + useProbability: false, + group: '', + }; + }); + + return outputObj; +} + +function convertCharacterBook(characterBook) { + const result = { entries: {}, originalData: characterBook }; + + characterBook.entries.forEach((entry, index) => { + // Not in the spec, but this is needed to find the entry in the original data + if (entry.id === undefined) { + entry.id = index; + } + + result.entries[entry.id] = { + uid: entry.id, + key: entry.keys, + keysecondary: entry.secondary_keys || [], + comment: entry.comment || '', + content: entry.content, + constant: entry.constant || false, + selective: entry.selective || false, + order: entry.insertion_order, + position: entry.extensions?.position ?? (entry.position === 'before_char' ? world_info_position.before : world_info_position.after), + excludeRecursion: entry.extensions?.exclude_recursion ?? false, + disable: !entry.enabled, + addMemo: entry.comment ? true : false, + displayIndex: entry.extensions?.display_index ?? index, + probability: entry.extensions?.probability ?? null, + useProbability: entry.extensions?.useProbability ?? false, + depth: entry.extensions?.depth ?? DEFAULT_DEPTH, + selectiveLogic: entry.extensions?.selectiveLogic ?? 0, + group: entry.extensions?.group ?? '', + }; + }); + + return result; +} + +export function setWorldInfoButtonClass(chid, forceValue = undefined) { + if (forceValue !== undefined) { + $('#set_character_world, #world_button').toggleClass('world_set', forceValue); + return; + } + + if (!chid) { + return; + } + + const world = characters[chid]?.data?.extensions?.world; + const worldSet = Boolean(world && world_names.includes(world)); + $('#set_character_world, #world_button').toggleClass('world_set', worldSet); +} + +export function checkEmbeddedWorld(chid) { + $('#import_character_info').hide(); + + if (chid === undefined) { + return false; + } + + if (characters[chid]?.data?.character_book) { + $('#import_character_info').data('chid', chid).show(); + + // Only show the alert once per character + const checkKey = `AlertWI_${characters[chid].avatar}`; + const worldName = characters[chid]?.data?.extensions?.world; + if (!localStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) { + localStorage.setItem(checkKey, 1); + + if (power_user.world_import_dialog) { + const html = `

This character has an embedded World/Lorebook.

+

Would you like to import it now?

+
If you want to import it later, select "Import Card Lore" in the "More..." dropdown menu on the character panel.
`; + const checkResult = (result) => { + if (result) { + importEmbeddedWorldInfo(true); + } + }; + callPopup(html, 'confirm', '', { okButton: 'Yes' }).then(checkResult); + } + else { + toastr.info( + 'To import and use it, select "Import Card Lore" in the "More..." dropdown menu on the character panel.', + `${characters[chid].name} has an embedded World/Lorebook`, + { timeOut: 5000, extendedTimeOut: 10000, positionClass: 'toast-top-center' }, + ); + } + } + return true; + } + + return false; +} + +export async function importEmbeddedWorldInfo(skipPopup = false) { + const chid = $('#import_character_info').data('chid'); + + if (chid === undefined) { + return; + } + + const bookName = characters[chid]?.data?.character_book?.name || `${characters[chid]?.name}'s Lorebook`; + const confirmationText = (`

Are you sure you want to import "${bookName}"?

`) + (world_names.includes(bookName) ? 'It will overwrite the World/Lorebook with the same name.' : ''); + + if (!skipPopup) { + const confirmation = await callPopup(confirmationText, 'confirm'); + + if (!confirmation) { + return; + } + } + + const convertedBook = convertCharacterBook(characters[chid].data.character_book); + + await saveWorldInfo(bookName, convertedBook, true); + await updateWorldInfoList(); + $('#character_world').val(bookName).trigger('change'); + + toastr.success(`The world "${bookName}" has been imported and linked to the character successfully.`, 'World/Lorebook imported'); + + const newIndex = world_names.indexOf(bookName); + if (newIndex >= 0) { + //show&draw the WI panel before.. + $('#WIDrawerIcon').trigger('click'); + //..auto-opening the new imported WI + $('#world_editor_select').val(newIndex).trigger('change'); + } + + setWorldInfoButtonClass(chid, true); +} + +function onWorldInfoChange(_, text) { + if (_ !== '__notSlashCommand__') { // if it's a slash command + if (text.trim() !== '') { // and args are provided + const slashInputSplitText = text.trim().toLowerCase().split(','); + + slashInputSplitText.forEach((worldName) => { + const wiElement = getWIElement(worldName); + if (wiElement.length > 0) { + selected_world_info.push(wiElement.text()); + wiElement.prop('selected', true); + toastr.success(`Activated world: ${wiElement.text()}`); + } else { + toastr.error(`No world found named: ${worldName}`); + } + }); + $('#world_info').trigger('change'); + } else { // if no args, unset all worlds + toastr.success('Deactivated all worlds'); + selected_world_info = []; + $('#world_info').val(null).trigger('change'); + } + } else { //if it's a pointer selection + let tempWorldInfo = []; + let selectedWorlds = $('#world_info').val().map((e) => Number(e)).filter((e) => !isNaN(e)); + if (selectedWorlds.length > 0) { + selectedWorlds.forEach((worldIndex) => { + const existingWorldName = world_names[worldIndex]; + if (existingWorldName) { + tempWorldInfo.push(existingWorldName); + } else { + const wiElement = getWIElement(existingWorldName); + wiElement.prop('selected', false); + toastr.error(`The world with ${existingWorldName} is invalid or corrupted.`); + } + }); + } + selected_world_info = tempWorldInfo; + } + + saveSettingsDebounced(); + eventSource.emit(event_types.WORLDINFO_SETTINGS_UPDATED); +} + +export async function importWorldInfo(file) { + if (!file) { + return; + } + + const formData = new FormData(); + formData.append('avatar', file); + + try { + let jsonData; + + if (file.name.endsWith('.png')) { + const buffer = new Uint8Array(await getFileBuffer(file)); + jsonData = extractDataFromPng(buffer, 'naidata'); + } else { + // File should be a JSON file + jsonData = await parseJsonFile(file); + } + + if (jsonData === undefined || jsonData === null) { + toastr.error(`File is not valid: ${file.name}`); + return; + } + + // Convert Novel Lorebook + if (jsonData.lorebookVersion !== undefined) { + console.log('Converting Novel Lorebook'); + formData.append('convertedData', JSON.stringify(convertNovelLorebook(jsonData))); + } + + // Convert Agnai Memory Book + if (jsonData.kind === 'memory') { + console.log('Converting Agnai Memory Book'); + formData.append('convertedData', JSON.stringify(convertAgnaiMemoryBook(jsonData))); + } + + // Convert Risu Lorebook + if (jsonData.type === 'risu') { + console.log('Converting Risu Lorebook'); + formData.append('convertedData', JSON.stringify(convertRisuLorebook(jsonData))); + } + } catch (error) { + toastr.error(`Error parsing file: ${error}`); + return; + } + + jQuery.ajax({ + type: 'POST', + url: '/importworldinfo', + data: formData, + beforeSend: () => { }, + cache: false, + contentType: false, + processData: false, + success: async function (data) { + if (data.name) { + await updateWorldInfoList(); + + const newIndex = world_names.indexOf(data.name); + if (newIndex >= 0) { + $('#world_editor_select').val(newIndex).trigger('change'); + } + + toastr.info(`World Info "${data.name}" imported successfully!`); + } + }, + error: (jqXHR, exception) => { }, + }); +} + +function assignLorebookToChat() { + const selectedName = chat_metadata[METADATA_KEY]; + const template = $('#chat_world_template .chat_world').clone(); + + const worldSelect = template.find('select'); + const chatName = template.find('.chat_name'); + chatName.text(getCurrentChatId()); + + for (const worldName of world_names) { + const option = document.createElement('option'); + option.value = worldName; + option.innerText = worldName; + option.selected = selectedName === worldName; + worldSelect.append(option); + } + + worldSelect.on('change', function () { + const worldName = $(this).val(); + + if (worldName) { + chat_metadata[METADATA_KEY] = worldName; + $('.chat_lorebook_button').addClass('world_set'); + } else { + delete chat_metadata[METADATA_KEY]; + $('.chat_lorebook_button').removeClass('world_set'); + } + + saveMetadata(); + }); + + callPopup(template, 'text'); +} + +jQuery(() => { + + $(document).ready(function () { + registerSlashCommand('world', onWorldInfoChange, [], '(optional name) – sets active World, or unsets if no args provided', true, true); + }); + + + $('#world_info').on('mousedown change', async function (e) { + // If there's no world names, don't do anything + if (world_names.length === 0) { + e.preventDefault(); + return; + } + + onWorldInfoChange('__notSlashCommand__'); + }); + + //**************************WORLD INFO IMPORT EXPORT*************************// + $('#world_import_button').on('click', function () { + $('#world_import_file').trigger('click'); + }); + + $('#world_import_file').on('change', async function (e) { + const file = e.target.files[0]; + + await importWorldInfo(file); + + // Will allow to select the same file twice in a row + e.target.value = ''; + }); + + $('#world_create_button').on('click', async () => { + const tempName = getFreeWorldName(); + const finalName = await callPopup('

Create a new World Info?

Enter a name for the new file:', 'input', tempName); + + if (finalName) { + await createNewWorldInfo(finalName); + } + }); + + $('#world_editor_select').on('change', async () => { + $('#world_info_search').val(''); + worldInfoFilter.setFilterData(FILTER_TYPES.WORLD_INFO_SEARCH, '', true); + const selectedIndex = String($('#world_editor_select').find(':selected').val()); + + if (selectedIndex === '') { + hideWorldEditor(); + } else { + const worldName = world_names[selectedIndex]; + showWorldEditor(worldName); + } + }); + + const saveSettings = () => { + saveSettingsDebounced(); + eventSource.emit(event_types.WORLDINFO_SETTINGS_UPDATED); + }; + + $('#world_info_depth').on('input', function () { + world_info_depth = Number($(this).val()); + $('#world_info_depth_counter').val($(this).val()); + saveSettings(); + }); + + $('#world_info_min_activations').on('input', function () { + world_info_min_activations = Number($(this).val()); + $('#world_info_min_activations_counter').val($(this).val()); + saveSettings(); + }); + + $('#world_info_min_activations_depth_max').on('input', function () { + world_info_min_activations_depth_max = Number($(this).val()); + $('#world_info_min_activations_depth_max_counter').val($(this).val()); + saveSettings(); + }); + + $('#world_info_budget').on('input', function () { + world_info_budget = Number($(this).val()); + $('#world_info_budget_counter').val($(this).val()); + saveSettings(); + }); + + $('#world_info_recursive').on('input', function () { + world_info_recursive = !!$(this).prop('checked'); + saveSettings(); + }); + + $('#world_info_case_sensitive').on('input', function () { + world_info_case_sensitive = !!$(this).prop('checked'); + saveSettings(); + }); + + $('#world_info_match_whole_words').on('input', function () { + world_info_match_whole_words = !!$(this).prop('checked'); + saveSettings(); + }); + + $('#world_info_character_strategy').on('change', function () { + world_info_character_strategy = Number($(this).val()); + saveSettings(); + }); + + $('#world_info_overflow_alert').on('change', function () { + world_info_overflow_alert = !!$(this).prop('checked'); + saveSettingsDebounced(); + }); + + $('#world_info_budget_cap').on('input', function () { + world_info_budget_cap = Number($(this).val()); + $('#world_info_budget_cap_counter').val(world_info_budget_cap); + saveSettings(); + }); + + $('#world_button').on('click', async function (event) { + const chid = $('#set_character_world').data('chid'); + + if (chid) { + const worldName = characters[chid]?.data?.extensions?.world; + const hasEmbed = checkEmbeddedWorld(chid); + if (worldName && world_names.includes(worldName) && !event.shiftKey) { + if (!$('#WorldInfo').is(':visible')) { + $('#WIDrawerIcon').trigger('click'); + } + const index = world_names.indexOf(worldName); + $('#world_editor_select').val(index).trigger('change'); + } else if (hasEmbed && !event.shiftKey) { + await importEmbeddedWorldInfo(); + saveCharacterDebounced(); + } + else { + $('#char-management-dropdown').val($('#set_character_world').val()).trigger('change'); + } + } + }); + + $('#world_info_search').on('input', function () { + const term = $(this).val(); + worldInfoFilter.setFilterData(FILTER_TYPES.WORLD_INFO_SEARCH, term); + }); + + $('#world_refresh').on('click', () => { + updateEditor(navigation_option.previous); + }); + + $('#world_info_sort_order').on('change', function () { + const value = String($(this).find(':selected').val()); + localStorage.setItem(SORT_ORDER_KEY, value); + updateEditor(navigation_option.none); + }); + + $(document).on('click', '.chat_lorebook_button', assignLorebookToChat); + + // Not needed on mobile + const deviceInfo = getDeviceInfo(); + if (deviceInfo && deviceInfo.device.type === 'desktop') { + $('#world_info').select2({ + width: '100%', + placeholder: 'No Worlds active. Click here to select.', + allowClear: true, + closeOnSelect: false, + }); + } +});