SillyTavern/public/scripts/world-info.js

2476 lines
88 KiB
JavaScript
Raw Normal View History

2023-12-02 19:04:51 +01:00
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';
2023-07-20 19:32:15 +02:00
export {
world_info,
world_info_budget,
world_info_depth,
world_info_min_activations,
world_info_min_activations_depth_max,
2023-07-20 19:32:15 +02:00
world_info_recursive,
world_info_overflow_alert,
2023-07-20 19:32:15 +02:00
world_info_case_sensitive,
world_info_match_whole_words,
world_info_character_strategy,
world_info_budget_cap,
2023-07-20 19:32:15 +02:00
world_names,
checkWorldInfo,
deleteWorldInfo,
setWorldInfoSettings,
getWorldInfoPrompt,
2023-12-02 20:11:06 +01:00
};
2023-07-20 19:32:15 +02:00
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;
2023-10-30 22:55:32 +01:00
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)
2023-10-30 22:55:32 +01:00
2023-07-20 19:32:15 +02:00
let world_info_budget = 25;
let world_info_recursive = false;
let world_info_overflow_alert = false;
2023-07-20 19:32:15 +02:00
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;
2023-07-20 19:32:15 +02:00
const saveWorldDebounced = debounce(async (name, data) => await _save(name, data), 1000);
const saveSettingsDebounced = debounce(() => {
2023-12-02 20:11:06 +01:00
Object.assign(world_info, { globalSelect: selected_world_info });
saveSettings();
2023-07-20 19:32:15 +02:00
}, 1000);
const sortFn = (a, b) => b.order - a.order;
2023-08-21 20:10:11 +02:00
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';
2023-10-16 22:03:42 +02:00
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,
2023-12-02 20:11:06 +01:00
};
}
2023-07-20 19:32:15 +02:00
const world_info_position = {
before: 0,
after: 1,
ANTop: 2,
ANBottom: 3,
2023-09-21 09:04:34 +02:00
atDepth: 4,
2023-07-20 19:32:15 +02:00
};
const worldInfoCache = {};
async function getWorldInfoPrompt(chat2, maxContext) {
2023-12-02 19:04:51 +01:00
let worldInfoString = '', worldInfoBefore = '', worldInfoAfter = '';
2023-07-20 19:32:15 +02:00
const activatedWorldInfo = await checkWorldInfo(chat2, maxContext);
worldInfoBefore = activatedWorldInfo.worldInfoBefore;
worldInfoAfter = activatedWorldInfo.worldInfoAfter;
worldInfoString = worldInfoBefore + worldInfoAfter;
return {
worldInfoString,
worldInfoBefore,
worldInfoAfter,
2023-12-02 21:06:57 +01:00
worldInfoDepth: activatedWorldInfo.WIDepthEntries,
};
2023-07-20 19:32:15 +02:00
}
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);
2023-07-20 19:32:15 +02:00
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);
2023-07-20 19:32:15 +02:00
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);
2023-07-20 19:32:15 +02:00
// 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;
2023-12-02 19:04:51 +01:00
if (typeof existingWorldInfo === 'string') {
2023-07-20 19:32:15 +02:00
delete settings.world_info;
selected_world_info = [existingWorldInfo];
} else if (Array.isArray(existingWorldInfo)) {
delete settings.world_info;
selected_world_info = existingWorldInfo;
}
2023-12-02 20:11:06 +01:00
world_info = settings.world_info ?? {};
2023-07-20 19:32:15 +02:00
2023-12-02 19:04:51 +01:00
$('#world_info_depth_counter').val(world_info_depth);
$('#world_info_depth').val(world_info_depth);
2023-07-20 19:32:15 +02:00
2023-12-02 19:04:51 +01:00
$('#world_info_min_activations_counter').val(world_info_min_activations);
$('#world_info_min_activations').val(world_info_min_activations);
2023-12-02 19:04:51 +01:00
$('#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);
2023-12-02 19:04:51 +01:00
$('#world_info_budget_counter').val(world_info_budget);
$('#world_info_budget').val(world_info_budget);
2023-07-20 19:32:15 +02:00
2023-12-02 19:04:51 +01:00
$('#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);
2023-07-20 19:32:15 +02:00
$(`#world_info_character_strategy option[value='${world_info_character_strategy}']`).prop('selected', true);
2023-12-02 19:04:51 +01:00
$('#world_info_character_strategy').val(world_info_character_strategy);
2023-07-20 19:32:15 +02:00
2023-12-02 19:04:51 +01:00
$('#world_info_budget_cap').val(world_info_budget_cap);
$('#world_info_budget_cap_counter').val(world_info_budget_cap);
2023-07-20 19:32:15 +02:00
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) {
2023-12-02 19:04:51 +01:00
$('#world_info').empty();
2023-07-20 19:32:15 +02:00
}
world_names.forEach((item, i) => {
2023-12-02 19:04:51 +01:00
$('#world_info').append(`<option value='${i}'${selected_world_info.includes(item) ? ' selected' : ''}>${item}</option>`);
$('#world_editor_select').append(`<option value='${i}'>${item}</option>`);
2023-07-20 19:32:15 +02:00
});
$('#world_info_sort_order').val(localStorage.getItem(SORT_ORDER_KEY) || '0');
2023-12-02 19:04:51 +01:00
$('#world_editor_select').trigger('change');
2023-10-16 22:03:42 +02:00
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() {
2023-12-01 20:51:49 +01:00
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)) {
2023-12-01 01:25:55 +01:00
toastr.warning('Valid World Info file name is required');
return '';
}
const data = await loadWorldInfoData(file);
2023-12-02 19:04:51 +01:00
if (!data || !('entries' in data)) {
2023-12-01 01:25:55 +01:00
toastr.warning('World Info file has an invalid format');
return '';
}
const entries = Object.values(data.entries);
if (!entries || entries.length === 0) {
2023-12-01 01:25:55 +01:00
toastr.warning('World Info file has no entries');
return '';
}
return entries;
}
async function getChatBookCallback() {
const chatId = getCurrentChatId();
if (!chatId) {
2023-12-01 01:25:55 +01:00
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) {
2023-12-01 20:51:49 +01:00
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(', ');
}
2023-12-01 20:51:49 +01:00
return substituteParams(String(fieldValue));
}
async function createEntryCallback(args, content) {
const file = resolveVariable(args.file);
const key = args.key;
const data = await loadWorldInfoData(file);
2023-12-02 19:04:51 +01:00
if (!data || !('entries' in data)) {
2023-12-01 01:25:55 +01:00
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);
2023-12-01 20:51:49 +01:00
reloadEditor(file);
2023-12-01 20:51:49 +01:00
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 '';
}
2023-12-01 20:51:49 +01:00
const data = await loadWorldInfoData(file);
2023-12-02 19:04:51 +01:00
if (!data || !('entries' in data)) {
2023-12-01 20:51:49 +01:00
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);
2023-12-02 19:04:51 +01:00
registerSlashCommand('findentry', findBookEntryCallback, ['findlore', 'findwi'], '<span class="monospace">(file=bookName field=field [texts])</span> 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. <tt>/findentry file=chatLore field=key Shadowfang</tt>', true, true);
registerSlashCommand('getentryfield', getEntryFieldCallback, ['getlorefield', 'getwifield'], '<span class="monospace">(file=bookName field=field [UID])</span> get a field value (default: content) of the record with the UID from the specified book and pass it down the pipe, e.g. <tt>/getentryfield file=chatLore field=content 123</tt>', true, true);
registerSlashCommand('createentry', createEntryCallback, ['createlore', 'createwi'], '<span class="monospace">(file=bookName key=key [content])</span> 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. <tt>/createentry file=chatLore key=Shadowfang The sword of the king</tt>', true, true);
2023-12-01 20:51:49 +01:00
registerSlashCommand('setentryfield', setEntryFieldCallback, ['setlorefield', 'setwifield'], '<span class="monospace">(file=bookName uid=UID field=field [value])</span> 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. <tt>/setentryfield file=chatLore uid=123 field=key Shadowfang,sword,weapon</tt>', true, true);
2023-07-20 19:32:15 +02:00
}
// 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];
}
2023-12-02 19:04:51 +01:00
const response = await fetch('/getworldinfo', {
method: 'POST',
2023-07-20 19:32:15 +02:00
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() {
2023-12-02 19:04:51 +01:00
const result = await fetch('/getsettings', {
method: 'POST',
2023-07-20 19:32:15 +02:00
headers: getRequestHeaders(),
body: JSON.stringify({}),
});
if (result.ok) {
var data = await result.json();
world_names = data.world_names?.length ? data.world_names : [];
2023-12-02 19:04:51 +01:00
$('#world_info').find('option[value!=""]').remove();
$('#world_editor_select').find('option[value!=""]').remove();
2023-07-20 19:32:15 +02:00
world_names.forEach((item, i) => {
2023-12-02 19:04:51 +01:00
$('#world_info').append(`<option value='${i}'${selected_world_info.includes(item) ? ' selected' : ''}>${item}</option>`);
$('#world_editor_select').append(`<option value='${i}'>${item}</option>`);
2023-07-20 19:32:15 +02:00
});
}
}
function hideWorldEditor() {
displayWorldEntries(null, null);
}
function getWIElement(name) {
2023-12-02 19:04:51 +01:00
const wiElement = $('#world_info').children().filter(function () {
2023-12-02 20:11:06 +01:00
return $(this).text().toLowerCase() === name.toLowerCase();
2023-07-20 19:32:15 +02:00
});
return wiElement;
}
/**
* @param {any[]} data WI entries
* @returns {any[]} Sorted data
*/
function sortEntries(data) {
2023-12-02 19:04:51 +01:00
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;
2023-11-11 19:16:57 +01:00
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 {
2023-10-06 00:18:50 +02:00
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));
2023-10-06 00:18:50 +02:00
};
const secondarySort = (a, b) => a.order - b.order;
2023-10-06 00:18:50 +02:00
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;
}
2023-07-20 19:32:15 +02:00
function nullWorldInfo() {
2023-12-02 19:04:51 +01:00
toastr.info('Create or import a new World Info file first.', 'World Info is not set', { timeOut: 10000, preventDuplicates: true });
2023-07-20 19:32:15 +02:00
}
2023-08-21 20:10:11 +02:00
function displayWorldEntries(name, data, navigation = navigation_option.none) {
updateEditor = (navigation) => displayWorldEntries(name, data, navigation);
2023-12-02 19:04:51 +01:00
$('#world_popup_entries_list').empty().show();
2023-07-20 19:32:15 +02:00
2023-12-02 19:04:51 +01:00
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('');
2023-07-20 19:32:15 +02:00
return;
}
2023-08-21 20:10:11 +02:00
function getDataArray(callback) {
// Convert the data.entries object into an array
let entriesArray = Object.keys(data.entries).map(uid => {
2023-08-21 20:10:11 +02:00
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;
2023-08-21 20:10:11 +02:00
}
let startPage = 1;
if (navigation === navigation_option.previous) {
2023-12-02 19:04:51 +01:00
startPage = $('#world_info_pagination').pagination('getCurrentPageNum');
2023-08-21 20:10:11 +02:00
}
2023-07-20 19:32:15 +02:00
2023-08-21 20:10:11 +02:00
const storageKey = 'WI_PerPage';
const perPageDefault = 25;
2023-12-02 19:04:51 +01:00
$('#world_info_pagination').pagination({
2023-08-21 20:10:11 +02:00
dataSource: getDataArray,
pageSize: Number(localStorage.getItem(storageKey)) || perPageDefault,
sizeChangerOptions: [10, 25, 50, 100],
showSizeChanger: true,
2023-08-21 20:10:11 +02:00
pageRange: 1,
pageNumber: startPage,
position: 'top',
showPageNumbers: false,
prevText: '<',
nextText: '>',
formatNavigator: PAGINATION_TEMPLATE,
showNavigator: true,
2023-11-11 19:16:57 +01:00
callback: function (/** @type {object[]} */ page) {
2023-12-02 19:04:51 +01:00
$('#world_popup_entries_list').empty();
2023-09-25 11:03:10 +02:00
const keywordHeaders = `
2023-10-05 22:55:39 +02:00
<div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 2.5em;">
<small class="flex1">
Title/Memo
</small>
2023-11-04 16:44:43 +01:00
<small style="width: calc(3.5em + 5px)">
Status
</small>
2023-11-04 16:44:43 +01:00
<small style="width: calc(3.5em + 20px)">
Position
</small>
2023-11-04 16:44:43 +01:00
<small style="width: calc(3.5em + 15px)">
Depth
</small>
2023-11-04 16:44:43 +01:00
<small style="width: calc(3.5em + 15px)">
Order
</small>
2023-11-04 16:44:43 +01:00
<small style="width: calc(3.5em + 15px)">
Trigger %
</small>
2023-12-02 20:11:06 +01:00
</div>`;
2023-11-04 19:02:38 +01:00
const blocks = page.map(entry => getWorldEntry(name, data, entry)).filter(x => x);
2023-11-11 19:16:57 +01:00
const isCustomOrder = $('#world_info_sort_order').find(':selected').data('rule') === 'custom';
if (!isCustomOrder) {
blocks.forEach(block => {
block.find('.drag-handle').remove();
});
}
2023-12-02 19:04:51 +01:00
$('#world_popup_entries_list').append(keywordHeaders);
$('#world_popup_entries_list').append(blocks);
2023-08-21 20:10:11 +02:00
},
afterSizeSelectorChange: function (e) {
localStorage.setItem(storageKey, e.target.value);
2023-12-02 21:06:57 +01:00
},
2023-08-21 20:10:11 +02:00
});
2023-07-20 19:32:15 +02:00
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;
2023-12-02 19:04:51 +01:00
$('#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);
});
2023-07-20 19:32:15 +02:00
}
2023-12-02 19:04:51 +01:00
$('#world_popup_new').off('click').on('click', () => {
2023-07-20 19:32:15 +02:00
createWorldInfoEntry(name, data);
});
2023-12-02 19:04:51 +01:00
$('#world_popup_name_button').off('click').on('click', async () => {
2023-07-20 19:32:15 +02:00
await renameWorldInfo(name, data);
});
2023-12-02 19:04:51 +01:00
$('#world_backfill_memos').off('click').on('click', async () => {
2023-10-05 22:56:31 +02:00
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];
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, entry.uid, 'comment', entry.comment);
2023-10-05 22:56:31 +02:00
counter++;
}
}
if (counter > 0) {
toastr.info(`Backfilled ${counter} titles`);
await saveWorldInfo(name, data, true);
updateEditor(navigation_option.previous);
}
});
2023-12-02 19:04:51 +01:00
$('#world_popup_export').off('click').on('click', () => {
2023-07-20 19:32:15 +02:00
if (name && data) {
const jsonValue = JSON.stringify(data);
const fileName = `${name}.json`;
2023-12-02 19:04:51 +01:00
download(jsonValue, fileName, 'application/json');
2023-07-20 19:32:15 +02:00
}
});
2023-12-02 19:04:51 +01:00
$('#world_popup_delete').off('click').on('click', async () => {
const confirmation = await callPopup(`<h3>Delete the World/Lorebook: "${name}"?</h3>This action is irreversible!`, 'confirm');
2023-07-20 19:32:15 +02:00
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');
}
2023-12-02 19:04:51 +01:00
$('#world_popup_entries_list').sortable({
delay: getSortableDelay(),
2023-12-02 19:04:51 +01:00
handle: '.drag-handle',
2023-07-20 19:32:15 +02:00
stop: async function (event, ui) {
2023-11-11 19:16:57 +01:00
const firstEntryUid = $('#world_popup_entries_list .world_entry').first().data('uid');
const minDisplayIndex = data?.entries[firstEntryUid]?.displayIndex ?? 0;
2023-07-20 19:32:15 +02:00
$('#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;
}
2023-11-11 19:16:57 +01:00
item.displayIndex = minDisplayIndex + index;
setOriginalDataValue(data, uid, 'extensions.display_index', item.displayIndex);
2023-07-20 19:32:15 +02:00
});
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);
2023-12-02 21:06:57 +01:00
},
2023-07-20 19:32:15 +02:00
});
//$("#world_popup_entries_list").disableSelection();
}
2023-12-01 20:51:49 +01:00
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',
2023-12-02 20:11:06 +01:00
};
2023-12-01 20:51:49 +01:00
2023-07-20 19:32:15 +02:00
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];
2023-12-02 16:21:57 +01:00
if (!Object.hasOwn(currentObject, part)) {
2023-07-20 19:32:15 +02:00
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);
}
}
}
2023-08-21 20:10:11 +02:00
function getWorldEntry(name, data, entry) {
2023-11-04 19:02:38 +01:00
if (!data.entries[entry.uid]) {
return;
}
2023-12-02 19:04:51 +01:00
const template = $('#entry_edit_template .world_entry').clone();
template.data('uid', entry.uid);
template.attr('uid', entry.uid);
2023-07-20 19:32:15 +02:00
// key
const keyInput = template.find('textarea[name="key"]');
2023-12-02 19:04:51 +01:00
keyInput.data('uid', entry.uid);
keyInput.on('click', function (event) {
2023-07-20 19:32:15 +02:00
// Prevent closing the drawer on clicking the input
event.stopPropagation();
});
2023-12-02 19:04:51 +01:00
keyInput.on('input', function () {
const uid = $(this).data('uid');
const value = String($(this).val());
2023-07-20 19:32:15 +02:00
resetScrollHeight(this);
data.entries[uid].key = value
2023-12-02 19:04:51 +01:00
.split(',')
2023-07-20 19:32:15 +02:00
.map((x) => x.trim())
.filter((x) => x);
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, uid, 'keys', data.entries[uid].key);
2023-07-20 19:32:15 +02:00
saveWorldInfo(name, data);
});
2023-12-02 19:04:51 +01:00
keyInput.val(entry.key.join(', ')).trigger('input');
//initScrollHeight(keyInput);
2023-07-20 19:32:15 +02:00
// logic AND/NOT
const selectiveLogicDropdown = template.find('select[name="entryLogicType"]');
2023-12-02 19:04:51 +01:00
selectiveLogicDropdown.data('uid', entry.uid);
2023-07-20 19:32:15 +02:00
2023-12-02 19:04:51 +01:00
selectiveLogicDropdown.on('click', function (event) {
event.stopPropagation();
2023-12-02 20:11:06 +01:00
});
2023-12-02 19:04:51 +01:00
selectiveLogicDropdown.on('input', function () {
const uid = $(this).data('uid');
2023-07-20 19:32:15 +02:00
const value = Number($(this).val());
2023-12-02 20:11:06 +01:00
console.debug(`logic for ${entry.uid} set to ${value}`);
2023-07-20 19:32:15 +02:00
data.entries[uid].selectiveLogic = !isNaN(value) ? value : 0;
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, uid, 'selectiveLogic', data.entries[uid].selectiveLogic);
2023-07-20 19:32:15 +02:00
saveWorldInfo(name, data);
});
2023-07-20 19:32:15 +02:00
template
.find(`select[name="entryLogicType"] option[value=${entry.selectiveLogic}]`)
2023-12-02 19:04:51 +01:00
.prop('selected', true)
.trigger('input');
2023-07-20 19:32:15 +02:00
// Character filter
2023-12-02 19:04:51 +01:00
const characterFilterLabel = template.find('label[for="characterFilter"] > small');
characterFilterLabel.text(entry.characterFilter?.isExclude ? 'Exclude Character(s)' : 'Filter to Character(s)');
// exclude characters checkbox
2023-12-02 19:04:51 +01:00
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) {
2023-11-14 22:54:08 +01:00
if (!value && data.entries[uid].characterFilter.names.length === 0 && data.entries[uid].characterFilter.tags.length === 0) {
delete data.entries[uid].characterFilter;
} else {
2023-12-02 20:11:06 +01:00
data.entries[uid].characterFilter.isExclude = value;
}
} else if (value) {
Object.assign(
data.entries[uid],
{
characterFilter: {
isExclude: true,
2023-11-14 22:54:08 +01:00
names: [],
tags: [],
2023-12-02 21:06:57 +01:00
},
},
);
}
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter);
saveWorldInfo(name, data);
});
2023-12-02 19:04:51 +01:00
characterExclusionInput.prop('checked', entry.characterFilter?.isExclude ?? false).trigger('input');
2023-12-02 19:04:51 +01:00
const characterFilter = template.find('select[name="characterFilter"]');
2023-12-02 20:11:06 +01:00
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,
});
}
2023-11-14 22:54:08 +01:00
const characters = getContext().characters;
characters.forEach((character) => {
const option = document.createElement('option');
2023-12-02 19:04:51 +01:00
const name = character.avatar.replace(/\.[^/.]+$/, '') ?? character.name;
2023-11-14 22:54:08 +01:00
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;
}
2023-12-02 19:04:51 +01:00
const uid = $(this).data('uid');
2023-11-14 22:54:08 +01:00
const selected = $(this).find(':selected');
if ((!selected || selected?.length === 0) && !data.entries[uid].characterFilter?.isExclude) {
delete data.entries[uid].characterFilter;
} else {
2023-11-14 22:54:08 +01:00
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,
2023-11-14 22:54:08 +01:00
names: names,
tags: tags,
2023-12-02 21:06:57 +01:00
},
},
);
}
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter);
saveWorldInfo(name, data);
});
2023-07-20 19:32:15 +02:00
// keysecondary
const keySecondaryInput = template.find('textarea[name="keysecondary"]');
2023-12-02 19:04:51 +01:00
keySecondaryInput.data('uid', entry.uid);
keySecondaryInput.on('input', function () {
const uid = $(this).data('uid');
const value = String($(this).val());
2023-07-20 19:32:15 +02:00
resetScrollHeight(this);
data.entries[uid].keysecondary = value
2023-12-02 19:04:51 +01:00
.split(',')
2023-07-20 19:32:15 +02:00
.map((x) => x.trim())
.filter((x) => x);
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, uid, 'secondary_keys', data.entries[uid].keysecondary);
2023-07-20 19:32:15 +02:00
saveWorldInfo(name, data);
});
2023-12-02 19:04:51 +01:00
keySecondaryInput.val(entry.keysecondary.join(', ')).trigger('input');
2023-07-20 19:32:15 +02:00
initScrollHeight(keySecondaryInput);
// comment
const commentInput = template.find('textarea[name="comment"]');
const commentToggle = template.find('input[name="addMemo"]');
2023-12-02 19:04:51 +01:00
commentInput.data('uid', entry.uid);
commentInput.on('input', function () {
const uid = $(this).data('uid');
2023-07-20 19:32:15 +02:00
const value = $(this).val();
resetScrollHeight(this);
2023-07-20 19:32:15 +02:00
data.entries[uid].comment = value;
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, uid, 'comment', data.entries[uid].comment);
2023-07-20 19:32:15 +02:00
saveWorldInfo(name, data);
});
2023-12-02 19:04:51 +01:00
commentToggle.data('uid', entry.uid);
commentToggle.on('input', function () {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
2023-07-20 19:32:15 +02:00
//console.log(value)
const commentContainer = $(this)
2023-12-02 19:04:51 +01:00
.closest('.world_entry')
.find('.commentContainer');
2023-07-20 19:32:15 +02:00
data.entries[uid].addMemo = value;
saveWorldInfo(name, data);
value ? commentContainer.show() : commentContainer.hide();
});
2023-12-02 19:04:51 +01:00
commentInput.val(entry.comment).trigger('input');
initScrollHeight(commentInput);
2023-12-02 19:04:51 +01:00
commentToggle.prop('checked', true /* entry.addMemo */).trigger('input');
2023-12-02 20:11:06 +01:00
commentToggle.parent().hide();
2023-07-20 19:32:15 +02:00
// content
2023-12-02 19:04:51 +01:00
const counter = template.find('.world_entry_form_token_counter');
2023-08-21 20:10:11 +02:00
const countTokensDebounced = debounce(function (counter, value) {
const numberOfTokens = getTokenCount(value);
$(counter).text(numberOfTokens);
}, 1000);
2023-07-20 19:32:15 +02:00
const contentInput = template.find('textarea[name="content"]');
2023-12-02 19:04:51 +01:00
contentInput.data('uid', entry.uid);
contentInput.on('input', function (_, { skipCount } = {}) {
const uid = $(this).data('uid');
2023-07-20 19:32:15 +02:00
const value = $(this).val();
data.entries[uid].content = value;
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, uid, 'content', data.entries[uid].content);
2023-07-20 19:32:15 +02:00
saveWorldInfo(name, data);
if (skipCount) {
return;
}
2023-07-20 19:32:15 +02:00
// count tokens
countTokensDebounced(counter, value);
2023-07-20 19:32:15 +02:00
});
2023-12-02 19:04:51 +01:00
contentInput.val(entry.content).trigger('input', { skipCount: true });
2023-07-20 19:32:15 +02:00
//initScrollHeight(contentInput);
template.find('.inline-drawer-toggle').on('click', function () {
if (counter.data('first-run')) {
counter.data('first-run', false);
countTokensDebounced(counter, contentInput.val());
}
});
2023-07-20 19:32:15 +02:00
// selective
const selectiveInput = template.find('input[name="selective"]');
2023-12-02 19:04:51 +01:00
selectiveInput.data('uid', entry.uid);
selectiveInput.on('input', function () {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
2023-07-20 19:32:15 +02:00
data.entries[uid].selective = value;
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, uid, 'selective', data.entries[uid].selective);
2023-07-20 19:32:15 +02:00
saveWorldInfo(name, data);
const keysecondary = $(this)
2023-12-02 19:04:51 +01:00
.closest('.world_entry')
.find('.keysecondary');
2023-07-20 19:32:15 +02:00
const keysecondarytextpole = $(this)
2023-12-02 19:04:51 +01:00
.closest('.world_entry')
.find('.keysecondarytextpole');
2023-07-20 19:32:15 +02:00
const keyprimarytextpole = $(this)
2023-12-02 19:04:51 +01:00
.closest('.world_entry')
.find('.keyprimarytextpole');
2023-07-20 19:32:15 +02:00
const keyprimaryHeight = keyprimarytextpole.outerHeight();
keysecondarytextpole.css('height', keyprimaryHeight + 'px');
value ? keysecondary.show() : keysecondary.hide();
});
//forced on, ignored if empty
2023-12-02 19:04:51 +01:00
selectiveInput.prop('checked', true /* entry.selective */).trigger('input');
2023-07-20 19:32:15 +02:00
selectiveInput.parent().hide();
// constant
/*
2023-07-20 19:32:15 +02:00
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");
*/
2023-07-20 19:32:15 +02:00
// order
const orderInput = template.find('input[name="order"]');
2023-12-02 19:04:51 +01:00
orderInput.data('uid', entry.uid);
orderInput.on('input', function () {
const uid = $(this).data('uid');
2023-07-20 19:32:15 +02:00
const value = Number($(this).val());
data.entries[uid].order = !isNaN(value) ? value : 0;
2023-12-02 20:11:06 +01:00
updatePosOrdDisplay(uid);
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, uid, 'insertion_order', data.entries[uid].order);
2023-07-20 19:32:15 +02:00
saveWorldInfo(name, data);
});
2023-12-02 19:04:51 +01:00
orderInput.val(entry.order).trigger('input');
2023-11-04 16:44:43 +01:00
orderInput.css('width', 'calc(3em + 15px)');
2023-07-20 19:32:15 +02:00
// probability
if (entry.probability === undefined) {
entry.probability = null;
}
// depth
const depthInput = template.find('input[name="depth"]');
2023-12-02 19:04:51 +01:00
depthInput.data('uid', entry.uid);
2023-12-02 19:04:51 +01:00
depthInput.on('input', function () {
const uid = $(this).data('uid');
const value = Number($(this).val());
data.entries[uid].depth = !isNaN(value) ? value : 0;
2023-12-02 20:11:06 +01:00
updatePosOrdDisplay(uid);
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, uid, 'extensions.depth', data.entries[uid].depth);
saveWorldInfo(name, data);
});
2023-12-02 19:04:51 +01:00
depthInput.val(entry.depth ?? DEFAULT_DEPTH).trigger('input');
2023-11-04 16:44:43 +01:00
depthInput.css('width', 'calc(3em + 15px)');
// Hide by default unless depth is specified
if (entry.position === world_info_position.atDepth) {
//depthInput.parent().hide();
}
2023-07-20 19:32:15 +02:00
const probabilityInput = template.find('input[name="probability"]');
2023-12-02 19:04:51 +01:00
probabilityInput.data('uid', entry.uid);
probabilityInput.on('input', function () {
const uid = $(this).data('uid');
2023-07-20 19:32:15 +02:00
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);
}
}
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, uid, 'extensions.probability', data.entries[uid].probability);
2023-07-20 19:32:15 +02:00
saveWorldInfo(name, data);
});
2023-12-02 19:04:51 +01:00
probabilityInput.val(entry.probability).trigger('input');
2023-11-04 16:44:43 +01:00
probabilityInput.css('width', 'calc(3em + 15px)');
2023-07-20 19:32:15 +02:00
// probability toggle
if (entry.useProbability === undefined) {
entry.useProbability = false;
}
const probabilityToggle = template.find('input[name="useProbability"]');
2023-12-02 19:04:51 +01:00
probabilityToggle.data('uid', entry.uid);
probabilityToggle.on('input', function () {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
2023-07-20 19:32:15 +02:00
data.entries[uid].useProbability = value;
const probabilityContainer = $(this)
2023-12-02 19:04:51 +01:00
.closest('.world_entry')
.find('.probabilityContainer');
2023-07-20 19:32:15 +02:00
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;
}
2023-12-02 19:04:51 +01:00
probabilityInput.val(data.entries[uid].probability).trigger('input');
2023-07-20 19:32:15 +02:00
});
//forced on, 100% by default
2023-12-02 19:04:51 +01:00
probabilityToggle.prop('checked', true /* entry.useProbability */).trigger('input');
2023-07-20 19:32:15 +02:00
probabilityToggle.parent().hide();
// position
if (entry.position === undefined) {
entry.position = 0;
}
const positionInput = template.find('select[name="position"]');
initScrollHeight(positionInput);
2023-12-02 19:04:51 +01:00
positionInput.data('uid', entry.uid);
positionInput.on('click', function (event) {
// Prevent closing the drawer on clicking the input
event.stopPropagation();
});
2023-12-02 19:04:51 +01:00
positionInput.on('input', function () {
const uid = $(this).data('uid');
2023-07-20 19:32:15 +02:00
const value = Number($(this).val());
data.entries[uid].position = !isNaN(value) ? value : 0;
if (value === world_info_position.atDepth) {
depthInput.prop('disabled', false);
2023-12-02 20:11:06 +01:00
depthInput.css('visibility', 'visible');
//depthInput.parent().show();
} else {
depthInput.prop('disabled', true);
2023-12-02 20:11:06 +01:00
depthInput.css('visibility', 'hidden');
//depthInput.parent().hide();
}
2023-12-02 20:11:06 +01:00
updatePosOrdDisplay(uid);
2023-07-20 19:32:15 +02:00
// Spec v2 only supports before_char and after_char
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, uid, 'position', data.entries[uid].position == 0 ? 'before_char' : 'after_char');
2023-07-20 19:32:15 +02:00
// Write the original value as extensions field
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, uid, 'extensions.position', data.entries[uid].position);
2023-07-20 19:32:15 +02:00
saveWorldInfo(name, data);
});
template
.find(`select[name="position"] option[value=${entry.position}]`)
2023-12-02 19:04:51 +01:00
.prop('selected', true)
.trigger('input');
2023-07-20 19:32:15 +02:00
2023-09-25 11:03:10 +02:00
//add UID above content box (less important doesn't need to be always visible)
2023-12-02 19:04:51 +01:00
template.find('.world_entry_form_uid_value').text(`(UID: ${entry.uid})`);
2023-07-20 19:32:15 +02:00
// disable
2023-10-04 21:41:10 +02:00
/*
2023-07-20 19:32:15 +02:00
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"]');
2023-12-02 19:04:51 +01:00
entryStateSelector.data('uid', entry.uid);
2023-12-02 20:11:06 +01:00
console.log(entry.uid);
2023-12-02 19:04:51 +01:00
entryStateSelector.on('click', function (event) {
// Prevent closing the drawer on clicking the input
event.stopPropagation();
});
2023-12-02 19:04:51 +01:00
entryStateSelector.on('input', function () {
const uid = entry.uid;
const value = $(this).val();
switch (value) {
2023-12-02 19:04:51 +01:00
case 'constant':
data.entries[uid].constant = true;
data.entries[uid].disable = false;
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, uid, 'enabled', true);
setOriginalDataValue(data, uid, 'constant', true);
template.removeClass('disabledWIEntry');
2023-12-02 20:11:06 +01:00
console.debug('set to constant');
break;
2023-12-02 19:04:51 +01:00
case 'normal':
data.entries[uid].constant = false;
data.entries[uid].disable = false;
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, uid, 'enabled', true);
setOriginalDataValue(data, uid, 'constant', false);
template.removeClass('disabledWIEntry');
2023-12-02 20:11:06 +01:00
console.debug('set to normal');
break;
2023-12-02 19:04:51 +01:00
case 'disabled':
data.entries[uid].constant = false;
data.entries[uid].disable = true;
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, uid, 'enabled', false);
setOriginalDataValue(data, uid, 'constant', false);
template.addClass('disabledWIEntry');
2023-12-02 20:11:06 +01:00
console.debug('set to disabled');
break;
}
saveWorldInfo(name, data);
2023-12-02 20:11:06 +01:00
});
const entryState = function () {
2023-12-02 20:11:06 +01:00
console.log(`constant: ${entry.constant}, disabled: ${entry.disable}`);
if (entry.constant === true) {
2023-12-02 20:11:06 +01:00
console.debug('found constant');
return 'constant';
} else if (entry.disable === true) {
2023-12-02 20:11:06 +01:00
console.debug('found disabled');
return 'disabled';
} else {
2023-12-02 20:11:06 +01:00
console.debug('found normal');
return 'normal';
}
2023-12-02 20:11:06 +01:00
};
template
.find(`select[name="entryStateSelector"] option[value=${entryState()}]`)
2023-12-02 19:04:51 +01:00
.prop('selected', true)
.trigger('input');
saveWorldInfo(name, data);
2023-07-20 19:32:15 +02:00
// exclude recursion
2023-07-20 19:32:15 +02:00
const excludeRecursionInput = template.find('input[name="exclude_recursion"]');
2023-12-02 19:04:51 +01:00
excludeRecursionInput.data('uid', entry.uid);
excludeRecursionInput.on('input', function () {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
2023-07-20 19:32:15 +02:00
data.entries[uid].excludeRecursion = value;
2023-12-02 19:04:51 +01:00
setOriginalDataValue(data, uid, 'extensions.exclude_recursion', data.entries[uid].excludeRecursion);
2023-07-20 19:32:15 +02:00
saveWorldInfo(name, data);
});
2023-12-02 19:04:51 +01:00
excludeRecursionInput.prop('checked', entry.excludeRecursion).trigger('input');
2023-07-20 19:32:15 +02:00
// delete button
2023-12-02 19:04:51 +01:00
const deleteButton = template.find('.delete_entry_button');
deleteButton.data('uid', entry.uid);
deleteButton.on('click', function () {
const uid = $(this).data('uid');
2023-07-20 19:32:15 +02:00
deleteWorldInfoEntry(data, uid);
deleteOriginalDataValue(data, uid);
saveWorldInfo(name, data);
2023-08-21 20:10:11 +02:00
updateEditor(navigation_option.previous);
2023-07-20 19:32:15 +02:00
});
template.find('.inline-drawer-content').css('display', 'none'); //entries start collapsed
function updatePosOrdDisplay(uid) {
// display position/order info left of keyword box
2023-12-02 20:11:06 +01:00
let entry = data.entries[uid];
let posText = entry.position;
switch (entry.position) {
case 0:
posText = '↑CD';
2023-12-02 20:11:06 +01:00
break;
case 1:
posText = 'CD↓';
2023-12-02 20:11:06 +01:00
break;
case 2:
posText = '↑AN';
2023-12-02 20:11:06 +01:00
break;
case 3:
posText = 'AN↓';
2023-12-02 20:11:06 +01:00
break;
case 4:
posText = `@D${entry.depth}`;
2023-12-02 20:11:06 +01:00
break;
}
2023-12-02 19:04:51 +01:00
template.find('.world_entry_form_position_value').text(`(${posText} ${entry.order})`);
}
2023-07-20 19:32:15 +02:00
return template;
}
async function deleteWorldInfoEntry(data, uid) {
2023-12-02 19:04:51 +01:00
if (!data || !('entries' in data)) {
2023-07-20 19:32:15 +02:00
return;
}
2023-09-05 11:05:20 +02:00
if (!confirm(`Delete the entry with UID: ${uid}? This action is irreversible!`)) {
2023-12-02 19:04:51 +01:00
throw new Error('User cancelled deletion');
2023-09-05 11:05:20 +02:00
}
2023-07-20 19:32:15 +02:00
delete data.entries[uid];
}
2023-12-01 20:51:49 +01:00
const newEntryTemplate = {
key: [],
keysecondary: [],
2023-12-02 19:04:51 +01:00
comment: '',
content: '',
2023-12-01 20:51:49 +01:00
constant: false,
selective: true,
selectiveLogic: 0,
addMemo: false,
order: 100,
position: 0,
disable: false,
excludeRecursion: false,
probability: 100,
useProbability: true,
2023-12-01 21:09:13 +01:00
depth: DEFAULT_DEPTH,
group: '',
2023-12-01 20:51:49 +01:00
};
function createWorldInfoEntry(name, data, fromSlashCommand = false) {
2023-07-20 19:32:15 +02:00
const newUid = getFreeWorldEntryUid(data);
if (!Number.isInteger(newUid)) {
2023-12-02 19:04:51 +01:00
console.error('Couldn\'t assign UID to a new entry');
2023-07-20 19:32:15 +02:00
return;
}
2023-12-01 20:51:49 +01:00
const newEntry = { uid: newUid, ...structuredClone(newEntryTemplate) };
2023-07-20 19:32:15 +02:00
data.entries[newUid] = newEntry;
if (!fromSlashCommand) {
updateEditor(newUid);
}
return newEntry;
2023-07-20 19:32:15 +02:00
}
async function _save(name, data) {
2023-12-02 19:04:51 +01:00
await fetch('/editworldinfo', {
method: 'POST',
2023-07-20 19:32:15 +02:00
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;
2023-12-02 19:04:51 +01:00
const newName = await callPopup('<h3>Rename World Info</h3>Enter a new name:', 'input', oldName);
2023-07-20 19:32:15 +02:00
if (oldName === newName || !newName) {
2023-12-02 19:04:51 +01:00
console.debug('World info rename cancelled');
2023-07-20 19:32:15 +02:00
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);
2023-12-02 19:04:51 +01:00
wiElement.prop('selected', true);
$('#world_info').trigger('change');
2023-07-20 19:32:15 +02:00
}
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;
}
2023-12-02 19:04:51 +01:00
const response = await fetch('/deleteworldinfo', {
method: 'POST',
2023-07-20 19:32:15 +02:00
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) {
2023-12-02 19:04:51 +01:00
if (!data || !('entries' in data)) {
2023-07-20 19:32:15 +02:00
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 {
2023-12-02 20:11:06 +01:00
console.debug(`Character ${name}'s base world could not be found or is empty! Skipping...`);
2023-07-20 19:32:15 +02:00
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;
}
2023-10-16 22:13:32 +02:00
if (chat_metadata[METADATA_KEY] === worldName) {
console.debug(`Character ${name}'s world ${worldName} is already activated in chat lore! Skipping...`);
continue;
}
2023-07-20 19:32:15 +02:00
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;
}
2023-10-16 22:03:42 +02:00
async function getChatLore() {
const chatWorld = chat_metadata[METADATA_KEY];
if (!chatWorld) {
return [];
}
2023-10-16 22:13:32 +02:00
if (selected_world_info.includes(chatWorld)) {
console.debug(`Chat world ${chatWorld} is already activated in global world info! Skipping...`);
return [];
}
2023-10-16 22:03:42 +02:00
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;
}
2023-07-20 19:32:15 +02:00
async function getSortedEntries() {
try {
const globalLore = await getGlobalLore();
const characterLore = await getCharacterLore();
2023-10-16 22:03:42 +02:00
const chatLore = await getChatLore();
2023-07-20 19:32:15 +02:00
let entries;
switch (Number(world_info_character_strategy)) {
case world_info_insertion_strategy.evenly:
2023-12-02 20:11:06 +01:00
console.debug('WI using evenly');
2023-07-20 19:32:15 +02:00
entries = [...globalLore, ...characterLore].sort(sortFn);
break;
case world_info_insertion_strategy.character_first:
2023-12-02 20:11:06 +01:00
console.debug('WI using char first');
2023-07-20 19:32:15 +02:00
entries = [...characterLore.sort(sortFn), ...globalLore.sort(sortFn)];
break;
case world_info_insertion_strategy.global_first:
2023-12-02 20:11:06 +01:00
console.debug('WI using global first');
2023-07-20 19:32:15 +02:00
entries = [...globalLore.sort(sortFn), ...characterLore.sort(sortFn)];
break;
default:
2023-12-02 19:04:51 +01:00
console.error('Unknown WI insertion strategy: ', world_info_character_strategy, 'defaulting to evenly');
2023-07-20 19:32:15 +02:00
entries = [...globalLore, ...characterLore].sort(sortFn);
break;
}
2023-10-16 22:03:42 +02:00
// Chat lore always goes first
entries = [...chatLore.sort(sortFn), ...entries];
2023-07-20 19:32:15 +02:00
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
2023-09-26 08:53:04 +02:00
return structuredClone(entries);
2023-07-20 19:32:15 +02:00
}
catch (e) {
console.error(e);
return [];
}
}
async function checkWorldInfo(chat, maxContext) {
const context = getContext();
const messagesToLookBack = world_info_depth * 2 || 1;
// Combine the chat
2023-12-02 19:04:51 +01:00
let textToScan = chat.slice(0, messagesToLookBack).join('');
2023-10-30 22:55:32 +01:00
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')) {
2023-12-02 20:11:06 +01:00
const depthPrompt = getExtensionPromptByName(key);
if (depthPrompt) {
2023-12-02 20:11:06 +01:00
textToScan = `${depthPrompt}\n${textToScan}`;
}
}
}
const anPrompt = getExtensionPromptByName(NOTE_MODULE_NAME);
if (anPrompt) {
2023-12-02 20:11:06 +01:00
textToScan = `${anPrompt}\n${textToScan}`;
}
}
// Transform the resulting string
textToScan = transformString(textToScan);
2023-07-20 19:32:15 +02:00
let needsToScan = true;
let token_budget_overflowed = false;
2023-07-20 19:32:15 +02:00
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})`);
2023-07-20 19:32:15 +02:00
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
2023-11-14 22:54:08 +01:00
if (entry.characterFilter && entry.characterFilter?.names?.length > 0) {
const nameIncluded = entry.characterFilter.names.includes(getCharaFilename());
2023-12-02 20:11:06 +01:00
const filtered = entry.characterFilter.isExclude ? nameIncluded : !nameIncluded;
if (filtered) {
2023-11-14 22:54:08 +01:00
console.debug(`WI entry ${entry.uid} filtered out by character`);
continue;
}
}
2023-11-14 22:54:08 +01:00
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;
}
}
}
}
2023-07-20 19:32:15 +02:00
if (failedProbabilityChecks.has(entry)) {
continue;
}
if (allActivatedEntries.has(entry) || entry.disable == true || (count > 1 && world_info_recursive && entry.excludeRecursion)) {
continue;
}
if (entry.constant) {
2023-12-02 20:11:06 +01:00
entry.content = substituteParams(entry.content);
2023-07-20 19:32:15 +02:00
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 notFlag = true;
2023-07-20 19:32:15 +02:00
primary: for (let key of entry.key) {
const substituted = substituteParams(key);
2023-12-02 20:11:06 +01:00
console.debug(`${entry.uid}: ${substituted}`);
2023-07-20 19:32:15 +02:00
if (substituted && matchKeys(textToScan, substituted.trim())) {
2023-12-02 20:11:06 +01:00
console.debug(`${entry.uid}: got primary match`);
2023-07-20 19:32:15 +02:00
//selective logic begins
if (
entry.selective && //all entries are selective now
Array.isArray(entry.keysecondary) && //always true
entry.keysecondary.length //ignore empties
) {
2023-12-02 20:11:06 +01:00
console.debug(`uid:${entry.uid}: checking logic: ${entry.selectiveLogic}`);
2023-07-20 19:32:15 +02:00
secondary: for (let keysecondary of entry.keysecondary) {
const secondarySubstituted = substituteParams(keysecondary);
console.debug(`uid:${entry.uid}: filtering ${secondarySubstituted}`);
//AND operator
if (selectiveLogic === 0) {
2023-12-02 20:11:06 +01:00
console.debug('saw AND logic, checking..');
2023-07-20 19:32:15 +02:00
if (secondarySubstituted && matchKeys(textToScan, secondarySubstituted.trim())) {
2023-12-02 20:11:06 +01:00
console.debug(`activating entry ${entry.uid} with AND found`);
2023-07-20 19:32:15 +02:00
activatedNow.add(entry);
break secondary;
}
}
//NOT operator
if (selectiveLogic === 1) {
2023-12-02 20:11:06 +01:00
console.debug(`uid ${entry.uid}: checking NOT logic for ${secondarySubstituted}`);
2023-07-20 19:32:15 +02:00
if (secondarySubstituted && matchKeys(textToScan, secondarySubstituted.trim())) {
2023-12-02 20:11:06 +01:00
console.debug(`uid ${entry.uid}: canceled; filtered out by ${secondarySubstituted}`);
notFlag = false;
2023-07-20 19:32:15 +02:00
break primary;
}
}
}
//handle cases where secondary is empty
} else {
2023-12-02 20:11:06 +01:00
console.debug(`uid ${entry.uid}: activated without filter logic`);
2023-07-20 19:32:15 +02:00
activatedNow.add(entry);
break primary;
}
2023-12-02 20:11:06 +01:00
} else { console.debug('no active entries for logic checks yet'); }
2023-07-20 19:32:15 +02:00
}
//for a NOT all entries must be checked, a single match invalidates activation
if (selectiveLogic === 1 && notFlag) {
2023-12-02 20:11:06 +01:00
console.debug(`${entry.uid}: activated; passed NOT filter`);
2023-11-25 21:26:41 +01:00
activatedNow.add(entry);
}
2023-07-20 19:32:15 +02:00
}
}
needsToScan = world_info_recursive && activatedNow.size > 0;
const newEntries = [...activatedNow]
.sort((a, b) => sortedEntries.indexOf(a) - sortedEntries.indexOf(b));
2023-12-02 19:04:51 +01:00
let newContent = '';
2023-07-20 19:32:15 +02:00
const textToScanTokens = getTokenCount(allActivatedText);
const probabilityChecksBefore = failedProbabilityChecks.size;
2023-12-02 20:11:06 +01:00
console.debug('-- PROBABILITY CHECKS BEGIN --');
2023-07-20 19:32:15 +02:00
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;
2023-12-02 20:11:06 +01:00
} else { console.debug(`uid:${entry.uid} passed probability check, inserting to prompt`); }
2023-07-20 19:32:15 +02:00
newContent += `${substituteParams(entry.content)}\n`;
if (textToScanTokens + getTokenCount(newContent) >= budget) {
2023-12-02 19:04:51 +01:00
console.debug('WI budget reached, stopping');
if (world_info_overflow_alert) {
2023-12-02 19:04:51 +01:00
console.log('Alerting');
2023-09-01 22:14:01 +02:00
toastr.warning(`World info budget reached after ${allActivatedEntries.size} entries.`, 'World Info');
}
2023-07-20 19:32:15 +02:00
needsToScan = false;
token_budget_overflowed = true;
2023-07-20 19:32:15 +02:00
break;
}
allActivatedEntries.add(entry);
console.debug('WI entry activated:', entry);
}
const probabilityChecksAfter = failedProbabilityChecks.size;
if ((probabilityChecksAfter - probabilityChecksBefore) === activatedNow.size) {
2023-12-02 19:04:51 +01:00
console.debug('WI probability checks failed for all activated entries, stopping');
2023-07-20 19:32:15 +02:00
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);
}
2023-10-30 22:55:32 +01:00
// world_info_min_activations
if (!needsToScan && !token_budget_overflowed) {
2023-10-30 22:55:32 +01:00
if (world_info_min_activations > 0 && (allActivatedEntries.size < world_info_min_activations)) {
2023-12-02 20:11:06 +01:00
let over_max = false;
over_max = (
world_info_min_activations_depth_max > 0 &&
minActivationMsgIndex > world_info_min_activations_depth_max
2023-12-02 20:11:06 +01:00
) || (minActivationMsgIndex >= chat.length);
if (!over_max) {
2023-12-02 20:11:06 +01:00
needsToScan = true;
2023-12-02 19:04:51 +01:00
textToScan = transformString(chat.slice(minActivationMsgIndex, minActivationMsgIndex + 1).join(''));
2023-12-02 20:11:06 +01:00
minActivationMsgIndex += 1;
2023-10-30 22:55:32 +01:00
}
}
}
2023-07-20 19:32:15 +02:00
}
// Forward-sorted list of entries for joining
const WIBeforeEntries = [];
const WIAfterEntries = [];
const ANTopEntries = [];
const ANBottomEntries = [];
const WIDepthEntries = [];
2023-07-20 19:32:15 +02:00
// 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;
2023-12-02 16:14:06 +01:00
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,
2023-12-02 21:06:57 +01:00
entries: [entry.content],
});
}
2023-12-02 15:07:08 +01:00
break;
2023-12-02 16:14:06 +01:00
}
2023-07-20 19:32:15 +02:00
default:
break;
}
});
2023-12-02 19:04:51 +01:00
const worldInfoBefore = WIBeforeEntries.length ? WIBeforeEntries.join('\n') : '';
const worldInfoAfter = WIAfterEntries.length ? WIAfterEntries.join('\n') : '';
2023-07-20 19:32:15 +02:00
if (shouldWIAddPrompt) {
const originalAN = context.extensionPrompts[NOTE_MODULE_NAME].value;
2023-12-02 20:11:06 +01:00
const ANWithWI = `${ANTopEntries.join('\n')}\n${originalAN}\n${ANBottomEntries.join('\n')}`;
2023-07-20 19:32:15 +02:00
context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth]);
}
return { worldInfoBefore, worldInfoAfter, WIDepthEntries };
2023-07-20 19:32:15 +02:00
}
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 {
2023-08-18 13:54:38 +02:00
const regex = new RegExp(`\\b${escapeRegex(transformedString)}\\b`);
2023-07-20 19:32:15 +02:00
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: '',
2023-07-20 19:32:15 +02:00
};
});
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: '',
2023-07-20 19:32:15 +02:00
};
});
return outputObj;
}
function convertNovelLorebook(inputObj) {
const outputObj = {
2023-12-02 21:06:57 +01:00
entries: {},
2023-07-20 19:32:15 +02:00
};
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: '',
2023-07-20 19:32:15 +02:00
};
});
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 || [],
2023-12-02 19:04:51 +01:00
comment: entry.comment || '',
2023-07-20 19:32:15 +02:00
content: entry.content,
constant: entry.constant || false,
selective: entry.selective || false,
order: entry.insertion_order,
2023-12-02 19:04:51 +01:00
position: entry.extensions?.position ?? (entry.position === 'before_char' ? world_info_position.before : world_info_position.after),
2023-07-20 19:32:15 +02:00
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,
2023-09-25 19:11:16 +02:00
depth: entry.extensions?.depth ?? DEFAULT_DEPTH,
selectiveLogic: entry.extensions?.selectiveLogic ?? 0,
group: entry.extensions?.group ?? '',
2023-07-20 19:32:15 +02:00
};
});
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);
2023-10-04 21:41:10 +02:00
2023-10-10 19:47:54 +02:00
if (power_user.world_import_dialog) {
2023-12-02 20:56:16 +01:00
const html = `<h3>This character has an embedded World/Lorebook.</h3>
<h3>Would you like to import it now?</h3>
<div class="m-b-1">If you want to import it later, select "Import Card Lore" in the "More..." dropdown menu on the character panel.</div>`;
const checkResult = (result) => {
if (result) {
importEmbeddedWorldInfo(true);
}
};
2023-12-02 21:06:57 +01:00
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' },
);
}
2023-07-20 19:32:15 +02:00
}
return true;
}
return false;
}
2023-10-04 21:41:10 +02:00
export async function importEmbeddedWorldInfo(skipPopup = false) {
2023-07-20 19:32:15 +02:00
const chid = $('#import_character_info').data('chid');
if (chid === undefined) {
return;
}
const bookName = characters[chid]?.data?.character_book?.name || `${characters[chid]?.name}'s Lorebook`;
const confirmationText = (`<h3>Are you sure you want to import "${bookName}"?</h3>`) + (world_names.includes(bookName) ? 'It will overwrite the World/Lorebook with the same name.' : '');
2023-10-04 21:41:10 +02:00
if (!skipPopup) {
const confirmation = await callPopup(confirmationText, 'confirm');
2023-07-20 19:32:15 +02:00
2023-10-04 21:41:10 +02:00
if (!confirmation) {
return;
}
2023-07-20 19:32:15 +02:00
}
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..
2023-12-02 19:04:51 +01:00
$('#WIDrawerIcon').trigger('click');
//..auto-opening the new imported WI
2023-12-02 19:04:51 +01:00
$('#world_editor_select').val(newIndex).trigger('change');
2023-07-20 19:32:15 +02:00
}
setWorldInfoButtonClass(chid, true);
}
function onWorldInfoChange(_, text) {
if (_ !== '__notSlashCommand__') { // if it's a slash command
2023-11-25 21:26:41 +01:00
if (text.trim() !== '') { // and args are provided
2023-12-02 19:04:51 +01:00
const slashInputSplitText = text.trim().toLowerCase().split(',');
2023-07-20 19:32:15 +02:00
slashInputSplitText.forEach((worldName) => {
const wiElement = getWIElement(worldName);
if (wiElement.length > 0) {
selected_world_info.push(wiElement.text());
2023-12-02 19:04:51 +01:00
wiElement.prop('selected', true);
2023-07-20 19:32:15 +02:00
toastr.success(`Activated world: ${wiElement.text()}`);
} else {
toastr.error(`No world found named: ${worldName}`);
}
});
2023-12-02 19:04:51 +01:00
$('#world_info').trigger('change');
2023-07-20 19:32:15 +02:00
} else { // if no args, unset all worlds
toastr.success('Deactivated all worlds');
selected_world_info = [];
2023-12-02 19:04:51 +01:00
$('#world_info').val(null).trigger('change');
2023-07-20 19:32:15 +02:00
}
} else { //if it's a pointer selection
let tempWorldInfo = [];
2023-12-02 19:04:51 +01:00
let selectedWorlds = $('#world_info').val().map((e) => Number(e)).filter((e) => !isNaN(e));
2023-07-20 19:32:15 +02:00
if (selectedWorlds.length > 0) {
selectedWorlds.forEach((worldIndex) => {
const existingWorldName = world_names[worldIndex];
if (existingWorldName) {
tempWorldInfo.push(existingWorldName);
} else {
const wiElement = getWIElement(existingWorldName);
2023-12-02 19:04:51 +01:00
wiElement.prop('selected', false);
2023-07-20 19:32:15 +02:00
toastr.error(`The world with ${existingWorldName} is invalid or corrupted.`);
}
});
}
selected_world_info = tempWorldInfo;
}
saveSettingsDebounced();
eventSource.emit(event_types.WORLDINFO_SETTINGS_UPDATED);
2023-07-20 19:32:15 +02:00
}
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({
2023-12-02 19:04:51 +01:00
type: 'POST',
url: '/importworldinfo',
2023-07-20 19:32:15 +02:00
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) {
2023-12-02 19:04:51 +01:00
$('#world_editor_select').val(newIndex).trigger('change');
2023-07-20 19:32:15 +02:00
}
toastr.info(`World Info "${data.name}" imported successfully!`);
}
},
error: (jqXHR, exception) => { },
});
}
2023-10-16 22:03:42 +02:00
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');
}
2023-07-20 19:32:15 +02:00
jQuery(() => {
$(document).ready(function () {
registerSlashCommand('world', onWorldInfoChange, [], '<span class="monospace">(optional name)</span> sets active World, or unsets if no args provided', true, true);
2023-12-02 20:11:06 +01:00
});
2023-07-20 19:32:15 +02:00
2023-12-02 19:04:51 +01:00
$('#world_info').on('mousedown change', async function (e) {
2023-07-20 19:32:15 +02:00
// If there's no world names, don't do anything
if (world_names.length === 0) {
e.preventDefault();
return;
}
onWorldInfoChange('__notSlashCommand__');
});
//**************************WORLD INFO IMPORT EXPORT*************************//
2023-12-02 19:04:51 +01:00
$('#world_import_button').on('click', function () {
$('#world_import_file').trigger('click');
2023-07-20 19:32:15 +02:00
});
2023-12-02 19:04:51 +01:00
$('#world_import_file').on('change', async function (e) {
2023-07-20 19:32:15 +02:00
const file = e.target.files[0];
await importWorldInfo(file);
// Will allow to select the same file twice in a row
e.target.value = '';
2023-07-20 19:32:15 +02:00
});
2023-12-02 19:04:51 +01:00
$('#world_create_button').on('click', async () => {
2023-07-20 19:32:15 +02:00
const tempName = getFreeWorldName();
2023-12-02 19:04:51 +01:00
const finalName = await callPopup('<h3>Create a new World Info?</h3>Enter a name for the new file:', 'input', tempName);
2023-07-20 19:32:15 +02:00
if (finalName) {
await createNewWorldInfo(finalName);
}
});
2023-12-02 19:04:51 +01:00
$('#world_editor_select').on('change', async () => {
$('#world_info_search').val('');
worldInfoFilter.setFilterData(FILTER_TYPES.WORLD_INFO_SEARCH, '', true);
2023-12-02 19:04:51 +01:00
const selectedIndex = String($('#world_editor_select').find(':selected').val());
2023-07-20 19:32:15 +02:00
2023-12-02 19:04:51 +01:00
if (selectedIndex === '') {
2023-07-20 19:32:15 +02:00
hideWorldEditor();
} else {
const worldName = world_names[selectedIndex];
showWorldEditor(worldName);
}
});
const saveSettings = () => {
2023-12-02 20:11:06 +01:00
saveSettingsDebounced();
eventSource.emit(event_types.WORLDINFO_SETTINGS_UPDATED);
2023-12-02 20:11:06 +01:00
};
2023-12-02 19:04:51 +01:00
$('#world_info_depth').on('input', function () {
2023-07-20 19:32:15 +02:00
world_info_depth = Number($(this).val());
2023-12-02 19:04:51 +01:00
$('#world_info_depth_counter').val($(this).val());
saveSettings();
2023-07-20 19:32:15 +02:00
});
2023-12-02 19:04:51 +01:00
$('#world_info_min_activations').on('input', function () {
world_info_min_activations = Number($(this).val());
2023-12-02 19:04:51 +01:00
$('#world_info_min_activations_counter').val($(this).val());
saveSettings();
});
2023-12-02 19:04:51 +01:00
$('#world_info_min_activations_depth_max').on('input', function () {
world_info_min_activations_depth_max = Number($(this).val());
2023-12-02 19:04:51 +01:00
$('#world_info_min_activations_depth_max_counter').val($(this).val());
saveSettings();
});
2023-12-02 19:04:51 +01:00
$('#world_info_budget').on('input', function () {
2023-07-20 19:32:15 +02:00
world_info_budget = Number($(this).val());
2023-12-02 19:04:51 +01:00
$('#world_info_budget_counter').val($(this).val());
saveSettings();
2023-07-20 19:32:15 +02:00
});
2023-12-02 19:04:51 +01:00
$('#world_info_recursive').on('input', function () {
2023-07-20 19:32:15 +02:00
world_info_recursive = !!$(this).prop('checked');
saveSettings();
2023-11-04 16:44:43 +01:00
});
2023-07-20 19:32:15 +02:00
$('#world_info_case_sensitive').on('input', function () {
world_info_case_sensitive = !!$(this).prop('checked');
saveSettings();
2023-11-04 16:44:43 +01:00
});
2023-07-20 19:32:15 +02:00
$('#world_info_match_whole_words').on('input', function () {
world_info_match_whole_words = !!$(this).prop('checked');
saveSettings();
2023-07-20 19:32:15 +02:00
});
$('#world_info_character_strategy').on('change', function () {
2023-10-17 12:55:02 +02:00
world_info_character_strategy = Number($(this).val());
saveSettings();
2023-07-20 19:32:15 +02:00
});
$('#world_info_overflow_alert').on('change', function () {
2023-08-07 21:12:50 +02:00
world_info_overflow_alert = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#world_info_budget_cap').on('input', function () {
world_info_budget_cap = Number($(this).val());
2023-12-02 19:04:51 +01:00
$('#world_info_budget_cap_counter').val(world_info_budget_cap);
saveSettings();
});
2023-07-20 19:32:15 +02:00
2023-10-17 12:55:02 +02:00
$('#world_button').on('click', async function (event) {
2023-07-20 19:32:15 +02:00
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) {
2023-07-20 19:32:15 +02:00
if (!$('#WorldInfo').is(':visible')) {
$('#WIDrawerIcon').trigger('click');
}
const index = world_names.indexOf(worldName);
2023-12-02 19:04:51 +01:00
$('#world_editor_select').val(index).trigger('change');
} else if (hasEmbed && !event.shiftKey) {
2023-07-20 19:32:15 +02:00
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);
2023-07-20 19:32:15 +02:00
});
2023-10-08 11:30:12 +02:00
$('#world_refresh').on('click', () => {
updateEditor(navigation_option.previous);
});
2023-11-11 19:16:57 +01:00
$('#world_info_sort_order').on('change', function () {
2023-12-02 19:04:51 +01:00
const value = String($(this).find(':selected').val());
2023-11-11 19:16:57 +01:00
localStorage.setItem(SORT_ORDER_KEY, value);
updateEditor(navigation_option.none);
2023-12-02 20:11:06 +01:00
});
2023-10-16 22:03:42 +02:00
$(document).on('click', '.chat_lorebook_button', assignLorebookToChat);
2023-07-20 19:32:15 +02:00
// Not needed on mobile
const deviceInfo = getDeviceInfo();
if (deviceInfo && deviceInfo.device.type === 'desktop') {
2023-07-20 19:32:15 +02:00
$('#world_info').select2({
width: '100%',
placeholder: 'No Worlds active. Click here to select.',
allowClear: true,
closeOnSelect: false,
});
}
2023-12-02 20:11:06 +01:00
});