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 @@
49244924 <input id="your_name" name="your_name" data-i18n="[placeholder]Enter your name" placeholder="Enter your name" class="text_pole wide100p" value="" autocomplete="off">
49254925 <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">
49264926 </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>
49294927 <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">
49304928 </div>
49314929 <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 @@
49374935 <div class="extension_token_counter">
49384936 <span data-i18n="Tokens persona description">Tokens</span>: <span id="persona_description_token_count">0</span>
49394937 </div>
4940- </div>
4941-
4942- <div class="persona_management_global_settings">
4943- <h4 class="standoutHeader" data-i18n="Global Settings">Global Settings</h4>
49444938
49454939 <div class="persona_management_description_position_container">
49464940 <label for="persona_description_position" data-i18n="Position:">Position:</label>
@@ -4967,6 +4961,27 @@
49674961 </div>
49684962 </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+
49704985 <div class="persona_management_show_notifications range-block">
49714986 <label for="persona_show_notifications" class="checkbox_label">
49724987 <input id="persona_show_notifications" type="checkbox" />
public/scripts/personas.js+153 -32
@@ -7,7 +7,9 @@ import {
77 event_types,
88 getRequestHeaders,
99 getThumbnailUrl,
10+ menu_type,
1011 name1,
12+ name2,
1113 reloadCurrentChat,
1214 saveChatConditional,
1315 saveMetadata,
@@ -26,6 +28,14 @@ import { t } from './i18n.js';
2628import { openWorldInfoEditor, world_names } from './world-info.js';
2729import { 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+
2939let savePersonasPage = 0;
3040const GRID_STORAGE_KEY = 'Personas_GridView';
3141const DEFAULT_DEPTH = 2;
@@ -607,15 +617,35 @@ function selectCurrentPersona() {
607617}
608618
609619/**
610620 * 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
611632 * @returns {boolean} Whether the persona is locked
612633 */
613634function 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+ }
615645}
616646
617647/**
618648 * Locks or unlocks the persona for the current chat.
619649 * @param {boolean} state Desired lock state
620650 * @returns {Promise<void>}
621651 */
@@ -624,34 +654,63 @@ export async function setPersonaLockState(state) {
624654}
625655
626656/**
627657 * Toggle the persona lock state for the current chat.
658+ * @param {PersonaLockType} type - Lock type
628659 * @returns {Promise<void>}
629660 */
630661export async function togglePersonaLock(type = 'chat') {
631662 return isPersonaLocked(type)
632663 ? await unlockPersona(type)
633664 : await lockPersona(type);
634665}
635666
636667/**
637668 * Unlock the persona for the current chat.
669+ * @param {PersonaLockType} type - Lock type
670+ * @returns {Promise<void>}
638671 */
639672async 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': {
640680 if (chat_metadata['persona']) {
641681 console.log(`Unlocking persona for${user_avatar} from this chat ${chat_metadata['persona']}`);
642682 delete chat_metadata['persona'];
643683 await saveMetadata();
644684 if (power_user.persona_show_notifications) {
645685 toastr.info(t`User persona ${name1} is now unlocked forfrom this chat. Click the "Lock" again to revert.`, t`Persona unlocked`);
646686 }
647- updateUserLockIcon();
648687 }
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();
649706}
650707
651708/**
652709 * Lock the persona for the current chat.
710+ * @param {PersonaLockType} type - Lock type
653711 */
654712async function lockPersona(type = 'chat') {
713+ // First make sure that user_avatar is actually a persona
655714 if (!(user_avatar in power_user.personas)) {
656715 console.log(`Creating a new persona ${user_avatar}`);
657716 if (power_user.persona_show_notifications) {
@@ -668,17 +727,44 @@ async function lockPersona() {
668727 depth: DEFAULT_DEPTH,
669728 role: DEFAULT_ROLE,
670729 lorebook: '',
730+ connections: [],
671731 };
672732 }
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`);
674742 chat_metadata['persona'] = user_avatar;
675743 await saveMetadata();
676744 saveSettingsDebounced();
677- console.log(`Locking persona for this chat ${user_avatar}`);
678745 if (power_user.persona_show_notifications) {
679746 toastr.success(t`User persona ${name1} is locked to ${name1name2} 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;
680764 }
681- updateUserLockIcon();
765+ }
766+
767+ updatePersonaLockIcons();
682768}
683769
684770
@@ -730,7 +816,7 @@ async function deleteUserAvatar(e) {
730816
731817 saveSettingsDebounced();
732818 await getUserAvatars();
733819 updateUserLockIconupdatePersonaLockIcons();
734820 }
735821}
736822
@@ -853,16 +939,33 @@ function getOrCreatePersonaDescriptor() {
853939 depth: power_user.persona_description_depth,
854940 role: power_user.persona_description_role,
855941 lorebook: power_user.persona_description_lorebook,
942+ connections: [],
856943 };
857944 power_user.persona_descriptions[user_avatar] = object;
858945 }
859946 return object;
860947}
861948
862-async function setDefaultPersona(e) {
949+
950+
951+async function toggleDefaultPersonaClicked(e) {
863952 e?.stopPropagation();
864953 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+}
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+ */
968+async function toggleDefaultPersona(avatarId, { quiet: quiet = false } = {}) {
866969 if (!avatarId) {
867970 console.warn('No avatar id found');
868971 return;
@@ -879,12 +982,13 @@ async function setDefaultPersona(e) {
879982 const personaName = power_user.personas[avatarId];
880983
881984 if (avatarId === currentDefault) {
985+ if (!quiet) {
882986 const confirm = await Popup.show.confirm(t`Are you sure you want to remove the default persona?`, personaName);
883-
884987 if (!confirm) {
885988 console.debug('User cancelled removing default persona');
886989 return;
887990 }
991+ }
888992
889993 console.log(`Removing default persona ${avatarId}`);
890994 if (power_user.persona_show_notifications) {
@@ -892,12 +996,13 @@ async function setDefaultPersona(e) {
892996 }
893997 delete power_user.default_persona;
894998 } else {
999+ if (!quiet) {
8951000 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-
8971001 if (!confirm) {
8981002 console.debug('User cancelled setting default persona');
8991003 return;
9001004 }
1005+ }
9011006
9021007 power_user.default_persona = avatarId;
9031008 if (power_user.persona_show_notifications) {
@@ -907,12 +1012,26 @@ async function setDefaultPersona(e) {
9071012
9081013 saveSettingsDebounced();
9091014 await getUserAvatars(true, avatarId);
1015+ updatePersonaLockIcons();
9101016}
9111017
9121018function updateUserLockIconupdatePersonaLockIcons() {
913- const hasLock = !!chat_metadata['persona'];
1019+ const isDefaultPersona = power_user.default_persona === user_avatar;
9141020 $('#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);
9161035}
9171036
9181037async function setChatLockedPersona() {
@@ -942,7 +1061,7 @@ async function setChatLockedPersona() {
9421061 if (chat_metadata['persona'] && !userAvatars.includes(chatPersona)) {
9431062 console.warn('Persona avatar not found, unlocking persona');
9441063 delete chat_metadata['persona'];
9451064 updateUserLockIconupdatePersonaLockIcons();
9461065 return;
9471066 }
9481067
@@ -956,7 +1075,7 @@ async function setChatLockedPersona() {
9561075
9571076 // Persona avatar found, select it
9581077 setUserAvatar(chatPersona);
9591078 updateUserLockIconupdatePersonaLockIcons();
9601079}
9611080
9621081function onBackupPersonas() {
@@ -1112,9 +1231,11 @@ async function duplicatePersona(avatarId) {
11121231
11131232export function initPersonas() {
11141233 $(document).on('click', '.bind_user_name', bindUserNameToPersona);
11151234 $(document).on('click', '.set_default_persona', setDefaultPersonatoggleDefaultPersonaClicked);
11161235 $(document).on('click', '.delete_avatar', deleteUserAvatar);
11171236 $('#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'));
11181239 $('#create_dummy_persona').on('click', createDummyPersona);
11191240 $('#persona_description').on('input', onPersonaDescriptionInput);
11201241 $('#persona_description_position').on('input', onPersonaDescriptionPositionInput);
public/style.css+8 -0
@@ -3759,6 +3759,14 @@ input[type='checkbox'].del_checkbox {
37593759 color: var(--golden);
37603760}
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+
37623770#user_avatar_block .avatar_upload {
37633771 cursor: pointer;
37643772 width: 60px;