diff --git a/public/index.html b/public/index.html index 8e5b8b5fd..561b15d55 100644 --- a/public/index.html +++ b/public/index.html @@ -4813,6 +4813,8 @@ +

Persona Description

@@ -5539,26 +5541,6 @@
-
-
-
-

- Chat Lorebook for -

-
-
- - 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. - -
-
- -
-
-
diff --git a/public/scripts/personas.js b/public/scripts/personas.js index 01747a528..57f6026c7 100644 --- a/public/scripts/personas.js +++ b/public/scripts/personas.js @@ -21,8 +21,10 @@ import { PAGINATION_TEMPLATE, debounce, delay, download, ensureImageFormatSuppor import { debounce_timeout } from './constants.js'; import { FILTER_TYPES, FilterHelper } from './filters.js'; import { selected_group } from './group-chats.js'; -import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js'; +import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; import { t } from './i18n.js'; +import { world_names } from './world-info.js'; +import { renderTemplateAsync } from './templates.js'; let savePersonasPage = 0; const GRID_STORAGE_KEY = 'Personas_GridView'; @@ -375,6 +377,7 @@ export function initPersona(avatarId, personaName, personaDescription) { position: persona_description_positions.IN_PROMPT, depth: DEFAULT_DEPTH, role: DEFAULT_ROLE, + lorebook: '', }; saveSettingsDebounced(); @@ -418,6 +421,7 @@ export async function convertCharacterToPersona(characterId = null) { position: persona_description_positions.IN_PROMPT, depth: DEFAULT_DEPTH, role: DEFAULT_ROLE, + lorebook: '', }; // If the user is currently using this persona, update the description @@ -461,6 +465,7 @@ export function setPersonaDescription() { .val(power_user.persona_description_role) .find(`option[value="${power_user.persona_description_role}"]`) .prop('selected', String(true)); + $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook); countPersonaDescriptionTokens(); } @@ -490,6 +495,7 @@ async function updatePersonaNameIfExists(avatarId, newName) { position: persona_description_positions.IN_PROMPT, depth: DEFAULT_DEPTH, role: DEFAULT_ROLE, + lorebook: '', }; console.log(`Created persona name for ${avatarId} as ${newName}`); } @@ -535,6 +541,7 @@ async function bindUserNameToPersona(e) { position: isCurrentPersona ? power_user.persona_description_position : persona_description_positions.IN_PROMPT, depth: isCurrentPersona ? power_user.persona_description_depth : DEFAULT_DEPTH, role: isCurrentPersona ? power_user.persona_description_role : DEFAULT_ROLE, + lorebook: isCurrentPersona ? power_user.persona_description_lorebook : '', }; } @@ -579,12 +586,20 @@ function selectCurrentPersona() { power_user.persona_description_position = descriptor.position ?? persona_description_positions.IN_PROMPT; power_user.persona_description_depth = descriptor.depth ?? DEFAULT_DEPTH; power_user.persona_description_role = descriptor.role ?? DEFAULT_ROLE; + power_user.persona_description_lorebook = descriptor.lorebook ?? ''; } else { power_user.persona_description = ''; power_user.persona_description_position = persona_description_positions.IN_PROMPT; power_user.persona_description_depth = DEFAULT_DEPTH; power_user.persona_description_role = DEFAULT_ROLE; - power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.IN_PROMPT, depth: DEFAULT_DEPTH, role: DEFAULT_ROLE }; + power_user.persona_description_lorebook = ''; + power_user.persona_descriptions[user_avatar] = { + description: '', + position: persona_description_positions.IN_PROMPT, + depth: DEFAULT_DEPTH, + role: DEFAULT_ROLE, + lorebook: '', + }; } setPersonaDescription(); @@ -652,6 +667,7 @@ async function lockPersona() { position: persona_description_positions.IN_PROMPT, depth: DEFAULT_DEPTH, role: DEFAULT_ROLE, + lorebook: '', }; } @@ -731,6 +747,7 @@ function onPersonaDescriptionInput() { position: Number($('#persona_description_position').find(':selected').val()), depth: Number($('#persona_depth_value').val()), role: Number($('#persona_depth_role').find(':selected').val()), + lorebook: '', }; power_user.persona_descriptions[user_avatar] = object; } @@ -766,6 +783,43 @@ function onPersonaDescriptionDepthRoleInput() { saveSettingsDebounced(); } +async function onPersonaLoreButtonClick() { + const personaName = power_user.personas[user_avatar]; + const selectedLorebook = power_user.persona_description_lorebook; + + if (!personaName) { + toastr.warning(t`You must bind a name to this persona before you can set a lorebook.`, t`Persona name not set`); + return; + } + + const template = $(await renderTemplateAsync('personaLorebook')); + + const worldSelect = template.find('select'); + template.find('.persona_name').text(personaName); + + for (const worldName of world_names) { + const option = document.createElement('option'); + option.value = worldName; + option.innerText = worldName; + option.selected = selectedLorebook === worldName; + worldSelect.append(option); + } + + worldSelect.on('change', function () { + power_user.persona_description_lorebook = String($(this).val()); + + if (power_user.personas[user_avatar]) { + const object = getOrCreatePersonaDescriptor(); + object.lorebook = power_user.persona_description_lorebook; + } + + $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook); + saveSettingsDebounced(); + }); + + await callGenericPopup(template, POPUP_TYPE.TEXT); +} + function onPersonaDescriptionPositionInput() { power_user.persona_description_position = Number( $('#persona_description_position').find(':selected').val(), @@ -789,6 +843,7 @@ function getOrCreatePersonaDescriptor() { position: power_user.persona_description_position, depth: power_user.persona_description_depth, role: power_user.persona_description_role, + lorebook: power_user.persona_description_lorebook, }; power_user.persona_descriptions[user_avatar] = object; } @@ -1038,6 +1093,7 @@ async function duplicatePersona(avatarId) { position: descriptor?.position ?? persona_description_positions.IN_PROMPT, depth: descriptor?.depth ?? DEFAULT_DEPTH, role: descriptor?.role ?? DEFAULT_ROLE, + lorebook: descriptor?.lorebook ?? '', }; await uploadUserAvatar(getUserAvatar(avatarId), newAvatarId); @@ -1055,6 +1111,7 @@ export function initPersonas() { $('#persona_description_position').on('input', onPersonaDescriptionPositionInput); $('#persona_depth_value').on('input', onPersonaDescriptionDepthValueInput); $('#persona_depth_role').on('input', onPersonaDescriptionDepthRoleInput); + $('#persona_lore_button').on('click', onPersonaLoreButtonClick); $('#personas_backup').on('click', onBackupPersonas); $('#personas_restore').on('click', () => $('#personas_restore_input').trigger('click')); $('#personas_restore_input').on('change', onPersonasRestoreInput); diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 504d53b13..31e486110 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -262,6 +262,7 @@ let power_user = { persona_description_position: persona_description_positions.IN_PROMPT, persona_description_role: 0, persona_description_depth: 2, + persona_description_lorebook: '', persona_show_notifications: true, persona_sort_order: 'asc', @@ -1925,7 +1926,7 @@ export function fuzzySearchPersonas(data, searchValue, fuzzySearchCaches = null) const mappedData = data.map(x => ({ key: x, name: power_user.personas[x] ?? '', - description: power_user.persona_descriptions[x]?.description ?? '' + description: power_user.persona_descriptions[x]?.description ?? '', })); const keys = [ diff --git a/public/scripts/templates/chatLorebook.html b/public/scripts/templates/chatLorebook.html new file mode 100644 index 000000000..c23c466e4 --- /dev/null +++ b/public/scripts/templates/chatLorebook.html @@ -0,0 +1,18 @@ +
+
+

+ Chat Lorebook for +

+
+
+ + 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. + +
+
+ +
+
diff --git a/public/scripts/templates/personaLorebook.html b/public/scripts/templates/personaLorebook.html new file mode 100644 index 000000000..5a8a2b928 --- /dev/null +++ b/public/scripts/templates/personaLorebook.html @@ -0,0 +1,18 @@ +
+
+

+ Persona Lorebook for +

+
+
+ + A selected World Info will be bound to this persona. When generating an AI reply, + it will be combined with the entries from global, character and chat lorebooks. + +
+
+ +
+
diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 0f4351933..77ab6e249 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -3548,6 +3548,11 @@ async function getCharacterLore() { continue; } + if (power_user.persona_description_lorebook === worldName) { + console.debug(`[WI] Character ${name}'s world ${worldName} is already activated in persona lore! Skipping...`); + continue; + } + const data = await loadWorldInfo(worldName); const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : []; entries = entries.concat(newEntries); @@ -3598,11 +3603,45 @@ async function getChatLore() { return entries; } +async function getPersonaLore() { + const chatWorld = chat_metadata[METADATA_KEY]; + const personaWorld = power_user.persona_description_lorebook; + + if (!personaWorld) { + return []; + } + + if (chatWorld === personaWorld) { + console.debug(`[WI] Persona world ${personaWorld} is already activated in chat world! Skipping...`); + return []; + } + + if (selected_world_info.includes(personaWorld)) { + console.debug(`[WI] Persona world ${personaWorld} is already activated in global world info! Skipping...`); + return []; + } + + const data = await loadWorldInfo(personaWorld); + const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: personaWorld, ...rest })) : []; + + console.debug(`[WI] Persona lore has ${entries.length} entries`, [personaWorld]); + + return entries; +} + export async function getSortedEntries() { try { - const globalLore = await getGlobalLore(); - const characterLore = await getCharacterLore(); - const chatLore = await getChatLore(); + const [ + globalLore, + characterLore, + chatLore, + personaLore, + ] = await Promise.all([ + getGlobalLore(), + getCharacterLore(), + getChatLore(), + getPersonaLore(), + ]); let entries; @@ -3622,8 +3661,8 @@ export async function getSortedEntries() { break; } - // Chat lore always goes first - entries = [...chatLore.sort(sortFn), ...entries]; + // Chat lore always goes first, then persona lore, then the rest + entries = [...chatLore.sort(sortFn), ...personaLore.sort(sortFn), ...entries]; // Calculate hash and parse decorators. Split maps to preserve old hashes. entries = entries.map((entry) => { @@ -4816,9 +4855,9 @@ export async function importWorldInfo(file) { }); } -export function assignLorebookToChat() { +export async function assignLorebookToChat() { const selectedName = chat_metadata[METADATA_KEY]; - const template = $('#chat_world_template .chat_world').clone(); + const template = $(await renderTemplateAsync('chatLorebook')); const worldSelect = template.find('select'); const chatName = template.find('.chat_name'); @@ -4846,7 +4885,7 @@ export function assignLorebookToChat() { saveMetadata(); }); - callPopup(template, 'text'); + callGenericPopup(template, POPUP_TYPE.TEXT); } jQuery(() => {