#1226 Add chat-bound lorebooks

This commit is contained in:
Cohee
2023-10-16 23:03:42 +03:00
parent 8a3547ecd0
commit 6a1b230c7e
6 changed files with 99 additions and 9 deletions

View File

@ -3428,6 +3428,7 @@
<input type="hidden" id="fav_checkbox" name="fav" /> <input type="hidden" id="fav_checkbox" name="fav" />
<div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div> <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div>
<div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore" data-i18n="[title]Character Lore"></div> <div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore" data-i18n="[title]Character Lore"></div>
<div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore" data-i18n="[title]Chat Lore"></div>
<div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div> <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div>
<!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> --> <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> -->
<!-- <div id="set_character_world" class="menu_button fa-solid fa-globe" title="Set a character World Info / Lorebook"></div> --> <!-- <div id="set_character_world" class="menu_button fa-solid fa-globe" title="Set a character World Info / Lorebook"></div> -->
@ -3543,8 +3544,11 @@
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div> </div>
<div class="inline-drawer-content"> <div class="inline-drawer-content">
<div name="group-metadata-controls" class="marginTopBot5"> <div id="group-metadata-controls" class="marginTopBot5">
<input id="rm_group_chat_name" class="text_pole wide100p" type="text" name="chat_name" data-i18n="[placeholder]Chat Name (Optional)" placeholder="Chat Name (Optional)" maxlength="100" /> <div class="flex-container wide100p">
<input id="rm_group_chat_name" class="text_pole flex1" type="text" name="chat_name" data-i18n="[placeholder]Chat Name (Optional)" placeholder="Chat Name (Optional)" maxlength="100" />
<div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore" data-i18n="[title]Chat Lore"></div>
</div>
<div id="group_tags_div" class="wide100p"> <div id="group_tags_div" class="wide100p">
<div class="tag_controls"> <div class="tag_controls">
<input id="groupTagInput" class="text_pole tag_input flex1 margin0" data-i18n="[placeholder]Search / Create Tags" placeholder="Search / Create tags" maxlength="25" /> <input id="groupTagInput" class="text_pole tag_input flex1 margin0" data-i18n="[placeholder]Search / Create Tags" placeholder="Search / Create tags" maxlength="25" />
@ -3896,6 +3900,25 @@
</div> </div>
</div> </div>
<div id="chat_world_template" class="template_element">
<div class="chat_world range-block flexFlowColumn flex-container">
<div class="range-block-title">
<h4 data-i18n="Chat Lorebook">Chat Lorebook for <span class="chat_name"></span></h4>
</div>
<div class="range-block-counter justifyLeft flex-container flexFlowColumn margin-bot-10px">
<span data-i18n="A selected World Info will be bound to this chat.">
A selected World Info will be bound to this chat. When generating an AI reply,
it will be combined with the entries from global and character lorebooks.
</span>
</div>
<div class="range-block-range wide100p">
<select class="chat_world_info_selector wide100p">
<option value="">--- None ---</option>
</select>
</div>
</div>
</div>
<div id="character_world_template" class="template_element"> <div id="character_world_template" class="template_element">
<div class="character_world range-block flexFlowColumn flex-container"> <div class="character_world range-block flexFlowColumn flex-container">
<div class="range-block-title"> <div class="range-block-title">

View File

@ -5729,6 +5729,7 @@ export function select_selected_character(chid) {
checkEmbeddedWorld(chid); checkEmbeddedWorld(chid);
$("#form_create").attr("actiontype", "editcharacter"); $("#form_create").attr("actiontype", "editcharacter");
$('.form_create_bottom_buttons_block .chat_lorebook_button').show();
saveSettingsDebounced(); saveSettingsDebounced();
} }
@ -5787,6 +5788,7 @@ function select_rm_create() {
checkEmbeddedWorld(); checkEmbeddedWorld();
$("#form_create").attr("actiontype", "createcharacter"); $("#form_create").attr("actiontype", "createcharacter");
$('.form_create_bottom_buttons_block .chat_lorebook_button').hide();
} }
function select_rm_characters() { function select_rm_characters() {

View File

@ -1110,6 +1110,7 @@ function select_group_chats(groupId, skipAnimation) {
$("#rm_group_submit").hide(); $("#rm_group_submit").hide();
$("#rm_group_delete").show(); $("#rm_group_delete").show();
$("#rm_group_scenario").show(); $("#rm_group_scenario").show();
$('#group-metadata-controls .chat_lorebook_button').removeClass('disabled').prop('disabled', false);
} else { } else {
$("#rm_group_submit").show(); $("#rm_group_submit").show();
if ($("#groupAddMemberListToggle .inline-drawer-content").css('display') !== 'block') { if ($("#groupAddMemberListToggle .inline-drawer-content").css('display') !== 'block') {
@ -1117,6 +1118,7 @@ function select_group_chats(groupId, skipAnimation) {
} }
$("#rm_group_delete").hide(); $("#rm_group_delete").hide();
$("#rm_group_scenario").hide(); $("#rm_group_scenario").hide();
$('#group-metadata-controls .chat_lorebook_button').addClass('disabled').prop('disabled', true);
} }
updateFavButtonState(group?.fav ?? false); updateFavButtonState(group?.fav ?? false);

View File

@ -1,4 +1,4 @@
import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPrompt, MAX_INJECTION_DEPTH, extension_prompt_types, getExtensionPromptByName } from "../script.js"; import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPrompt, MAX_INJECTION_DEPTH, extension_prompt_types, getExtensionPromptByName, saveMetadata, getCurrentChatId } from "../script.js";
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition } from "./utils.js"; import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition } from "./utils.js";
import { extension_settings, getContext } from "./extensions.js"; import { extension_settings, getContext } from "./extensions.js";
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from "./authors-note.js"; import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from "./authors-note.js";
@ -53,6 +53,7 @@ let updateEditor = (navigation) => { navigation; };
// Do not optimize. updateEditor is a function that is updated by the displayWorldEntries with new data. // Do not optimize. updateEditor is a function that is updated by the displayWorldEntries with new data.
const worldInfoFilter = new FilterHelper(() => updateEditor()); const worldInfoFilter = new FilterHelper(() => updateEditor());
const SORT_ORDER_KEY = 'world_info_sort_order'; const SORT_ORDER_KEY = 'world_info_sort_order';
const METADATA_KEY = 'world_info';
const InputWidthReference = $("#WIInputWidthReference"); const InputWidthReference = $("#WIInputWidthReference");
@ -167,6 +168,11 @@ function setWorldInfoSettings(settings, data) {
$('#world_info_sort_order').val(localStorage.getItem(SORT_ORDER_KEY) || '0'); $('#world_info_sort_order').val(localStorage.getItem(SORT_ORDER_KEY) || '0');
$("#world_editor_select").trigger("change"); $("#world_editor_select").trigger("change");
eventSource.on(event_types.CHAT_CHANGED, () => {
const hasWorldInfo = !!chat_metadata[METADATA_KEY] && world_names.includes(chat_metadata[METADATA_KEY]);
$('.chat_lorebook_button').toggleClass('world_set', hasWorldInfo);
});
} }
// World Info Editor // World Info Editor
@ -1301,10 +1307,26 @@ async function getGlobalLore() {
return entries; return entries;
} }
async function getChatLore() {
const chatWorld = chat_metadata[METADATA_KEY];
if (!chatWorld) {
return [];
}
const data = await loadWorldInfoData(chatWorld);
const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
console.debug(`Chat lore has ${entries.length} entries`);
return entries;
}
async function getSortedEntries() { async function getSortedEntries() {
try { try {
const globalLore = await getGlobalLore(); const globalLore = await getGlobalLore();
const characterLore = await getCharacterLore(); const characterLore = await getCharacterLore();
const chatLore = await getChatLore();
let entries; let entries;
@ -1327,6 +1349,9 @@ async function getSortedEntries() {
break; break;
} }
// Chat lore always goes first
entries = [...chatLore.sort(sortFn), ...entries];
console.debug(`Sorted ${entries.length} world lore entries using strategy ${world_info_character_strategy}`); console.debug(`Sorted ${entries.length} world lore entries using strategy ${world_info_character_strategy}`);
// Need to deep clone the entries to avoid modifying the cached data // Need to deep clone the entries to avoid modifying the cached data
@ -1911,6 +1936,39 @@ export async function importWorldInfo(file) {
}); });
} }
function assignLorebookToChat() {
const selectedName = chat_metadata[METADATA_KEY];
const template = $('#chat_world_template .chat_world').clone();
const worldSelect = template.find('select');
const chatName = template.find('.chat_name');
chatName.text(getCurrentChatId());
for (const worldName of world_names) {
const option = document.createElement('option');
option.value = worldName;
option.innerText = worldName;
option.selected = selectedName === worldName;
worldSelect.append(option);
}
worldSelect.on('change', function () {
const worldName = $(this).val();
if (worldName) {
chat_metadata[METADATA_KEY] = worldName;
$('.chat_lorebook_button').addClass('world_set');
} else {
delete chat_metadata[METADATA_KEY];
$('.chat_lorebook_button').removeClass('world_set');
}
saveMetadata();
});
callPopup(template, 'text');
}
jQuery(() => { jQuery(() => {
$(document).ready(function () { $(document).ready(function () {
@ -2051,6 +2109,8 @@ jQuery(() => {
updateEditor(navigation_option.none); updateEditor(navigation_option.none);
}) })
$(document).on('click', '.chat_lorebook_button', assignLorebookToChat);
// Not needed on mobile // Not needed on mobile
const deviceInfo = getDeviceInfo(); const deviceInfo = getDeviceInfo();
if (deviceInfo && deviceInfo.device.type === 'desktop') { if (deviceInfo && deviceInfo.device.type === 'desktop') {

View File

@ -1316,7 +1316,7 @@ select option:not(:checked) {
} }
.menu_button.disabled { .menu_button.disabled {
filter: brightness(50%); filter: brightness(75%) grayscale(1);
cursor: not-allowed; cursor: not-allowed;
} }
@ -1911,10 +1911,10 @@ grammarly-extension {
font-weight: bold; font-weight: bold;
padding: 5px; padding: 5px;
margin: 0; margin: 0;
height: 32px; height: 26px;
filter: grayscale(0.5); filter: grayscale(0.5);
text-align: center; text-align: center;
font-size: 20px; font-size: 17px;
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
} }
@ -2307,7 +2307,7 @@ input[type="range"]::-webkit-slider-thumb {
#char-management-dropdown, #char-management-dropdown,
#tagInput { #tagInput {
height: 32px; height: 26px;
margin-bottom: 0; margin-bottom: 0;
} }

View File

@ -1811,15 +1811,18 @@ function convertWorldInfoToCharacterBook(name, entries) {
} }
function readWorldInfoFile(worldInfoName) { function readWorldInfoFile(worldInfoName) {
const dummyObject = { entries: {} };
if (!worldInfoName) { if (!worldInfoName) {
return { entries: {} }; return dummyObject;
} }
const filename = `${worldInfoName}.json`; const filename = `${worldInfoName}.json`;
const pathToWorldInfo = path.join(DIRECTORIES.worlds, filename); const pathToWorldInfo = path.join(DIRECTORIES.worlds, filename);
if (!fs.existsSync(pathToWorldInfo)) { if (!fs.existsSync(pathToWorldInfo)) {
throw new Error(`World info file ${filename} doesn't exist.`); console.log(`World info file ${filename} doesn't exist.`);
return dummyObject;
} }
const worldInfoText = fs.readFileSync(pathToWorldInfo, 'utf8'); const worldInfoText = fs.readFileSync(pathToWorldInfo, 'utf8');