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> <option value="">--- None ---</option>
</select> </select>
</div> </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>
</div> </div>

View File

@ -131,6 +131,7 @@ import {
timestampToMoment, timestampToMoment,
download, download,
isDataURL, isDataURL,
getCharaFilename,
} from "./scripts/utils.js"; } from "./scripts/utils.js";
import { extension_settings, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from "./scripts/extensions.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 { context_settings, loadContextTemplatesFromSettings } from "./scripts/context-template.js";
import { markdownExclusionExt } from "./scripts/showdown-exclusion.js"; import { markdownExclusionExt } from "./scripts/showdown-exclusion.js";
import { NOTE_MODULE_NAME, metadata_keys, setFloatingPrompt, shouldWIAddPrompt } from "./scripts/extensions/floating-prompt/index.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 //exporting functions and vars for mods
export { export {
@ -5492,7 +5494,7 @@ function openCharacterWorldPopup() {
} }
function onSelectCharacterWorld() { 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 worldIndex = value !== '' ? Number(value) : NaN;
const name = !isNaN(worldIndex) ? world_names[worldIndex] : ''; const name = !isNaN(worldIndex) ? world_names[worldIndex] : '';
@ -5509,12 +5511,42 @@ function openCharacterWorldPopup() {
setWorldInfoButtonClass(undefined, !!value); setWorldInfoButtonClass(undefined, !!value);
} }
const name = (menu_type == 'create' ? create_save.name : characters[chid]?.data?.name) || 'Nameless'; function onExtraWorldInfoChanged() {
const worldId = (menu_type == 'create' ? create_save.world : characters[chid]?.data?.extensions?.world) || ''; 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 template = $('#character_world_template .character_world').clone();
const select = template.find('.character_world_info_selector'); 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); template.find('.character_name').text(name);
// Apped to base dropdown
world_names.forEach((item, i) => { world_names.forEach((item, i) => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = i; option.value = i;
@ -5523,7 +5555,42 @@ function openCharacterWorldPopup() {
select.append(option); 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); 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'); callPopup(template, 'text');
} }

View File

@ -457,9 +457,9 @@ export function isDataURL(str) {
return regex.test(str); return regex.test(str);
} }
export function getCharaFilename() { export function getCharaFilename(chid) {
const context = getContext(); const context = getContext();
const fileName = context.characters[context.characterId].avatar; const fileName = context.characters[chid ?? context.characterId].avatar;
if (fileName) { if (fileName) {
return fileName.replace(/\.[^/.]+$/, "") 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 { 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 { getContext } from "./extensions.js";
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from "./extensions/floating-prompt/index.js"; import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from "./extensions/floating-prompt/index.js";
import { registerSlashCommand } from "./slash-commands.js"; import { registerSlashCommand } from "./slash-commands.js";
@ -26,7 +26,8 @@ const world_info_insertion_strategy = {
global_first: 2, global_first: 2,
}; };
let world_info = []; let world_info = {};
let selected_world_info = [];
let world_names; let world_names;
let world_info_depth = 2; let world_info_depth = 2;
let world_info_budget = 25; 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_match_whole_words = false;
let world_info_character_strategy = world_info_insertion_strategy.evenly; let world_info_character_strategy = world_info_insertion_strategy.evenly;
const saveWorldDebounced = debounce(async (name, data) => await _save(name, data), 1000); 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 sortFn = (a, b) => b.order - a.order;
const world_info_position = { const world_info_position = {
@ -78,12 +82,19 @@ function setWorldInfoSettings(settings, data) {
world_info_budget = 25; world_info_budget = 25;
} }
// Reset selected world from old string // Reset selected world from old string and delete old keys
if (typeof settings.world_info === "string") { // TODO: Remove next release
const existingWorldInfo = settings.world_info; const existingWorldInfo = settings.world_info;
settings.world_info = [existingWorldInfo]; 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_counter").text(world_info_depth);
$("#world_info_depth").val(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_info_character_strategy").val(world_info_character_strategy);
world_names = data.world_names?.length ? data.world_names : []; 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) { if (world_names.length > 0) {
$("#world_info").empty(); $("#world_info").empty();
} }
world_names.forEach((item, i) => { 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").append(`<option value='${i}'>${item}</option>`);
}); });
$("#world_editor_select").trigger("change"); $("#world_editor_select").trigger("change");
// Update settings
saveSettingsDebounced();
} }
// World Info Editor // World Info Editor
@ -162,7 +178,7 @@ async function updateWorldInfoList() {
$("#world_editor_select").find('option[value!=""]').remove(); $("#world_editor_select").find('option[value!=""]').remove();
world_names.forEach((item, i) => { 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").append(`<option value='${i}'>${item}</option>`);
}); });
} }
@ -593,7 +609,7 @@ async function renameWorldInfo(name, data) {
return; return;
} }
const entryPreviouslySelected = world_info.findIndex((e) => e === oldName); const entryPreviouslySelected = selected_world_info.findIndex((e) => e === oldName);
await saveWorldInfo(newName, data, true); await saveWorldInfo(newName, data, true);
await deleteWorldInfo(oldName); await deleteWorldInfo(oldName);
@ -622,9 +638,9 @@ async function deleteWorldInfo(worldInfoName) {
}); });
if (response.ok) { if (response.ok) {
const existingWorldIndex = world_info.findIndex((e) => e === worldInfoName); const existingWorldIndex = selected_world_info.findIndex((e) => e === worldInfoName);
if (existingWorldIndex !== -1) { if (existingWorldIndex !== -1) {
world_info.splice(existingWorldIndex, 1); selected_world_info.splice(existingWorldIndex, 1);
saveSettingsDebounced(); saveSettingsDebounced();
} }
@ -694,35 +710,48 @@ function transformString(str) {
} }
async function getCharacterLore() { 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 []; return [];
} }
if (world_info.includes(name)) { // TODO: Maybe make the utility function not use the window context?
console.debug(`Character ${characters[this_chid]?.name} world info is the same as global: ${name}. Skipping...`); const fileName = getCharaFilename(this_chid);
return []; const extraCharLore = world_info.charLore.find((e) => e.name === fileName);
if (extraCharLore) {
worldsToSearch = new Set([...worldsToSearch, ...extraCharLore.extraBooks]);
} }
if (!world_names.includes(name)) { let entries = [];
console.log(`Character ${characters[this_chid]?.name} world info does not exist: ${name}`); for (const worldName of worldsToSearch) {
return []; 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(name); const data = await loadWorldInfoData(worldName);
const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : []; const newEntries = 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`); entries = entries.concat(newEntries);
}
console.debug(`Character ${characters[this_chid]?.name} lore (${baseWorldName}) has ${entries.length} world info entries`);
return entries; return entries;
} }
async function getGlobalLore() { async function getGlobalLore() {
if (!world_info) { if (!selected_world_info) {
return []; return [];
} }
let entries = []; let entries = [];
for (const worldName of world_info) { for (const worldName of selected_world_info) {
const data = await loadWorldInfoData(worldName); const data = await loadWorldInfoData(worldName);
const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : []; const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
entries = entries.concat(newEntries); entries = entries.concat(newEntries);
@ -1091,7 +1120,6 @@ function onWorldInfoChange(_, text) {
if (_ !== '__notSlashCommand__') { // if it's a slash command if (_ !== '__notSlashCommand__') { // if it's a slash command
if (text !== undefined) { // and args are provided if (text !== undefined) { // and args are provided
const slashInputSplitText = text.trim().toLowerCase().split(","); const slashInputSplitText = text.trim().toLowerCase().split(",");
console.log(slashInputSplitText);
slashInputSplitText.forEach((worldName) => { slashInputSplitText.forEach((worldName) => {
const wiElement = getWIElement(worldName); const wiElement = getWIElement(worldName);
@ -1104,7 +1132,7 @@ function onWorldInfoChange(_, text) {
}) })
} else { // if no args, unset all worlds } else { // if no args, unset all worlds
toastr.success('Deactivated all worlds'); toastr.success('Deactivated all worlds');
world_info = []; selected_world_info = [];
$("#world_info").val(""); $("#world_info").val("");
} }
} else { //if it's a pointer selection } else { //if it's a pointer selection
@ -1122,7 +1150,7 @@ function onWorldInfoChange(_, text) {
} }
}); });
} }
world_info = tempWorldInfo; selected_world_info = tempWorldInfo;
} }
saveSettingsDebounced(); saveSettingsDebounced();
@ -1137,6 +1165,12 @@ jQuery(() => {
let selectScrollTop = null; let selectScrollTop = null;
$("#world_info").on('mousedown change', async function (e) { $("#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') { if (deviceInfo.device.type === 'desktop') {
e.preventDefault(); e.preventDefault();
const option = $(e.target); const option = $(e.target);