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)

74fc259c9cbc7a9d19a6212072d3673383bbdc8f

Wolfsblvt <wolfsblvt@gmail.com>

3 files changed, +182 -38Showing whitespace changes
public/index.html+21 -6
@@ -4924,8 +4924,6 @@
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">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 <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">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 </div>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 <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">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 </div>4928 </div>
4931 <div id="persona_lore_button" class="menu_button fa-solid fa-globe" title="Persona Lore&#10;Alt+Click to open the lorebook" data-i18n="[title]Persona Lore Alt+Click to open the lorebook">4929 <div id="persona_lore_button" class="menu_button fa-solid fa-globe" title="Persona Lore&#10;Alt+Click to open the lorebook" data-i18n="[title]Persona Lore Alt+Click to open the lorebook">
@@ -4937,10 +4935,6 @@
4937 <div class="extension_token_counter">4935 <div class="extension_token_counter">
4938 <span data-i18n="Tokens persona description">Tokens</span>: <span id="persona_description_token_count">0</span>4936 <span data-i18n="Tokens persona description">Tokens</span>: <span id="persona_description_token_count">0</span>
4939 </div>4937 </div>
4940 </div>
4941
4942 <div class="persona_management_global_settings">
4943 <h4 class="standoutHeader" data-i18n="Global Settings">Global Settings</h4>
49444938
4945 <div class="persona_management_description_position_container">4939 <div class="persona_management_description_position_container">
4946 <label for="persona_description_position" data-i18n="Position:">Position:</label>4940 <label for="persona_description_position" data-i18n="Position:">Position:</label>
@@ -4967,6 +4961,27 @@
4967 </div>4961 </div>
4968 </div>4962 </div>
49694963
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 <div class="persona_management_show_notifications range-block">4985 <div class="persona_management_show_notifications range-block">
4971 <label for="persona_show_notifications" class="checkbox_label">4986 <label for="persona_show_notifications" class="checkbox_label">
4972 <input id="persona_show_notifications" type="checkbox" />4987 <input id="persona_show_notifications" type="checkbox" />
public/scripts/personas.js+153 -32
@@ -7,7 +7,9 @@ import {
7 event_types,7 event_types,
8 getRequestHeaders,8 getRequestHeaders,
9 getThumbnailUrl,9 getThumbnailUrl,
10 menu_type,
10 name1,11 name1,
12 name2,
11 reloadCurrentChat,13 reloadCurrentChat,
12 saveChatConditional,14 saveChatConditional,
13 saveMetadata,15 saveMetadata,
@@ -26,6 +28,14 @@ import { t } from './i18n.js';
26import { openWorldInfoEditor, world_names } from './world-info.js';28import { openWorldInfoEditor, world_names } from './world-info.js';
27import { renderTemplateAsync } from './templates.js';29import { renderTemplateAsync } from './templates.js';
2830
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
29let savePersonasPage = 0;39let savePersonasPage = 0;
30const GRID_STORAGE_KEY = 'Personas_GridView';40const GRID_STORAGE_KEY = 'Personas_GridView';
31const DEFAULT_DEPTH = 2;41const DEFAULT_DEPTH = 2;
@@ -607,15 +617,35 @@ function selectCurrentPersona() {
607}617}
608618
609/**619/**
610 * Checks if the persona is locked for the current chat.620 * Checks if a connection is locked for the current character or group edit menu
621 * @param {PersonaConnection} connection - Connection to check
622 * @returns {boolean} Whether the connection is locked
623 */
624function 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 * @returns {boolean} Whether the persona is locked632 * @returns {boolean} Whether the persona is locked
612 */633 */
613function isPersonaLocked() {634function 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}
616646
617/**647/**
618 * Locks or unlocks the persona for the current chat.648 * Locks or unlocks the persona
619 * @param {boolean} state Desired lock state649 * @param {boolean} state Desired lock state
620 * @returns {Promise<void>}650 * @returns {Promise<void>}
621 */651 */
@@ -624,34 +654,63 @@ export async function setPersonaLockState(state) {
624}654}
625655
626/**656/**
627 * Toggle the persona lock state for the current chat.657 * Toggle the persona lock state
658 * @param {PersonaLockType} type - Lock type
628 * @returns {Promise<void>}659 * @returns {Promise<void>}
629 */660 */
630export async function togglePersonaLock() {661export async function togglePersonaLock(type = 'chat') {
631 return isPersonaLocked()662 return isPersonaLocked(type)
632 ? await unlockPersona()663 ? await unlockPersona(type)
633 : await lockPersona();664 : await lockPersona(type);
634}665}
635666
636/**667/**
637 * Unlock the persona for the current chat.668 * Unlock the persona
669 * @param {PersonaLockType} type - Lock type
670 * @returns {Promise<void>}
638 */671 */
639async function unlockPersona() {672async function unlockPersona(type = 'chat') {
673 switch (type) {
674 case 'default': {
675 // TODO: Make this toggle-able
676 await toggleDefaultPersona(user_avatar, { quiet: true });
677 break;
678 }
679 case 'chat': {
640 if (chat_metadata['persona']) {680 if (chat_metadata['persona']) {
641 console.log(`Unlocking persona for this chat ${chat_metadata['persona']}`);681 console.log(`Unlocking persona ${user_avatar} from this chat`);
642 delete chat_metadata['persona'];682 delete chat_metadata['persona'];
643 await saveMetadata();683 await saveMetadata();
644 if (power_user.persona_show_notifications) {684 if (power_user.persona_show_notifications) {
645 toastr.info(t`User persona is now unlocked for this chat. Click the "Lock" again to revert.`, t`Persona unlocked`);685 toastr.info(t`User persona ${name1} is now unlocked from this chat. Click the "Lock" again to revert.`, t`Persona unlocked`);
646 }686 }
647 updateUserLockIcon();
648 }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;
702 }
703 }
704
705 updatePersonaLockIcons();
649}706}
650707
651/**708/**
652 * Lock the persona for the current chat.709 * Lock the persona
710 * @param {PersonaLockType} type - Lock type
653 */711 */
654async function lockPersona() {712async function lockPersona(type = 'chat') {
713 // First make sure that user_avatar is actually a persona
655 if (!(user_avatar in power_user.personas)) {714 if (!(user_avatar in power_user.personas)) {
656 console.log(`Creating a new persona ${user_avatar}`);715 console.log(`Creating a new persona ${user_avatar}`);
657 if (power_user.persona_show_notifications) {716 if (power_user.persona_show_notifications) {
@@ -668,17 +727,44 @@ async function lockPersona() {
668 depth: DEFAULT_DEPTH,727 depth: DEFAULT_DEPTH,
669 role: DEFAULT_ROLE,728 role: DEFAULT_ROLE,
670 lorebook: '',729 lorebook: '',
730 connections: [],
671 };731 };
672 }732 }
673733
734 switch (type) {
735 case 'default': {
736 // TODO: Make this toggle-able
737 await toggleDefaultPersona(user_avatar, { quiet: true });
738 break;
739 }
740 case 'chat': {
741 console.log(`Locking persona ${user_avatar} to this chat`);
674 chat_metadata['persona'] = user_avatar;742 chat_metadata['persona'] = user_avatar;
675 await saveMetadata();743 await saveMetadata();
676 saveSettingsDebounced();744 saveSettingsDebounced();
677 console.log(`Locking persona for this chat ${user_avatar}`);
678 if (power_user.persona_show_notifications) {745 if (power_user.persona_show_notifications) {
679 toastr.success(t`User persona is locked to ${name1} in this chat`);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;
680 }764 }
681 updateUserLockIcon();765 }
766
767 updatePersonaLockIcons();
682}768}
683769
684770
@@ -730,7 +816,7 @@ async function deleteUserAvatar(e) {
730816
731 saveSettingsDebounced();817 saveSettingsDebounced();
732 await getUserAvatars();818 await getUserAvatars();
733 updateUserLockIcon();819 updatePersonaLockIcons();
734 }820 }
735}821}
736822
@@ -853,16 +939,33 @@ function getOrCreatePersonaDescriptor() {
853 depth: power_user.persona_description_depth,939 depth: power_user.persona_description_depth,
854 role: power_user.persona_description_role,940 role: power_user.persona_description_role,
855 lorebook: power_user.persona_description_lorebook,941 lorebook: power_user.persona_description_lorebook,
942 connections: [],
856 };943 };
857 power_user.persona_descriptions[user_avatar] = object;944 power_user.persona_descriptions[user_avatar] = object;
858 }945 }
859 return object;946 return object;
860}947}
861948
862async function setDefaultPersona(e) {949
950
951async function toggleDefaultPersonaClicked(e) {
863 e?.stopPropagation();952 e?.stopPropagation();
864 const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');953 const avatarId = $(e.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}
865960
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 */
968async function toggleDefaultPersona(avatarId, { quiet: quiet = false } = {}) {
866 if (!avatarId) {969 if (!avatarId) {
867 console.warn('No avatar id found');970 console.warn('No avatar id found');
868 return;971 return;
@@ -879,12 +982,13 @@ async function setDefaultPersona(e) {
879 const personaName = power_user.personas[avatarId];982 const personaName = power_user.personas[avatarId];
880983
881 if (avatarId === currentDefault) {984 if (avatarId === currentDefault) {
985 if (!quiet) {
882 const confirm = await Popup.show.confirm(t`Are you sure you want to remove the default persona?`, personaName);986 const confirm = await Popup.show.confirm(t`Are you sure you want to remove the default persona?`, personaName);
883
884 if (!confirm) {987 if (!confirm) {
885 console.debug('User cancelled removing default persona');988 console.debug('User cancelled removing default persona');
886 return;989 return;
887 }990 }
991 }
888992
889 console.log(`Removing default persona ${avatarId}`);993 console.log(`Removing default persona ${avatarId}`);
890 if (power_user.persona_show_notifications) {994 if (power_user.persona_show_notifications) {
@@ -892,12 +996,13 @@ async function setDefaultPersona(e) {
892 }996 }
893 delete power_user.default_persona;997 delete power_user.default_persona;
894 } else {998 } else {
999 if (!quiet) {
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.`);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.`);
896
897 if (!confirm) {1001 if (!confirm) {
898 console.debug('User cancelled setting default persona');1002 console.debug('User cancelled setting default persona');
899 return;1003 return;
900 }1004 }
1005 }
9011006
902 power_user.default_persona = avatarId;1007 power_user.default_persona = avatarId;
903 if (power_user.persona_show_notifications) {1008 if (power_user.persona_show_notifications) {
@@ -907,12 +1012,26 @@ async function setDefaultPersona(e) {
9071012
908 saveSettingsDebounced();1013 saveSettingsDebounced();
909 await getUserAvatars(true, avatarId);1014 await getUserAvatars(true, avatarId);
1015 updatePersonaLockIcons();
910}1016}
9111017
912function updateUserLockIcon() {1018function updatePersonaLockIcons() {
913 const hasLock = !!chat_metadata['persona'];1019 const isDefaultPersona = power_user.default_persona === user_avatar;
914 $('#lock_user_name').toggleClass('fa-unlock', !hasLock);1020 $('#lock_persona_default').toggleClass('locked', isDefaultPersona);
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}
9171036
918async function setChatLockedPersona() {1037async function setChatLockedPersona() {
@@ -942,7 +1061,7 @@ async function setChatLockedPersona() {
942 if (chat_metadata['persona'] && !userAvatars.includes(chatPersona)) {1061 if (chat_metadata['persona'] && !userAvatars.includes(chatPersona)) {
943 console.warn('Persona avatar not found, unlocking persona');1062 console.warn('Persona avatar not found, unlocking persona');
944 delete chat_metadata['persona'];1063 delete chat_metadata['persona'];
945 updateUserLockIcon();1064 updatePersonaLockIcons();
946 return;1065 return;
947 }1066 }
9481067
@@ -956,7 +1075,7 @@ async function setChatLockedPersona() {
9561075
957 // Persona avatar found, select it1076 // Persona avatar found, select it
958 setUserAvatar(chatPersona);1077 setUserAvatar(chatPersona);
959 updateUserLockIcon();1078 updatePersonaLockIcons();
960}1079}
9611080
962function onBackupPersonas() {1081function onBackupPersonas() {
@@ -1112,9 +1231,11 @@ async function duplicatePersona(avatarId) {
11121231
1113export function initPersonas() {1232export function initPersonas() {
1114 $(document).on('click', '.bind_user_name', bindUserNameToPersona);1233 $(document).on('click', '.bind_user_name', bindUserNameToPersona);
1115 $(document).on('click', '.set_default_persona', setDefaultPersona);1234 $(document).on('click', '.set_default_persona', toggleDefaultPersonaClicked);
1116 $(document).on('click', '.delete_avatar', deleteUserAvatar);1235 $(document).on('click', '.delete_avatar', deleteUserAvatar);
1117 $('#lock_user_name').on('click', togglePersonaLock);1236 $('#lock_persona_default').on('click', () => togglePersonaLock('default'));
1237 $('#lock_user_name').on('click', () => togglePersonaLock('chat'));
1238 $('#lock_persona_to_char').on('click', () => togglePersonaLock('character'));
1118 $('#create_dummy_persona').on('click', createDummyPersona);1239 $('#create_dummy_persona').on('click', createDummyPersona);
1119 $('#persona_description').on('input', onPersonaDescriptionInput);1240 $('#persona_description').on('input', onPersonaDescriptionInput);
1120 $('#persona_description_position').on('input', onPersonaDescriptionPositionInput);1241 $('#persona_description_position').on('input', onPersonaDescriptionPositionInput);
public/style.css+8 -0
@@ -3759,6 +3759,14 @@ input[type='checkbox'].del_checkbox {
3759 color: var(--golden);3759 color: var(--golden);
3760}3760}
37613761
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#user_avatar_block .avatar_upload {3770#user_avatar_block .avatar_upload {
3763 cursor: pointer;3771 cursor: pointer;
3764 width: 60px;3772 width: 60px;