Reworked persona lock buttons + lock to character - Slight redesign of the persona panel right side, add headers - Add buttons for all three different lock states (chat, character, default) - Update lock and toggle lock functions to accept toggle type - Write persona locking to chat functionality (saves to persona description object)
| @@ -4924,8 +4924,6 @@ | ||
| 4924 | 4924 | <input id="your_name" name="your_name" data-i18n="[placeholder]Enter your name" placeholder="Enter your name" class="text_pole wide100p" value="" autocomplete="off"> |
| 4925 | 4925 | <div id="your_name_button" class="menu_button fa-solid fa-check" title="Click to set a new User Name" data-i18n="[title]Click to set a new User Name"> |
| 4926 | 4926 | </div> |
| 4927 | - <div id="lock_user_name" class="menu_button fa-solid fa-unlock" title="Click to lock your selected persona to the current chat. Click again to remove the lock." data-i18n="[title]Click to lock your selected persona to the current chat. Click again to remove the lock."> | |
| 4928 | - </div> | |
| 4929 | 4927 | <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"> |
| 4930 | 4928 | </div> |
| 4931 | 4929 | <div id="persona_lore_button" class="menu_button fa-solid fa-globe" title="Persona Lore Alt+Click to open the lorebook" data-i18n="[title]Persona Lore Alt+Click to open the lorebook"> |
| @@ -4937,10 +4935,6 @@ | ||
| 4937 | 4935 | <div class="extension_token_counter"> |
| 4938 | 4936 | <span data-i18n="Tokens persona description">Tokens</span>: <span id="persona_description_token_count">0</span> |
| 4939 | 4937 | </div> |
| 4940 | - </div> | |
| 4941 | - | |
| 4942 | - <div class="persona_management_global_settings"> | |
| 4943 | - <h4 class="standoutHeader" data-i18n="Global Settings">Global Settings</h4> | |
| 4944 | 4938 | |
| 4945 | 4939 | <div class="persona_management_description_position_container"> |
| 4946 | 4940 | <label for="persona_description_position" data-i18n="Position:">Position:</label> |
| @@ -4967,6 +4961,27 @@ | ||
| 4967 | 4961 | </div> |
| 4968 | 4962 | </div> |
| 4969 | 4963 | |
| 4964 | + <h4 data-i18n="Connections">Connections</h4> | |
| 4965 | + <div id="persona_connections_buttons" class="flex-container marginBot10"> | |
| 4966 | + <div id="lock_persona_default" class="menu_button menu_button_icon" title="Click to select this as default persona for the new chats. Click again to remove the lock." data-i18n="[title]Click to select this as default persona for the new chats. Click again to remove the lock."> | |
| 4967 | + <i class="icon fa-solid fa-crown fa-fw"></i> | |
| 4968 | + <div data-i18n="Default">Default</div> | |
| 4969 | + </div> | |
| 4970 | + <div id="lock_user_name" class="menu_button menu_button_icon" title="Click to lock your selected persona to the current chat. Click again to remove the lock." data-i18n="[title]Click to lock your selected persona to the current chat. Click again to remove the lock."> | |
| 4971 | + <i class="icon fa-solid fa-unlock fa-fw"></i> | |
| 4972 | + <div data-i18n="Chat">Chat</div> | |
| 4973 | + </div> | |
| 4974 | + <div id="lock_persona_to_char" class="menu_button menu_button_icon" title="Click to lock your selected persona to the current character. Click again to remove the lock." data-i18n="[title]Click to lock your selected persona to the current character. Click again to remove the lock."> | |
| 4975 | + <i class="icon fa-solid fa-unlock fa-fw"></i> | |
| 4976 | + <div data-i18n="Character">Character</div> | |
| 4977 | + </div> | |
| 4978 | + </div> | |
| 4979 | + <div id="persona_connections_list" class="text_muted m-b-1" data-i18n="persona_no_connections">[No connections]</div> | |
| 4980 | + </div> | |
| 4981 | + | |
| 4982 | + <div class="persona_management_global_settings"> | |
| 4983 | + <h4 class="standoutHeader" data-i18n="Global Settings">Global Settings</h4> | |
| 4984 | + | |
| 4970 | 4985 | <div class="persona_management_show_notifications range-block"> |
| 4971 | 4986 | <label for="persona_show_notifications" class="checkbox_label"> |
| 4972 | 4987 | <input id="persona_show_notifications" type="checkbox" /> |
| @@ -7,7 +7,9 @@ import { | ||
| 7 | 7 | event_types, |
| 8 | 8 | getRequestHeaders, |
| 9 | 9 | getThumbnailUrl, |
| 10 | + menu_type, | |
| 10 | 11 | name1, |
| 12 | + name2, | |
| 11 | 13 | reloadCurrentChat, |
| 12 | 14 | saveChatConditional, |
| 13 | 15 | saveMetadata, |
| @@ -26,6 +28,14 @@ import { t } from './i18n.js'; | ||
| 26 | 28 | import { openWorldInfoEditor, world_names } from './world-info.js'; |
| 27 | 29 | import { renderTemplateAsync } from './templates.js'; |
| 28 | 30 | |
| 31 | +/** | |
| 32 | + * @typedef {object} PersonaConnection A connection between a character and a character or group entity | |
| 33 | + * @property {'character' | 'group'} type - Type of connection | |
| 34 | + * @property {string} id - ID of the connection (character key (avatar url), group id) | |
| 35 | + */ | |
| 36 | + | |
| 37 | +/** @typedef {'chat' | 'character' | 'default'} PersonaLockType Type of the persona lock */ | |
| 38 | + | |
| 29 | 39 | let savePersonasPage = 0; |
| 30 | 40 | const GRID_STORAGE_KEY = 'Personas_GridView'; |
| 31 | 41 | const DEFAULT_DEPTH = 2; |
| @@ -607,15 +617,35 @@ function selectCurrentPersona() { | ||
| 607 | 617 | } |
| 608 | 618 | |
| 609 | 619 | /** |
| 610 | 620 | * Checks if thea personaconnection is locked for the current chat.character or group edit menu |
| 621 | + * @param {PersonaConnection} connection - Connection to check | |
| 622 | + * @returns {boolean} Whether the connection is locked | |
| 623 | + */ | |
| 624 | +function isPersonaConnectionLocked(connection) { | |
| 625 | + return (menu_type === 'character_edit' && connection.type === 'character' && connection.id === characters[this_chid]?.avatar) | |
| 626 | + || (menu_type === 'group_edit' && connection.type === 'group' && connection.id === selected_group); | |
| 627 | +} | |
| 628 | + | |
| 629 | +/** | |
| 630 | + * Checks if the persona is locked | |
| 631 | + * @param {PersonaLockType} type - Lock type | |
| 611 | 632 | * @returns {boolean} Whether the persona is locked |
| 612 | 633 | */ |
| 613 | 634 | function isPersonaLocked(type = 'chat') { |
| 614 | - return !!chat_metadata['persona']; | |
| 635 | + switch (type) { | |
| 636 | + case 'default': | |
| 637 | + return power_user.default_persona === user_avatar; | |
| 638 | + case 'chat': | |
| 639 | + return chat_metadata['persona'] == user_avatar; | |
| 640 | + case 'character': { | |
| 641 | + return !!power_user.persona_descriptions[user_avatar]?.connections?.some(isPersonaConnectionLocked); | |
| 642 | + } | |
| 643 | + default: throw new Error(`Unknown persona lock type: ${type}`); | |
| 644 | + } | |
| 615 | 645 | } |
| 616 | 646 | |
| 617 | 647 | /** |
| 618 | 648 | * Locks or unlocks the persona for the current chat. |
| 619 | 649 | * @param {boolean} state Desired lock state |
| 620 | 650 | * @returns {Promise<void>} |
| 621 | 651 | */ |
| @@ -624,34 +654,63 @@ export async function setPersonaLockState(state) { | ||
| 624 | 654 | } |
| 625 | 655 | |
| 626 | 656 | /** |
| 627 | 657 | * Toggle the persona lock state for the current chat. |
| 658 | + * @param {PersonaLockType} type - Lock type | |
| 628 | 659 | * @returns {Promise<void>} |
| 629 | 660 | */ |
| 630 | 661 | export async function togglePersonaLock(type = 'chat') { |
| 631 | 662 | return isPersonaLocked(type) |
| 632 | 663 | ? await unlockPersona(type) |
| 633 | 664 | : await lockPersona(type); |
| 634 | 665 | } |
| 635 | 666 | |
| 636 | 667 | /** |
| 637 | 668 | * Unlock the persona for the current chat. |
| 669 | + * @param {PersonaLockType} type - Lock type | |
| 670 | + * @returns {Promise<void>} | |
| 638 | 671 | */ |
| 639 | 672 | async function unlockPersona(type = 'chat') { |
| 640 | 673 | ifswitch (chat_metadata['persona']type) { |
| 641 | - console.log(`Unlocking persona for this chat ${chat_metadata['persona']}`); | |
| 674 | + case 'default': { | |
| 642 | - delete chat_metadata['persona']; | |
| 675 | + // TODO: Make this toggle-able | |
| 643 | - await saveMetadata(); | |
| 676 | + await toggleDefaultPersona(user_avatar, { quiet: true }); | |
| 644 | - if (power_user.persona_show_notifications) { | |
| 677 | + break; | |
| 645 | - toastr.info(t`User persona is now unlocked for this chat. Click the "Lock" again to revert.`, t`Persona unlocked`); | |
| 678 | + } | |
| 679 | + case 'chat': { | |
| 680 | + if (chat_metadata['persona']) { | |
| 681 | + console.log(`Unlocking persona ${user_avatar} from this chat`); | |
| 682 | + delete chat_metadata['persona']; | |
| 683 | + await saveMetadata(); | |
| 684 | + if (power_user.persona_show_notifications) { | |
| 685 | + toastr.info(t`User persona ${name1} is now unlocked from this chat. Click the "Lock" again to revert.`, t`Persona unlocked`); | |
| 686 | + } | |
| 687 | + } | |
| 688 | + break; | |
| 689 | + } | |
| 690 | + case 'character': { | |
| 691 | + /** @type {PersonaConnection[]} */ | |
| 692 | + const connections = power_user.persona_descriptions[user_avatar]?.connections; | |
| 693 | + if (connections) { | |
| 694 | + console.log(`Unlocking persona ${user_avatar} from this character ${name2}`); | |
| 695 | + power_user.persona_descriptions[user_avatar].connections = connections.filter(c => !isPersonaConnectionLocked(c)); | |
| 696 | + saveSettingsDebounced(); | |
| 697 | + if (power_user.persona_show_notifications) { | |
| 698 | + toastr.info(t`User persona ${name1} is now unlocked from character ${name2}. Click the "Lock" again to revert.`, t`Persona unlocked`); | |
| 699 | + } | |
| 700 | + } | |
| 701 | + break; | |
| 646 | 702 | } |
| 647 | - updateUserLockIcon(); | |
| 648 | 703 | } |
| 704 | + | |
| 705 | + updatePersonaLockIcons(); | |
| 649 | 706 | } |
| 650 | 707 | |
| 651 | 708 | /** |
| 652 | 709 | * Lock the persona for the current chat. |
| 710 | + * @param {PersonaLockType} type - Lock type | |
| 653 | 711 | */ |
| 654 | 712 | async function lockPersona(type = 'chat') { |
| 713 | + // First make sure that user_avatar is actually a persona | |
| 655 | 714 | if (!(user_avatar in power_user.personas)) { |
| 656 | 715 | console.log(`Creating a new persona ${user_avatar}`); |
| 657 | 716 | if (power_user.persona_show_notifications) { |
| @@ -668,17 +727,44 @@ async function lockPersona() { | ||
| 668 | 727 | depth: DEFAULT_DEPTH, |
| 669 | 728 | role: DEFAULT_ROLE, |
| 670 | 729 | lorebook: '', |
| 730 | + connections: [], | |
| 671 | 731 | }; |
| 672 | 732 | } |
| 673 | 733 | |
| 674 | - chat_metadata['persona'] = user_avatar; | |
| 734 | + switch (type) { | |
| 675 | - await saveMetadata(); | |
| 735 | + case 'default': { | |
| 676 | - saveSettingsDebounced(); | |
| 736 | + // TODO: Make this toggle-able | |
| 677 | - console.log(`Locking persona for this chat ${user_avatar}`); | |
| 737 | + await toggleDefaultPersona(user_avatar, { quiet: true }); | |
| 678 | - if (power_user.persona_show_notifications) { | |
| 738 | + break; | |
| 679 | - toastr.success(t`User persona is locked to ${name1} in this chat`); | |
| 739 | + } | |
| 740 | + case 'chat': { | |
| 741 | + console.log(`Locking persona ${user_avatar} to this chat`); | |
| 742 | + chat_metadata['persona'] = user_avatar; | |
| 743 | + await saveMetadata(); | |
| 744 | + saveSettingsDebounced(); | |
| 745 | + if (power_user.persona_show_notifications) { | |
| 746 | + toastr.success(t`User persona ${name1} is locked to ${name2} in this chat`); | |
| 747 | + } | |
| 748 | + break; | |
| 749 | + } | |
| 750 | + case 'character': { | |
| 751 | + const newConnection = menu_type === 'character_edit' ? { type: 'character', id: characters[this_chid]?.avatar } : | |
| 752 | + menu_type === 'group_edit' ? { type: 'group', id: selected_group } : null; | |
| 753 | + /** @type {PersonaConnection[]} */ | |
| 754 | + const connections = power_user.persona_descriptions[user_avatar].connections?.filter(c => !isPersonaConnectionLocked(c)) ?? []; | |
| 755 | + if (newConnection && newConnection.id) { | |
| 756 | + console.log(`Locking persona ${user_avatar} to this character ${name2}`); | |
| 757 | + power_user.persona_descriptions[user_avatar].connections = [...connections, newConnection]; | |
| 758 | + saveSettingsDebounced(); | |
| 759 | + if (power_user.persona_show_notifications) { | |
| 760 | + toastr.success(t`User persona ${name1} is locked to character ${name2}`); | |
| 761 | + } | |
| 762 | + } | |
| 763 | + break; | |
| 764 | + } | |
| 680 | 765 | } |
| 681 | - updateUserLockIcon(); | |
| 766 | + | |
| 767 | + updatePersonaLockIcons(); | |
| 682 | 768 | } |
| 683 | 769 | |
| 684 | 770 | |
| @@ -730,7 +816,7 @@ async function deleteUserAvatar(e) { | ||
| 730 | 816 | |
| 731 | 817 | saveSettingsDebounced(); |
| 732 | 818 | await getUserAvatars(); |
| 733 | 819 | updateUserLockIconupdatePersonaLockIcons(); |
| 734 | 820 | } |
| 735 | 821 | } |
| 736 | 822 | |
| @@ -853,16 +939,33 @@ function getOrCreatePersonaDescriptor() { | ||
| 853 | 939 | depth: power_user.persona_description_depth, |
| 854 | 940 | role: power_user.persona_description_role, |
| 855 | 941 | lorebook: power_user.persona_description_lorebook, |
| 942 | + connections: [], | |
| 856 | 943 | }; |
| 857 | 944 | power_user.persona_descriptions[user_avatar] = object; |
| 858 | 945 | } |
| 859 | 946 | return object; |
| 860 | 947 | } |
| 861 | 948 | |
| 862 | -async function setDefaultPersona(e) { | |
| 949 | + | |
| 950 | + | |
| 951 | +async function toggleDefaultPersonaClicked(e) { | |
| 863 | 952 | e?.stopPropagation(); |
| 864 | 953 | const avatarId = $(thise.currentTarget).closest('.avatar-container').find('.avatar').attr('imgfile'); |
| 954 | + if (avatarId) { | |
| 955 | + await toggleDefaultPersona(avatarId); | |
| 956 | + } else { | |
| 957 | + console.warn('No avatar id found'); | |
| 958 | + } | |
| 959 | +} | |
| 865 | 960 | |
| 961 | +/** | |
| 962 | + * Sets a persona as the default one to be used for all new chats and unlocked existing chats | |
| 963 | + * @param {string} avatarId The avatar id of the persona to set as the default | |
| 964 | + * @param {object} [options] Optional arguments | |
| 965 | + * @param {boolean} [options.quiet=false] If true, no confirmation popups will be shown | |
| 966 | + * @returns {Promise<void>} | |
| 967 | + */ | |
| 968 | +async function toggleDefaultPersona(avatarId, { quiet: quiet = false } = {}) { | |
| 866 | 969 | if (!avatarId) { |
| 867 | 970 | console.warn('No avatar id found'); |
| 868 | 971 | return; |
| @@ -879,11 +982,12 @@ async function setDefaultPersona(e) { | ||
| 879 | 982 | const personaName = power_user.personas[avatarId]; |
| 880 | 983 | |
| 881 | 984 | if (avatarId === currentDefault) { |
| 882 | - const confirm = await Popup.show.confirm(t`Are you sure you want to remove the default persona?`, personaName); | |
| 985 | + if (!quiet) { | |
| 883 | - | |
| 986 | + const confirm = await Popup.show.confirm(t`Are you sure you want to remove the default persona?`, personaName); | |
| 884 | 987 | if (!confirm) { |
| 885 | 988 | console.debug('User cancelled removing default persona'); |
| 886 | 989 | return; |
| 990 | + } | |
| 887 | 991 | } |
| 888 | 992 | |
| 889 | 993 | console.log(`Removing default persona ${avatarId}`); |
| @@ -892,11 +996,12 @@ async function setDefaultPersona(e) { | ||
| 892 | 996 | } |
| 893 | 997 | delete power_user.default_persona; |
| 894 | 998 | } else { |
| 895 | - const confirm = await Popup.show.confirm(t`Are you sure you want to set \"${personaName}\" as the default persona?`, t`This name and avatar will be used for all new chats, as well as existing chats where the user persona is not locked.`); | |
| 999 | + if (!quiet) { | |
| 896 | - | |
| 1000 | + const confirm = await Popup.show.confirm(t`Are you sure you want to set \"${personaName}\" as the default persona?`, t`This name and avatar will be used for all new chats, as well as existing chats where the user persona is not locked.`); | |
| 897 | 1001 | if (!confirm) { |
| 898 | 1002 | console.debug('User cancelled setting default persona'); |
| 899 | 1003 | return; |
| 1004 | + } | |
| 900 | 1005 | } |
| 901 | 1006 | |
| 902 | 1007 | power_user.default_persona = avatarId; |
| @@ -907,12 +1012,26 @@ async function setDefaultPersona(e) { | ||
| 907 | 1012 | |
| 908 | 1013 | saveSettingsDebounced(); |
| 909 | 1014 | await getUserAvatars(true, avatarId); |
| 1015 | + updatePersonaLockIcons(); | |
| 910 | 1016 | } |
| 911 | 1017 | |
| 912 | 1018 | function updateUserLockIconupdatePersonaLockIcons() { |
| 913 | - const hasLock = !!chat_metadata['persona']; | |
| 1019 | + const isDefaultPersona = power_user.default_persona === user_avatar; | |
| 914 | 1020 | $('#lock_user_namelock_persona_default').toggleClass('fa-unlocklocked', !hasLockisDefaultPersona); |
| 915 | - $('#lock_user_name').toggleClass('fa-lock', hasLock); | |
| 1021 | + | |
| 1022 | + const hasChatLock = chat_metadata['persona'] == user_avatar; | |
| 1023 | + $('#lock_user_name').toggleClass('locked', hasChatLock); | |
| 1024 | + $('#lock_user_name i.icon').toggleClass('fa-lock', hasChatLock); | |
| 1025 | + $('#lock_user_name i.icon').toggleClass('fa-unlock', !hasChatLock); | |
| 1026 | + | |
| 1027 | + /** @type {PersonaConnection[]} */ | |
| 1028 | + const connections = power_user.persona_descriptions[user_avatar]?.connections; | |
| 1029 | + const hasCharLock = !!connections.some(c => | |
| 1030 | + (menu_type === 'character_edit' && c.type === 'character' && c.id === characters[this_chid]?.avatar) | |
| 1031 | + || (menu_type === 'group_edit' && c.type === 'group' && c.id === selected_group)); | |
| 1032 | + $('#lock_persona_to_char').toggleClass('locked', hasCharLock); | |
| 1033 | + $('#lock_persona_to_char i.icon').toggleClass('fa-lock', hasCharLock); | |
| 1034 | + $('#lock_persona_to_char i.icon').toggleClass('fa-unlock', !hasCharLock); | |
| 916 | 1035 | } |
| 917 | 1036 | |
| 918 | 1037 | async function setChatLockedPersona() { |
| @@ -942,7 +1061,7 @@ async function setChatLockedPersona() { | ||
| 942 | 1061 | if (chat_metadata['persona'] && !userAvatars.includes(chatPersona)) { |
| 943 | 1062 | console.warn('Persona avatar not found, unlocking persona'); |
| 944 | 1063 | delete chat_metadata['persona']; |
| 945 | 1064 | updateUserLockIconupdatePersonaLockIcons(); |
| 946 | 1065 | return; |
| 947 | 1066 | } |
| 948 | 1067 | |
| @@ -956,7 +1075,7 @@ async function setChatLockedPersona() { | ||
| 956 | 1075 | |
| 957 | 1076 | // Persona avatar found, select it |
| 958 | 1077 | setUserAvatar(chatPersona); |
| 959 | 1078 | updateUserLockIconupdatePersonaLockIcons(); |
| 960 | 1079 | } |
| 961 | 1080 | |
| 962 | 1081 | function onBackupPersonas() { |
| @@ -1112,9 +1231,11 @@ async function duplicatePersona(avatarId) { | ||
| 1112 | 1231 | |
| 1113 | 1232 | export function initPersonas() { |
| 1114 | 1233 | $(document).on('click', '.bind_user_name', bindUserNameToPersona); |
| 1115 | 1234 | $(document).on('click', '.set_default_persona', setDefaultPersonatoggleDefaultPersonaClicked); |
| 1116 | 1235 | $(document).on('click', '.delete_avatar', deleteUserAvatar); |
| 1117 | 1236 | $('#lock_user_namelock_persona_default').on('click', () => togglePersonaLock('default')); |
| 1237 | + $('#lock_user_name').on('click', () => togglePersonaLock('chat')); | |
| 1238 | + $('#lock_persona_to_char').on('click', () => togglePersonaLock('character')); | |
| 1118 | 1239 | $('#create_dummy_persona').on('click', createDummyPersona); |
| 1119 | 1240 | $('#persona_description').on('input', onPersonaDescriptionInput); |
| 1120 | 1241 | $('#persona_description_position').on('input', onPersonaDescriptionPositionInput); |
| @@ -3759,6 +3759,14 @@ input[type='checkbox'].del_checkbox { | ||
| 3759 | 3759 | color: var(--golden); |
| 3760 | 3760 | } |
| 3761 | 3761 | |
| 3762 | +#lock_persona_default.locked i.icon { | |
| 3763 | + color: var(--golden); | |
| 3764 | +} | |
| 3765 | +#lock_user_name.locked i.icon, | |
| 3766 | +#lock_persona_to_char.locked i.icon { | |
| 3767 | + color: var(--active); | |
| 3768 | +} | |
| 3769 | + | |
| 3762 | 3770 | #user_avatar_block .avatar_upload { |
| 3763 | 3771 | cursor: pointer; |
| 3764 | 3772 | width: 60px; |