mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
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:
@ -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>
|
||||||
|
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(/\.[^/.]+$/, "")
|
||||||
|
@ -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);
|
||||||
|
Reference in New Issue
Block a user