World Info: Add global multi-selection/merging

Global world info always involved some kind of merging leading to
extremely large lorebook files that took a long time to import.

This commit adds the ability to select more than one world info file
and they will be merged together along with character world info.
In short, multiple worlds can be meshed together to further contribute
to context.

You can also use this for world "DLCs" of sorts. Let's say someone
else has more information to add regarding a world, but doesn't
want to use a large world file. The JSONs can now be merged.

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri
2023-06-25 00:36:40 -04:00
parent 31057e1e81
commit fc9c90c4ee
3 changed files with 125 additions and 96 deletions

View File

@ -1876,17 +1876,17 @@
</div> </div>
<div id="wi-holder" class="margin5"> <div id="wi-holder" class="margin5">
<div class="wi-settings flex-container gap10px alignitemscenter"> <div class="wi-settings flex-container gap10px alignitemscenter">
<div class="flex1 flex-container flexFlowColumn"> <div class="flex range-block">
<div class="flex range-block"> <div class="range-block-title justifyLeft">
<div class="range-block-title justifyLeft"> <small>Active World</small>
<small>Active World</small>
</div>
<div class="range-block-range">
<select id="world_info" class="flexGrow margin0">
<option value="">--- None ---</option>
</select>
</div>
</div> </div>
<div class="range-block-range">
<select id="world_info" multiple>
<option value="">-- World Info not found --</option>
</select>
</div>
</div>
<div class="flex1 flex-container flexFlowColumn">
<div class="flex range-block"> <div class="flex range-block">
<div class="range-block-title justifyLeft"> <div class="range-block-title justifyLeft">
<label for="world_info_character_strategy"> <label for="world_info_character_strategy">
@ -1901,41 +1901,42 @@
</select> </select>
</div> </div>
</div> </div>
</div>
<div name="WIScanAndTokens" class="flex1 flex-container flexFlowColumn"> <div name="WIScanAndTokens" class="flex1 flex-container flexFlowColumn">
<div class="flex1 gap5px range-block"> <div class="flex1 gap5px range-block">
<div class="wide10pMinFit"> <div class="wide10pMinFit">
<small data-i18n="Scan Depth">Scan Depth</small> <small data-i18n="Scan Depth">Scan Depth</small>
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range paddingLeftRight5">
<input type="range" id="world_info_depth" name="volume" min="0" max="10" step="1">
</div> </div>
<div class="range-block-counter margin0"> <div class="range-block-range-and-counter">
<div contenteditable="true" data-for="world_info_depth" id="world_info_depth_counter"> <div class="range-block-range paddingLeftRight5">
depth <input type="range" id="world_info_depth" name="volume" min="0" max="10" step="1">
</div>
<div class="range-block-counter margin0">
<div contenteditable="true" data-for="world_info_depth" id="world_info_depth_counter">
depth
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="flex1 gap5px range-block">
<div class="flex1 gap5px range-block"> <div class="wide10pMinFit">
<div class="wide10pMinFit"> <small data-i18n="Token Budget">Context %</small>
<small data-i18n="Token Budget">Context %</small>
</div>
<div class="range-block-range-and-counter ">
<div class="range-block-range paddingLeftRight5">
<input type="range" id="world_info_budget" name="volume" min="1" max="100" step="1">
</div> </div>
<div class="range-block-counter margin0"> <div class="range-block-range-and-counter ">
<div contenteditable="true" data-for="world_info_budget" id="world_info_budget_counter"> <div class="range-block-range paddingLeftRight5">
budget <input type="range" id="world_info_budget" name="volume" min="1" max="100" step="1">
</div>
<div class="range-block-counter margin0">
<div contenteditable="true" data-for="world_info_budget" id="world_info_budget_counter">
budget
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="range-block flex-container flexFlowColumn"> <div class="range-block flex-container flexFlowColumn">
<label title="Entries can activate other entries by mentioning their keywords" class="checkbox_label"> <label title="Entries can activate other entries by mentioning their keywords" class="checkbox_label">
<input id="world_info_recursive" type="checkbox" /> <input id="world_info_recursive" type="checkbox" />

View File

@ -2033,7 +2033,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
let chat2 = []; let chat2 = [];
for (let i = coreChat.length - 1, j = 0; i >= 0; i--, j++) { for (let i = coreChat.length - 1, j = 0; i >= 0; i--, j++) {
// For OpenAI it's only used in WI // For OpenAI it's only used in WI
if (main_api == 'openai' && !world_info) { if (main_api == 'openai' && (!world_info || world_info.length === 0)) {
console.debug('No WI, skipping chat2 for OAI'); console.debug('No WI, skipping chat2 for OAI');
break; break;
} }

View File

@ -1,8 +1,9 @@
import { saveSettings, callPopup, substituteParams, getTokenCount, getRequestHeaders, chat_metadata, this_chid, characters } from "../script.js"; import { saveSettings, callPopup, substituteParams, getTokenCount, getRequestHeaders, chat_metadata, this_chid, characters } from "../script.js";
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer } from "./utils.js"; import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, delay } from "./utils.js";
import { getContext } from "./extensions.js"; import { getContext } from "./extensions.js";
import { metadata_keys, shouldWIAddPrompt } from "./extensions/floating-prompt/index.js"; import { metadata_keys, shouldWIAddPrompt } from "./extensions/floating-prompt/index.js";
import { registerSlashCommand } from "./slash-commands.js"; import { registerSlashCommand } from "./slash-commands.js";
import { deviceInfo } from "./RossAscends-mods.js";
export { export {
world_info, world_info,
@ -25,7 +26,7 @@ const world_info_insertion_strategy = {
global_first: 2, global_first: 2,
}; };
let world_info = null; let 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;
@ -77,6 +78,12 @@ function setWorldInfoSettings(settings, data) {
world_info_budget = 25; 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];
}
$("#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);
@ -91,20 +98,15 @@ 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)) ?? [];
if (settings.world_info != undefined) { if (world_names.length > 0) {
if (world_names.includes(settings.world_info)) { $("#world_info").empty();
world_info = settings.world_info;
}
} }
world_names.forEach((item, i) => { world_names.forEach((item, i) => {
$("#world_info").append(`<option value='${i}'>${item}</option>`); $("#world_info").append(`<option value='${i}'${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>`);
// preselect world if saved
if (item == world_info) {
$("#world_info").val(i).trigger('change');
}
}); });
$("#world_editor_select").trigger("change"); $("#world_editor_select").trigger("change");
@ -160,7 +162,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}'>${item}</option>`); $("#world_info").append(`<option value='${i}'${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>`);
}); });
} }
@ -170,6 +172,14 @@ function hideWorldEditor() {
displayWorldEntries(null, null); displayWorldEntries(null, null);
} }
function getWIElement(name) {
const wiElement = $("#world_info").children().filter(function() {
return $(this).text().toLowerCase() === name.toLowerCase()
});
return wiElement;
}
function nullWorldInfo() { function nullWorldInfo() {
toastr.info("Create or import a new World Info file first.", "World Info is not set", { timeOut: 10000, preventDuplicates: true }); toastr.info("Create or import a new World Info file first.", "World Info is not set", { timeOut: 10000, preventDuplicates: true });
} }
@ -224,7 +234,8 @@ function displayWorldEntries(name, data) {
return; return;
} }
await deleteWorldInfo(name, world_info); // Selected world_info automatically refreshes
await deleteWorldInfo(name);
}); });
// Check if a sortable instance exists // Check if a sortable instance exists
@ -582,19 +593,16 @@ async function renameWorldInfo(name, data) {
return; return;
} }
let selectNewName = null; const entryPreviouslySelected = world_info.findIndex((e) => e === oldName);
if (oldName === world_info) {
console.debug("Renaming current world info");
world_info = newName;
selectNewName = newName;
}
else {
console.debug("Renaming non-current world info");
selectNewName = world_info;
}
await saveWorldInfo(newName, data, true); await saveWorldInfo(newName, data, true);
await deleteWorldInfo(oldName, selectNewName); await deleteWorldInfo(oldName);
if (entryPreviouslySelected !== -1) {
const wiElement = getWIElement(newName);
wiElement.prop("selected", true);
$("#world_info").trigger('change');
}
const selectedIndex = world_names.indexOf(newName); const selectedIndex = world_names.indexOf(newName);
if (selectedIndex !== -1) { if (selectedIndex !== -1) {
@ -602,7 +610,7 @@ async function renameWorldInfo(name, data) {
} }
} }
async function deleteWorldInfo(worldInfoName, selectWorldName) { async function deleteWorldInfo(worldInfoName) {
if (!world_names.includes(worldInfoName)) { if (!world_names.includes(worldInfoName)) {
return; return;
} }
@ -614,15 +622,13 @@ async function deleteWorldInfo(worldInfoName, selectWorldName) {
}); });
if (response.ok) { if (response.ok) {
await updateWorldInfoList(); const existingWorldIndex = world_info.findIndex((e) => e === worldInfoName);
if (existingWorldIndex !== -1) {
const selectedIndex = world_names.indexOf(selectWorldName); world_info.splice(existingWorldIndex, 1);
if (selectedIndex !== -1) { saveSettingsDebounced();
$("#world_info").val(selectedIndex).trigger('change');
} else {
$("#world_info").val("").trigger('change');
} }
await updateWorldInfoList();
$('#world_editor_select').trigger('change'); $('#world_editor_select').trigger('change');
} }
} }
@ -663,16 +669,13 @@ async function createNewWorldInfo(worldInfoName) {
return; return;
} }
world_info = worldInfoName; await saveWorldInfo(worldInfoName, worldInfoTemplate, true);
await saveWorldInfo(world_info, worldInfoTemplate, true);
await updateWorldInfoList(); await updateWorldInfoList();
const selectedIndex = world_names.indexOf(worldInfoName); const selectedIndex = world_names.indexOf(worldInfoName);
if (selectedIndex !== -1) { if (selectedIndex !== -1) {
$("#world_info").val(selectedIndex).trigger('change');
$('#world_editor_select').val(selectedIndex).trigger('change'); $('#world_editor_select').val(selectedIndex).trigger('change');
} else { } else {
$("#world_info").val("").trigger('change');
hideWorldEditor(); hideWorldEditor();
} }
} }
@ -689,7 +692,7 @@ async function getCharacterLore() {
return []; return [];
} }
if (name === world_info) { if (world_info.includes(name)) {
console.debug(`Character ${characters[this_chid]?.name} world info is the same as global: ${name}. Skipping...`); console.debug(`Character ${characters[this_chid]?.name} world info is the same as global: ${name}. Skipping...`);
return []; return [];
} }
@ -710,13 +713,13 @@ async function getGlobalLore() {
return []; return [];
} }
if (!world_names.includes(world_info)) { let entries = [];
console.log(`Global ${characters[this_chid]?.name} world info does not exist: ${world_info}`); for (const worldName of world_info) {
return []; 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(world_info);
const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
console.debug(`Global world info has ${entries.length} entries`); console.debug(`Global world info has ${entries.length} entries`);
return entries; return entries;
@ -1024,28 +1027,44 @@ export async function importEmbeddedWorldInfo() {
} }
function onWorldInfoChange(_, text) { function onWorldInfoChange(_, text) {
let selectedWorld; let selectedWorlds;
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
let slashInputWorld = text.toLowerCase(); const slashInputSplitText = text.trim().toLowerCase().split(",");
$("#world_info").find(`option`).filter(function () { console.log(slashInputSplitText);
return $(this).text().toLowerCase() === slashInputWorld;
}).prop('selected', true); //matches arg with worldnames and selects; if none found, unsets world slashInputSplitText.forEach((worldName) => {
let setWorldName = $("#world_info").find(":selected").text(); //only for toastr display const wiElement = getWIElement(worldName);
toastr.success(`Active world: ${setWorldName}`); if (wiElement.length > 0) {
selectedWorld = $("#world_info").find(":selected").val(); wiElement.prop("selected", true);
} else { //if no args, unset world toastr.success(`Activated world: ${wiElement.text()}`);
toastr.success('Deselected World') } else {
toastr.error(`No world found named: ${worldName}`);
}
})
} else { // if no args, unset all worlds
toastr.success('Deactivated all worlds');
world_info = [];
$("#world_info").val(""); $("#world_info").val("");
} }
} else { //if it's a pointer selection } else { //if it's a pointer selection
selectedWorld = $("#world_info").find(":selected").val(); let tempWorldInfo = [];
} let selectedWorlds = $("#world_info").val().map((e) => Number(e)).filter((e) => !isNaN(e));
world_info = null; if (selectedWorlds.length > 0) {
if (selectedWorld !== "") { selectedWorlds.forEach((worldIndex) => {
const worldIndex = Number(selectedWorld); const existingWorldName = world_names[worldIndex];
world_info = !isNaN(worldIndex) ? world_names[worldIndex] : null; if (existingWorldName) {
tempWorldInfo.push(existingWorldName);
} else {
const wiElement = getWIElement(existingWorldName);
wiElement.prop("selected", false);
toastr.error(`The world with ${existingWorldName} is invalid or corrupted.`);
}
});
}
world_info = tempWorldInfo;
} }
saveSettingsDebounced(); saveSettingsDebounced();
} }
@ -1054,7 +1073,16 @@ jQuery(() => {
$(document).ready(function () { $(document).ready(function () {
registerSlashCommand('world', onWorldInfoChange, [], " sets active World, or unsets if no args provided", true, true); registerSlashCommand('world', onWorldInfoChange, [], " sets active World, or unsets if no args provided", true, true);
}) })
$("#world_info").on('change', async function () { onWorldInfoChange('__notSlashCommand__') }); $("#world_info").on('mousedown change', async function (e) {
if (deviceInfo.device.type === 'desktop') {
e.preventDefault();
const option = $(e.target);
option.prop('selected', !option.prop('selected'));
await delay(1);
}
onWorldInfoChange('__notSlashCommand__');
});
//**************************WORLD INFO IMPORT EXPORT*************************// //**************************WORLD INFO IMPORT EXPORT*************************//
$("#world_import_button").on('click', function () { $("#world_import_button").on('click', function () {