[wip] Persona lorebook

af582f43a65a021f53b908f0a507d55dce8c1e34

Cohee <18619528+Cohee1207@users.noreply.github.com>

6 files changed, +146 -31Ignore whitespace
public/index.html+2 -20
@@ -4813,6 +4813,8 @@
48134813 </div>
48144814 <div id="sync_name_button" class="menu_button fa-solid fa-sync" title="Click to set user name for all messages" data-i18n="[title]Click to set user name for all messages">
48154815 </div>
4816+ <div id="persona_lore_button" class="menu_button fa-solid fa-globe" title="Persona Lore" data-i18n="[title]Persona Lore">
4817+ </div>
48164818 </div>
48174819 <div>
48184820 <h4 data-i18n="Persona Description">Persona Description</h4>
@@ -5539,26 +5541,6 @@
55395541 </div>
55405542 </div>
55415543 <!-- templates for JS to reuse when needed -->
5542- <div id="chat_world_template" class="template_element">
5543- <div class="chat_world range-block flexFlowColumn flex-container">
5544- <div class="range-block-title">
5545- <h4 data-i18n="Chat Lorebook"><!-- This data-i18n attribute is kept for backward compatibility, use the ones below when translating -->
5546- <span data-i18n="Chat Lorebook for">Chat Lorebook for</span> <span class="chat_name"></span>
5547- </h4>
5548- </div>
5549- <div class="range-block-counter justifyLeft flex-container flexFlowColumn margin-bot-10px">
5550- <span data-i18n="chat_world_template_txt">
5551- A selected World Info will be bound to this chat. When generating an AI reply,
5552- it will be combined with the entries from global and character lorebooks.
5553- </span>
5554- </div>
5555- <div class="range-block-range wide100p">
5556- <select class="chat_world_info_selector wide100p">
5557- <option value="">--- None ---</option>
5558- </select>
5559- </div>
5560- </div>
5561- </div>
55625544 <div id="character_world_template" class="template_element">
55635545 <div class="character_world range-block flexFlowColumn flex-container">
55645546 <div class="range-block-title">
public/scripts/personas.js+59 -2
@@ -21,8 +21,10 @@ import { PAGINATION_TEMPLATE, debounce, delay, download, ensureImageFormatSuppor
2121import { debounce_timeout } from './constants.js';
2222import { FILTER_TYPES, FilterHelper } from './filters.js';
2323import { selected_group } from './group-chats.js';
2424import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
2525import { t } from './i18n.js';
26+import { world_names } from './world-info.js';
27+import { renderTemplateAsync } from './templates.js';
2628
2729let savePersonasPage = 0;
2830const GRID_STORAGE_KEY = 'Personas_GridView';
@@ -375,6 +377,7 @@ export function initPersona(avatarId, personaName, personaDescription) {
375377 position: persona_description_positions.IN_PROMPT,
376378 depth: DEFAULT_DEPTH,
377379 role: DEFAULT_ROLE,
380+ lorebook: '',
378381 };
379382
380383 saveSettingsDebounced();
@@ -418,6 +421,7 @@ export async function convertCharacterToPersona(characterId = null) {
418421 position: persona_description_positions.IN_PROMPT,
419422 depth: DEFAULT_DEPTH,
420423 role: DEFAULT_ROLE,
424+ lorebook: '',
421425 };
422426
423427 // If the user is currently using this persona, update the description
@@ -461,6 +465,7 @@ export function setPersonaDescription() {
461465 .val(power_user.persona_description_role)
462466 .find(`option[value="${power_user.persona_description_role}"]`)
463467 .prop('selected', String(true));
468+ $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook);
464469 countPersonaDescriptionTokens();
465470}
466471
@@ -490,6 +495,7 @@ async function updatePersonaNameIfExists(avatarId, newName) {
490495 position: persona_description_positions.IN_PROMPT,
491496 depth: DEFAULT_DEPTH,
492497 role: DEFAULT_ROLE,
498+ lorebook: '',
493499 };
494500 console.log(`Created persona name for ${avatarId} as ${newName}`);
495501 }
@@ -535,6 +541,7 @@ async function bindUserNameToPersona(e) {
535541 position: isCurrentPersona ? power_user.persona_description_position : persona_description_positions.IN_PROMPT,
536542 depth: isCurrentPersona ? power_user.persona_description_depth : DEFAULT_DEPTH,
537543 role: isCurrentPersona ? power_user.persona_description_role : DEFAULT_ROLE,
544+ lorebook: isCurrentPersona ? power_user.persona_description_lorebook : '',
538545 };
539546 }
540547
@@ -579,12 +586,20 @@ function selectCurrentPersona() {
579586 power_user.persona_description_position = descriptor.position ?? persona_description_positions.IN_PROMPT;
580587 power_user.persona_description_depth = descriptor.depth ?? DEFAULT_DEPTH;
581588 power_user.persona_description_role = descriptor.role ?? DEFAULT_ROLE;
589+ power_user.persona_description_lorebook = descriptor.lorebook ?? '';
582590 } else {
583591 power_user.persona_description = '';
584592 power_user.persona_description_position = persona_description_positions.IN_PROMPT;
585593 power_user.persona_description_depth = DEFAULT_DEPTH;
586594 power_user.persona_description_role = DEFAULT_ROLE;
587- power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.IN_PROMPT, depth: DEFAULT_DEPTH, role: DEFAULT_ROLE };
595+ power_user.persona_description_lorebook = '';
596+ power_user.persona_descriptions[user_avatar] = {
597+ description: '',
598+ position: persona_description_positions.IN_PROMPT,
599+ depth: DEFAULT_DEPTH,
600+ role: DEFAULT_ROLE,
601+ lorebook: '',
602+ };
588603 }
589604
590605 setPersonaDescription();
@@ -652,6 +667,7 @@ async function lockPersona() {
652667 position: persona_description_positions.IN_PROMPT,
653668 depth: DEFAULT_DEPTH,
654669 role: DEFAULT_ROLE,
670+ lorebook: '',
655671 };
656672 }
657673
@@ -731,6 +747,7 @@ function onPersonaDescriptionInput() {
731747 position: Number($('#persona_description_position').find(':selected').val()),
732748 depth: Number($('#persona_depth_value').val()),
733749 role: Number($('#persona_depth_role').find(':selected').val()),
750+ lorebook: '',
734751 };
735752 power_user.persona_descriptions[user_avatar] = object;
736753 }
@@ -766,6 +783,43 @@ function onPersonaDescriptionDepthRoleInput() {
766783 saveSettingsDebounced();
767784}
768785
786+async function onPersonaLoreButtonClick() {
787+ const personaName = power_user.personas[user_avatar];
788+ const selectedLorebook = power_user.persona_description_lorebook;
789+
790+ if (!personaName) {
791+ toastr.warning(t`You must bind a name to this persona before you can set a lorebook.`, t`Persona name not set`);
792+ return;
793+ }
794+
795+ const template = $(await renderTemplateAsync('personaLorebook'));
796+
797+ const worldSelect = template.find('select');
798+ template.find('.persona_name').text(personaName);
799+
800+ for (const worldName of world_names) {
801+ const option = document.createElement('option');
802+ option.value = worldName;
803+ option.innerText = worldName;
804+ option.selected = selectedLorebook === worldName;
805+ worldSelect.append(option);
806+ }
807+
808+ worldSelect.on('change', function () {
809+ power_user.persona_description_lorebook = String($(this).val());
810+
811+ if (power_user.personas[user_avatar]) {
812+ const object = getOrCreatePersonaDescriptor();
813+ object.lorebook = power_user.persona_description_lorebook;
814+ }
815+
816+ $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook);
817+ saveSettingsDebounced();
818+ });
819+
820+ await callGenericPopup(template, POPUP_TYPE.TEXT);
821+}
822+
769823function onPersonaDescriptionPositionInput() {
770824 power_user.persona_description_position = Number(
771825 $('#persona_description_position').find(':selected').val(),
@@ -789,6 +843,7 @@ function getOrCreatePersonaDescriptor() {
789843 position: power_user.persona_description_position,
790844 depth: power_user.persona_description_depth,
791845 role: power_user.persona_description_role,
846+ lorebook: power_user.persona_description_lorebook,
792847 };
793848 power_user.persona_descriptions[user_avatar] = object;
794849 }
@@ -1038,6 +1093,7 @@ async function duplicatePersona(avatarId) {
10381093 position: descriptor?.position ?? persona_description_positions.IN_PROMPT,
10391094 depth: descriptor?.depth ?? DEFAULT_DEPTH,
10401095 role: descriptor?.role ?? DEFAULT_ROLE,
1096+ lorebook: descriptor?.lorebook ?? '',
10411097 };
10421098
10431099 await uploadUserAvatar(getUserAvatar(avatarId), newAvatarId);
@@ -1055,6 +1111,7 @@ export function initPersonas() {
10551111 $('#persona_description_position').on('input', onPersonaDescriptionPositionInput);
10561112 $('#persona_depth_value').on('input', onPersonaDescriptionDepthValueInput);
10571113 $('#persona_depth_role').on('input', onPersonaDescriptionDepthRoleInput);
1114+ $('#persona_lore_button').on('click', onPersonaLoreButtonClick);
10581115 $('#personas_backup').on('click', onBackupPersonas);
10591116 $('#personas_restore').on('click', () => $('#personas_restore_input').trigger('click'));
10601117 $('#personas_restore_input').on('change', onPersonasRestoreInput);
public/scripts/power-user.js+2 -1
@@ -262,6 +262,7 @@ let power_user = {
262262 persona_description_position: persona_description_positions.IN_PROMPT,
263263 persona_description_role: 0,
264264 persona_description_depth: 2,
265+ persona_description_lorebook: '',
265266 persona_show_notifications: true,
266267 persona_sort_order: 'asc',
267268
@@ -1925,7 +1926,7 @@ export function fuzzySearchPersonas(data, searchValue, fuzzySearchCaches = null)
19251926 const mappedData = data.map(x => ({
19261927 key: x,
19271928 name: power_user.personas[x] ?? '',
19281929 description: power_user.persona_descriptions[x]?.description ?? '',
19291930 }));
19301931
19311932 const keys = [
public/scripts/templates/chatLorebook.html+18 -0
@@ -0,0 +1,18 @@
1+<div class="chat_world range-block flexFlowColumn flex-container">
2+ <div class="range-block-title">
3+ <h4 data-i18n="Chat Lorebook"><!-- This data-i18n attribute is kept for backward compatibility, use the ones below when translating -->
4+ <span data-i18n="Chat Lorebook for">Chat Lorebook for</span> <span class="chat_name"></span>
5+ </h4>
6+ </div>
7+ <div class="range-block-counter justifyLeft flex-container flexFlowColumn margin-bot-10px">
8+ <span data-i18n="chat_world_template_txt">
9+ A selected World Info will be bound to this chat. When generating an AI reply,
10+ it will be combined with the entries from global and character lorebooks.
11+ </span>
12+ </div>
13+ <div class="range-block-range wide100p">
14+ <select class="chat_world_info_selector wide100p">
15+ <option value="">--- None ---</option>
16+ </select>
17+ </div>
18+</div>
public/scripts/templates/personaLorebook.html+18 -0
@@ -0,0 +1,18 @@
1+<div class="persona_world range-block flexFlowColumn flex-container">
2+ <div class="range-block-title">
3+ <h4>
4+ <span data-i18n="PErsona Lorebook for">Persona Lorebook for</span> <span class="persona_name"></span>
5+ </h4>
6+ </div>
7+ <div class="range-block-counter justifyLeft flex-container flexFlowColumn margin-bot-10px">
8+ <span data-i18n="persona_world_template_txt">
9+ A selected World Info will be bound to this persona. When generating an AI reply,
10+ it will be combined with the entries from global, character and chat lorebooks.
11+ </span>
12+ </div>
13+ <div class="range-block-range wide100p">
14+ <select class="persona_world_info_selector wide100p">
15+ <option value="">--- None ---</option>
16+ </select>
17+ </div>
18+</div>
public/scripts/world-info.js+47 -8
@@ -3548,6 +3548,11 @@ async function getCharacterLore() {
35483548 continue;
35493549 }
35503550
3551+ if (power_user.persona_description_lorebook === worldName) {
3552+ console.debug(`[WI] Character ${name}'s world ${worldName} is already activated in persona lore! Skipping...`);
3553+ continue;
3554+ }
3555+
35513556 const data = await loadWorldInfo(worldName);
35523557 const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : [];
35533558 entries = entries.concat(newEntries);
@@ -3598,11 +3603,45 @@ async function getChatLore() {
35983603 return entries;
35993604}
36003605
3606+async function getPersonaLore() {
3607+ const chatWorld = chat_metadata[METADATA_KEY];
3608+ const personaWorld = power_user.persona_description_lorebook;
3609+
3610+ if (!personaWorld) {
3611+ return [];
3612+ }
3613+
3614+ if (chatWorld === personaWorld) {
3615+ console.debug(`[WI] Persona world ${personaWorld} is already activated in chat world! Skipping...`);
3616+ return [];
3617+ }
3618+
3619+ if (selected_world_info.includes(personaWorld)) {
3620+ console.debug(`[WI] Persona world ${personaWorld} is already activated in global world info! Skipping...`);
3621+ return [];
3622+ }
3623+
3624+ const data = await loadWorldInfo(personaWorld);
3625+ const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: personaWorld, ...rest })) : [];
3626+
3627+ console.debug(`[WI] Persona lore has ${entries.length} entries`, [personaWorld]);
3628+
3629+ return entries;
3630+}
3631+
36013632export async function getSortedEntries() {
36023633 try {
3603- const globalLore = await getGlobalLore();
3634+ const [
3604- const characterLore = await getCharacterLore();
3635+ globalLore,
3605- const chatLore = await getChatLore();
3636+ characterLore,
3637+ chatLore,
3638+ personaLore,
3639+ ] = await Promise.all([
3640+ getGlobalLore(),
3641+ getCharacterLore(),
3642+ getChatLore(),
3643+ getPersonaLore(),
3644+ ]);
36063645
36073646 let entries;
36083647
@@ -3622,8 +3661,8 @@ export async function getSortedEntries() {
36223661 break;
36233662 }
36243663
36253664 // Chat lore always goes first, then persona lore, then the rest
36263665 entries = [...chatLore.sort(sortFn), ...personaLore.sort(sortFn), ...entries];
36273666
36283667 // Calculate hash and parse decorators. Split maps to preserve old hashes.
36293668 entries = entries.map((entry) => {
@@ -4816,9 +4855,9 @@ export async function importWorldInfo(file) {
48164855 });
48174856}
48184857
48194858export async function assignLorebookToChat() {
48204859 const selectedName = chat_metadata[METADATA_KEY];
48214860 const template = $('#chat_world_templateawait .chat_worldrenderTemplateAsync('chatLorebook').clone();
48224861
48234862 const worldSelect = template.find('select');
48244863 const chatName = template.find('.chat_name');
@@ -4846,7 +4885,7 @@ export function assignLorebookToChat() {
48464885 saveMetadata();
48474886 });
48484887
48494888 callPopupcallGenericPopup(template, 'text'POPUP_TYPE.TEXT);
48504889}
48514890
48524891jQuery(() => {