diff --git a/public/index.html b/public/index.html index b306554fa..6949d697b 100644 --- a/public/index.html +++ b/public/index.html @@ -2937,6 +2937,21 @@ + +
+

+ Associate extra world info +

+
+
+ Associate extra lorebooks with this character. + NOTE: These choices are optional and won't be preserved on character export! +
+
+ +
diff --git a/public/script.js b/public/script.js index 7dc07cad9..fdd29b289 100644 --- a/public/script.js +++ b/public/script.js @@ -131,6 +131,7 @@ import { timestampToMoment, download, isDataURL, + getCharaFilename, } from "./scripts/utils.js"; import { extension_settings, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from "./scripts/extensions.js"; @@ -155,6 +156,7 @@ import { EventEmitter } from './scripts/eventemitter.js'; import { context_settings, loadContextTemplatesFromSettings } from "./scripts/context-template.js"; import { markdownExclusionExt } from "./scripts/showdown-exclusion.js"; import { NOTE_MODULE_NAME, metadata_keys, setFloatingPrompt, shouldWIAddPrompt } from "./scripts/extensions/floating-prompt/index.js"; +import { deviceInfo } from "./scripts/RossAscends-mods.js"; //exporting functions and vars for mods export { @@ -5492,7 +5494,7 @@ function openCharacterWorldPopup() { } function onSelectCharacterWorld() { - const value = $(this).find('option:selected').val(); + const value = $('.character_world_info_selector').find('option:selected').val(); const worldIndex = value !== '' ? Number(value) : NaN; const name = !isNaN(worldIndex) ? world_names[worldIndex] : ''; @@ -5509,12 +5511,42 @@ function openCharacterWorldPopup() { setWorldInfoButtonClass(undefined, !!value); } - const name = (menu_type == 'create' ? create_save.name : characters[chid]?.data?.name) || 'Nameless'; - const worldId = (menu_type == 'create' ? create_save.world : characters[chid]?.data?.extensions?.world) || ''; + function onExtraWorldInfoChanged() { + const selectedWorlds = $('.character_extra_world_info_selector').val(); + let charLore = world_info.charLore ?? []; + + // TODO: Maybe make this utility function not use the window context? + const fileName = getCharaFilename(chid); + const tempExtraBooks = selectedWorlds.map((index) => world_names[index]).filter((e) => e !== undefined); + + const existingCharLore = charLore.find((e) => e.name === fileName); + if (existingCharLore) { + if (tempExtraBooks.length === 0) { + charLore.splice(existingCharLore, 1); + } else { + existingCharLore.extraBooks = tempExtraBooks; + } + } else { + const newCharLoreEntry = { + name: fileName, + extraBooks: tempExtraBooks + } + + charLore.push(newCharLoreEntry); + } + Object.assign(world_info, { charLore: charLore }); + saveSettingsDebounced(); + } + const template = $('#character_world_template .character_world').clone(); const select = template.find('.character_world_info_selector'); + const extraSelect = template.find('.character_extra_world_info_selector'); + const name = (menu_type == 'create' ? create_save.name : characters[chid]?.data?.name) || 'Nameless'; + const worldId = (menu_type == 'create' ? create_save.world : characters[chid]?.data?.extensions?.world) || ''; template.find('.character_name').text(name); + + // Apped to base dropdown world_names.forEach((item, i) => { const option = document.createElement('option'); option.value = i; @@ -5523,7 +5555,42 @@ function openCharacterWorldPopup() { select.append(option); }); + // Append to extras dropdown + if (world_names.length > 0) { + extraSelect.empty(); + } + world_names.forEach((item, i) => { + const option = document.createElement('option'); + option.value = i; + option.innerText = item; + + const existingCharLore = world_info.charLore?.find((e) => e.name === getCharaFilename()); + if (existingCharLore) { + option.selected = existingCharLore.extraBooks.includes(item); + } else { + option.selected = false; + } + extraSelect.append(option); + }); + select.on('change', onSelectCharacterWorld); + extraSelect.on('mousedown change', async function (e) { + // If there's no world names, don't do anything + if (world_names.length === 0) { + e.preventDefault(); + return; + } + + if (deviceInfo && deviceInfo.device.type === 'desktop') { + e.preventDefault(); + const option = $(e.target); + option.prop('selected', !option.prop('selected')); + await delay(1); + } + + onExtraWorldInfoChanged(); + }); + callPopup(template, 'text'); } diff --git a/public/scripts/utils.js b/public/scripts/utils.js index 2087ffa90..0467c5ffe 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -457,9 +457,9 @@ export function isDataURL(str) { return regex.test(str); } -export function getCharaFilename() { +export function getCharaFilename(chid) { const context = getContext(); - const fileName = context.characters[context.characterId].avatar; + const fileName = context.characters[chid ?? context.characterId].avatar; if (fileName) { return fileName.replace(/\.[^/.]+$/, "") diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 1e9e26348..5428e851f 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -1,5 +1,5 @@ import { saveSettings, callPopup, substituteParams, getTokenCount, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type } from "../script.js"; -import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, delay } from "./utils.js"; +import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, delay, getCharaFilename } from "./utils.js"; import { getContext } from "./extensions.js"; import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from "./extensions/floating-prompt/index.js"; import { registerSlashCommand } from "./slash-commands.js"; @@ -26,7 +26,8 @@ const world_info_insertion_strategy = { global_first: 2, }; -let world_info = []; +let world_info = {}; +let selected_world_info = []; let world_names; let world_info_depth = 2; let world_info_budget = 25; @@ -35,7 +36,10 @@ let world_info_case_sensitive = false; let world_info_match_whole_words = false; let world_info_character_strategy = world_info_insertion_strategy.evenly; const saveWorldDebounced = debounce(async (name, data) => await _save(name, data), 1000); -const saveSettingsDebounced = debounce(() => saveSettings(), 1000); +const saveSettingsDebounced = debounce(() => { + Object.assign(world_info, { globalSelect: selected_world_info }) + saveSettings() +}, 1000); const sortFn = (a, b) => b.order - a.order; const world_info_position = { @@ -78,12 +82,19 @@ function setWorldInfoSettings(settings, data) { world_info_budget = 25; } - // Reset selected world from old string - if (typeof settings.world_info === "string") { - const existingWorldInfo = settings.world_info; - settings.world_info = [existingWorldInfo]; + // 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").text(world_info_depth); $("#world_info_depth").val(world_info_depth); @@ -98,18 +109,23 @@ function setWorldInfoSettings(settings, data) { $("#world_info_character_strategy").val(world_info_character_strategy); world_names = data.world_names?.length ? data.world_names : []; - world_info = settings.world_info?.filter((e) => world_names.includes(e)) ?? []; + + // 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_info").append(``); $("#world_editor_select").append(``); }); $("#world_editor_select").trigger("change"); + + // Update settings + saveSettingsDebounced(); } // World Info Editor @@ -162,7 +178,7 @@ async function updateWorldInfoList() { $("#world_editor_select").find('option[value!=""]').remove(); world_names.forEach((item, i) => { - $("#world_info").append(``); + $("#world_info").append(``); $("#world_editor_select").append(``); }); } @@ -593,7 +609,7 @@ async function renameWorldInfo(name, data) { return; } - const entryPreviouslySelected = world_info.findIndex((e) => e === oldName); + const entryPreviouslySelected = selected_world_info.findIndex((e) => e === oldName); await saveWorldInfo(newName, data, true); await deleteWorldInfo(oldName); @@ -622,9 +638,9 @@ async function deleteWorldInfo(worldInfoName) { }); if (response.ok) { - const existingWorldIndex = world_info.findIndex((e) => e === worldInfoName); + const existingWorldIndex = selected_world_info.findIndex((e) => e === worldInfoName); if (existingWorldIndex !== -1) { - world_info.splice(existingWorldIndex, 1); + selected_world_info.splice(existingWorldIndex, 1); saveSettingsDebounced(); } @@ -694,35 +710,48 @@ function transformString(str) { } async function getCharacterLore() { - const name = characters[this_chid]?.data?.extensions?.world; + const character = characters[this_chid]; + const name = character?.name; + let worldsToSearch = new Set(); - if (!name) { + 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 []; } - if (world_info.includes(name)) { - console.debug(`Character ${characters[this_chid]?.name} world info is the same as global: ${name}. 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]); } - if (!world_names.includes(name)) { - console.log(`Character ${characters[this_chid]?.name} world info does not exist: ${name}`); - return []; + 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; + } + + const data = await loadWorldInfoData(worldName); + const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : []; + entries = entries.concat(newEntries); } - const data = await loadWorldInfoData(name); - const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : []; - console.debug(`Character ${characters[this_chid]?.name} lore (${name}) has ${entries.length} world info entries`); + console.debug(`Character ${characters[this_chid]?.name} lore (${baseWorldName}) has ${entries.length} world info entries`); return entries; } async function getGlobalLore() { - if (!world_info) { + if (!selected_world_info) { return []; } let entries = []; - for (const worldName of world_info) { + 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); @@ -1091,7 +1120,6 @@ function onWorldInfoChange(_, text) { if (_ !== '__notSlashCommand__') { // if it's a slash command if (text !== undefined) { // and args are provided const slashInputSplitText = text.trim().toLowerCase().split(","); - console.log(slashInputSplitText); slashInputSplitText.forEach((worldName) => { const wiElement = getWIElement(worldName); @@ -1104,7 +1132,7 @@ function onWorldInfoChange(_, text) { }) } else { // if no args, unset all worlds toastr.success('Deactivated all worlds'); - world_info = []; + selected_world_info = []; $("#world_info").val(""); } } else { //if it's a pointer selection @@ -1122,7 +1150,7 @@ function onWorldInfoChange(_, text) { } }); } - world_info = tempWorldInfo; + selected_world_info = tempWorldInfo; } saveSettingsDebounced(); @@ -1137,6 +1165,12 @@ jQuery(() => { let selectScrollTop = null; $("#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; + } + if (deviceInfo.device.type === 'desktop') { e.preventDefault(); const option = $(e.target);