[wip] Persona lorebook
| @@ -4813,6 +4813,8 @@ | ||
| 4813 | 4813 | </div> |
| 4814 | 4814 | <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"> |
| 4815 | 4815 | </div> |
| 4816 | + <div id="persona_lore_button" class="menu_button fa-solid fa-globe" title="Persona Lore" data-i18n="[title]Persona Lore"> | |
| 4817 | + </div> | |
| 4816 | 4818 | </div> |
| 4817 | 4819 | <div> |
| 4818 | 4820 | <h4 data-i18n="Persona Description">Persona Description</h4> |
| @@ -5539,26 +5541,6 @@ | ||
| 5539 | 5541 | </div> |
| 5540 | 5542 | </div> |
| 5541 | 5543 | <!-- 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> | |
| 5562 | 5544 | <div id="character_world_template" class="template_element"> |
| 5563 | 5545 | <div class="character_world range-block flexFlowColumn flex-container"> |
| 5564 | 5546 | <div class="range-block-title"> |
| @@ -21,8 +21,10 @@ import { PAGINATION_TEMPLATE, debounce, delay, download, ensureImageFormatSuppor | ||
| 21 | 21 | import { debounce_timeout } from './constants.js'; |
| 22 | 22 | import { FILTER_TYPES, FilterHelper } from './filters.js'; |
| 23 | 23 | import { selected_group } from './group-chats.js'; |
| 24 | 24 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| 25 | 25 | import { t } from './i18n.js'; |
| 26 | +import { world_names } from './world-info.js'; | |
| 27 | +import { renderTemplateAsync } from './templates.js'; | |
| 26 | 28 | |
| 27 | 29 | let savePersonasPage = 0; |
| 28 | 30 | const GRID_STORAGE_KEY = 'Personas_GridView'; |
| @@ -375,6 +377,7 @@ export function initPersona(avatarId, personaName, personaDescription) { | ||
| 375 | 377 | position: persona_description_positions.IN_PROMPT, |
| 376 | 378 | depth: DEFAULT_DEPTH, |
| 377 | 379 | role: DEFAULT_ROLE, |
| 380 | + lorebook: '', | |
| 378 | 381 | }; |
| 379 | 382 | |
| 380 | 383 | saveSettingsDebounced(); |
| @@ -418,6 +421,7 @@ export async function convertCharacterToPersona(characterId = null) { | ||
| 418 | 421 | position: persona_description_positions.IN_PROMPT, |
| 419 | 422 | depth: DEFAULT_DEPTH, |
| 420 | 423 | role: DEFAULT_ROLE, |
| 424 | + lorebook: '', | |
| 421 | 425 | }; |
| 422 | 426 | |
| 423 | 427 | // If the user is currently using this persona, update the description |
| @@ -461,6 +465,7 @@ export function setPersonaDescription() { | ||
| 461 | 465 | .val(power_user.persona_description_role) |
| 462 | 466 | .find(`option[value="${power_user.persona_description_role}"]`) |
| 463 | 467 | .prop('selected', String(true)); |
| 468 | + $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook); | |
| 464 | 469 | countPersonaDescriptionTokens(); |
| 465 | 470 | } |
| 466 | 471 | |
| @@ -490,6 +495,7 @@ async function updatePersonaNameIfExists(avatarId, newName) { | ||
| 490 | 495 | position: persona_description_positions.IN_PROMPT, |
| 491 | 496 | depth: DEFAULT_DEPTH, |
| 492 | 497 | role: DEFAULT_ROLE, |
| 498 | + lorebook: '', | |
| 493 | 499 | }; |
| 494 | 500 | console.log(`Created persona name for ${avatarId} as ${newName}`); |
| 495 | 501 | } |
| @@ -535,6 +541,7 @@ async function bindUserNameToPersona(e) { | ||
| 535 | 541 | position: isCurrentPersona ? power_user.persona_description_position : persona_description_positions.IN_PROMPT, |
| 536 | 542 | depth: isCurrentPersona ? power_user.persona_description_depth : DEFAULT_DEPTH, |
| 537 | 543 | role: isCurrentPersona ? power_user.persona_description_role : DEFAULT_ROLE, |
| 544 | + lorebook: isCurrentPersona ? power_user.persona_description_lorebook : '', | |
| 538 | 545 | }; |
| 539 | 546 | } |
| 540 | 547 | |
| @@ -579,12 +586,20 @@ function selectCurrentPersona() { | ||
| 579 | 586 | power_user.persona_description_position = descriptor.position ?? persona_description_positions.IN_PROMPT; |
| 580 | 587 | power_user.persona_description_depth = descriptor.depth ?? DEFAULT_DEPTH; |
| 581 | 588 | power_user.persona_description_role = descriptor.role ?? DEFAULT_ROLE; |
| 589 | + power_user.persona_description_lorebook = descriptor.lorebook ?? ''; | |
| 582 | 590 | } else { |
| 583 | 591 | power_user.persona_description = ''; |
| 584 | 592 | power_user.persona_description_position = persona_description_positions.IN_PROMPT; |
| 585 | 593 | power_user.persona_description_depth = DEFAULT_DEPTH; |
| 586 | 594 | 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 | + }; | |
| 588 | 603 | } |
| 589 | 604 | |
| 590 | 605 | setPersonaDescription(); |
| @@ -652,6 +667,7 @@ async function lockPersona() { | ||
| 652 | 667 | position: persona_description_positions.IN_PROMPT, |
| 653 | 668 | depth: DEFAULT_DEPTH, |
| 654 | 669 | role: DEFAULT_ROLE, |
| 670 | + lorebook: '', | |
| 655 | 671 | }; |
| 656 | 672 | } |
| 657 | 673 | |
| @@ -731,6 +747,7 @@ function onPersonaDescriptionInput() { | ||
| 731 | 747 | position: Number($('#persona_description_position').find(':selected').val()), |
| 732 | 748 | depth: Number($('#persona_depth_value').val()), |
| 733 | 749 | role: Number($('#persona_depth_role').find(':selected').val()), |
| 750 | + lorebook: '', | |
| 734 | 751 | }; |
| 735 | 752 | power_user.persona_descriptions[user_avatar] = object; |
| 736 | 753 | } |
| @@ -766,6 +783,43 @@ function onPersonaDescriptionDepthRoleInput() { | ||
| 766 | 783 | saveSettingsDebounced(); |
| 767 | 784 | } |
| 768 | 785 | |
| 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 | + | |
| 769 | 823 | function onPersonaDescriptionPositionInput() { |
| 770 | 824 | power_user.persona_description_position = Number( |
| 771 | 825 | $('#persona_description_position').find(':selected').val(), |
| @@ -789,6 +843,7 @@ function getOrCreatePersonaDescriptor() { | ||
| 789 | 843 | position: power_user.persona_description_position, |
| 790 | 844 | depth: power_user.persona_description_depth, |
| 791 | 845 | role: power_user.persona_description_role, |
| 846 | + lorebook: power_user.persona_description_lorebook, | |
| 792 | 847 | }; |
| 793 | 848 | power_user.persona_descriptions[user_avatar] = object; |
| 794 | 849 | } |
| @@ -1038,6 +1093,7 @@ async function duplicatePersona(avatarId) { | ||
| 1038 | 1093 | position: descriptor?.position ?? persona_description_positions.IN_PROMPT, |
| 1039 | 1094 | depth: descriptor?.depth ?? DEFAULT_DEPTH, |
| 1040 | 1095 | role: descriptor?.role ?? DEFAULT_ROLE, |
| 1096 | + lorebook: descriptor?.lorebook ?? '', | |
| 1041 | 1097 | }; |
| 1042 | 1098 | |
| 1043 | 1099 | await uploadUserAvatar(getUserAvatar(avatarId), newAvatarId); |
| @@ -1055,6 +1111,7 @@ export function initPersonas() { | ||
| 1055 | 1111 | $('#persona_description_position').on('input', onPersonaDescriptionPositionInput); |
| 1056 | 1112 | $('#persona_depth_value').on('input', onPersonaDescriptionDepthValueInput); |
| 1057 | 1113 | $('#persona_depth_role').on('input', onPersonaDescriptionDepthRoleInput); |
| 1114 | + $('#persona_lore_button').on('click', onPersonaLoreButtonClick); | |
| 1058 | 1115 | $('#personas_backup').on('click', onBackupPersonas); |
| 1059 | 1116 | $('#personas_restore').on('click', () => $('#personas_restore_input').trigger('click')); |
| 1060 | 1117 | $('#personas_restore_input').on('change', onPersonasRestoreInput); |
| @@ -262,6 +262,7 @@ let power_user = { | ||
| 262 | 262 | persona_description_position: persona_description_positions.IN_PROMPT, |
| 263 | 263 | persona_description_role: 0, |
| 264 | 264 | persona_description_depth: 2, |
| 265 | + persona_description_lorebook: '', | |
| 265 | 266 | persona_show_notifications: true, |
| 266 | 267 | persona_sort_order: 'asc', |
| 267 | 268 | |
| @@ -1925,7 +1926,7 @@ export function fuzzySearchPersonas(data, searchValue, fuzzySearchCaches = null) | ||
| 1925 | 1926 | const mappedData = data.map(x => ({ |
| 1926 | 1927 | key: x, |
| 1927 | 1928 | name: power_user.personas[x] ?? '', |
| 1928 | 1929 | description: power_user.persona_descriptions[x]?.description ?? '', |
| 1929 | 1930 | })); |
| 1930 | 1931 | |
| 1931 | 1932 | const keys = [ |
| @@ -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> | |
| @@ -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> | |
| @@ -3548,6 +3548,11 @@ async function getCharacterLore() { | ||
| 3548 | 3548 | continue; |
| 3549 | 3549 | } |
| 3550 | 3550 | |
| 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 | + | |
| 3551 | 3556 | const data = await loadWorldInfo(worldName); |
| 3552 | 3557 | const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : []; |
| 3553 | 3558 | entries = entries.concat(newEntries); |
| @@ -3598,11 +3603,45 @@ async function getChatLore() { | ||
| 3598 | 3603 | return entries; |
| 3599 | 3604 | } |
| 3600 | 3605 | |
| 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 | + | |
| 3601 | 3632 | export async function getSortedEntries() { |
| 3602 | 3633 | 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 | + ]); | |
| 3606 | 3645 | |
| 3607 | 3646 | let entries; |
| 3608 | 3647 | |
| @@ -3622,8 +3661,8 @@ export async function getSortedEntries() { | ||
| 3622 | 3661 | break; |
| 3623 | 3662 | } |
| 3624 | 3663 | |
| 3625 | 3664 | // Chat lore always goes first, then persona lore, then the rest |
| 3626 | 3665 | entries = [...chatLore.sort(sortFn), ...personaLore.sort(sortFn), ...entries]; |
| 3627 | 3666 | |
| 3628 | 3667 | // Calculate hash and parse decorators. Split maps to preserve old hashes. |
| 3629 | 3668 | entries = entries.map((entry) => { |
| @@ -4816,9 +4855,9 @@ export async function importWorldInfo(file) { | ||
| 4816 | 4855 | }); |
| 4817 | 4856 | } |
| 4818 | 4857 | |
| 4819 | 4858 | export async function assignLorebookToChat() { |
| 4820 | 4859 | const selectedName = chat_metadata[METADATA_KEY]; |
| 4821 | 4860 | const template = $('#chat_world_templateawait .chat_worldrenderTemplateAsync('chatLorebook').clone(); |
| 4822 | 4861 | |
| 4823 | 4862 | const worldSelect = template.find('select'); |
| 4824 | 4863 | const chatName = template.find('.chat_name'); |
| @@ -4846,7 +4885,7 @@ export function assignLorebookToChat() { | ||
| 4846 | 4885 | saveMetadata(); |
| 4847 | 4886 | }); |
| 4848 | 4887 | |
| 4849 | 4888 | callPopupcallGenericPopup(template, 'text'POPUP_TYPE.TEXT); |
| 4850 | 4889 | } |
| 4851 | 4890 | |
| 4852 | 4891 | jQuery(() => { |