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);