World Info: Add support for character layering

Some characters have different cards depending on what the user wants
from the character. However, maintaining multiple lorebooks for different
personas of the same character can be difficult. In addition, there
is redundancy, overlap, and possiblities to miss information when
creating separate lorebooks with the same base info.

Therefore, add a "DLC"/layering system of sorts for characters.
This works the same way as multi-global world info where character
lorebooks added as needed. The only catch is that a base character
book must be tied to a card before selecting any extra info.

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri
2023-06-25 22:27:02 -04:00
parent 47a5c9e9f6
commit b6fbe41f93
4 changed files with 150 additions and 34 deletions

View File

@ -2937,6 +2937,21 @@
<option value="">--- None ---</option>
</select>
</div>
<div class="range-block-title">
<h4>
Associate extra world info
</h4>
</div>
<div class="range-block-counter justifyLeft flex-container flexFlowColumn margin-bot-10px">
Associate extra lorebooks with this character.
NOTE: These choices are optional and won't be preserved on character export!
</div>
<div class="range-block-range wide100p">
<select class="character_extra_world_info_selector" multiple>
<option value="">-- World Info not found --</option>
</select>
</div>
</div>
</div>

View File

@ -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');
}

View File

@ -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(/\.[^/.]+$/, "")

View File

@ -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(`<option value='${i}'${world_info.includes(item) ? ' selected' : ''}>${item}</option>`);
$("#world_info").append(`<option value='${i}'${selected_world_info.includes(item) ? ' selected' : ''}>${item}</option>`);
$("#world_editor_select").append(`<option value='${i}'>${item}</option>`);
});
$("#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(`<option value='${i}'${world_info.includes(item) ? ' selected' : ''}>${item}</option>`);
$("#world_info").append(`<option value='${i}'${selected_world_info.includes(item) ? ' selected' : ''}>${item}</option>`);
$("#world_editor_select").append(`<option value='${i}'>${item}</option>`);
});
}
@ -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);