Add persona connection states to persona list - Show persona connection/lock states in persona list (chat & char lock) - Refactor persona images url to 'data-avatar-id' attribute - Try make persona blocks' height consistent - Fix persona list not correctly updating selected persona on navigating pages - Fix group/char difference not correctly working - Create common style template for hover-able images buttons/tags where the label only appears on hover

e5db40cf2dc22c7ca45ab73424d0b6bf594230e6

Wolfsblvt <wolfsblvt@gmail.com>

4 files changed, +162 -99Showing whitespace changes
public/css/toggle-dependent.css+0 -5
@@ -96,11 +96,6 @@ body.charListGrid #rm_print_characters_block .group_select .group_name_block,
9696 flex-direction: column;
9797}
9898
99-#user_avatar_block.gridView .avatar-container .avatar-buttons {
100- flex-wrap: wrap;
101- justify-content: space-evenly;
102-}
103-
10499body.charListGrid #rm_print_characters_block .bogus_folder_select .character_select_container,
105100body.charListGrid #rm_print_characters_block .character_select .character_select_container,
106101body.charListGrid #rm_print_characters_block .group_select .group_select_container,
public/index.html+12 -19
@@ -5470,27 +5470,20 @@
54705470 <div class="flex-container wide100pLess70px character_select_container">
54715471 <div class="wide100p character_name_block">
54725472 <span class="ch_name flex1"></span>
5473- <div class="avatar-buttons">
5474- <!-- <button class="menu_button bind_user_name" title="Bind user name to that avatar" data-i18n="[title]Bind user name to that avatar">
5475- <i class="fa-fw fa-solid fa-user-edit fa-sm"></i>
5476- </button>
5477- <button class="menu_button set_persona_image" title="Change persona image" data-i18n="[title]Change persona image">
5478- <i class="fa-fw fa-solid fa-image fa-sm"></i>
5479- </button>
5480-
5481- <button class="menu_button set_default_persona" title="Select this as default persona for the new chats." data-i18n="[title]Select this as default persona for the new chats.">
5482- <i class="fa-fw fa-solid fa-crown fa-sm"></i>
5483- </button>
5484-
5485- <button class="menu_button duplicate_persona" title="Duplicate persona" data-i18n="[title]Duplicate persona">
5486- <i class="fa-fw fa-solid fa-clone fa-sm"></i>
5487- </button>
5488- <button class="menu_button delete_avatar" title="Delete persona" data-i18n="[title]Delete persona">
5489- <i class="fa-fw fa-solid fa-trash-alt fa-sm"></i>
5490- </button> -->
5491- </div>
54925473 </div>
54935474 <div class="ch_description"></div>
5475+ <div class="avatar_container_states buttons_block">
5476+ <div class="locked_to_chat_label avatar_state has_hover_label menu_button menu_button_icon disabled" title="Persona is locked to the current chat" data-i18n="[title]Persona is locked to the current chat">
5477+ <i class="icon fa-solid fa-lock fa-fw"></i>
5478+ <i class="label_icon icon fa-solid fa-comments fa-fw"></i>
5479+ <div class="label" data-i18n="Chat">Chat</div>
5480+ </div>
5481+ <div class="locked_to_character_label avatar_state has_hover_label menu_button menu_button_icon disabled " title="Persona is locked to the current character" data-i18n="[title]Persona is locked to the current character">
5482+ <i class="icon fa-solid fa-lock fa-fw"></i>
5483+ <i class="label_icon icon fa-solid fa-user fa-fw"></i>
5484+ <div class="label" data-i18n="Character">Character</div>
5485+ </div>
5486+ </div>
54945487 </div>
54955488 </div>
54965489 </div>
public/scripts/personas.js+92 -45
@@ -10,7 +10,6 @@ import {
1010 getRequestHeaders,
1111 getThumbnailUrl,
1212 groupToEntity,
13- menu_type,
1413 name1,
1514 name2,
1615 reloadCurrentChat,
@@ -40,6 +39,15 @@ import { saveMetadataDebounced } from './extensions.js';
4039
4140/** @typedef {'chat' | 'character' | 'default'} PersonaLockType Type of the persona lock */
4241
42+/**
43+ * @typedef {object} PersonaState
44+ * @property {string} avatarId - The avatar id of the persona
45+ * @property {boolean} default - Whether this persona is the default one for all new chats
46+ * @property {object} locked - An object containing the lock states
47+ * @property {boolean} locked.chat - Whether the persona is locked to the currently open chat
48+ * @property {boolean} locked.character - Whether the persona is locked to the currently open character or group
49+ */
50+
4351const USER_AVATAR_PATH = 'User Avatars/';
4452
4553let savePersonasPage = 0;
@@ -66,7 +74,7 @@ export function getUserAvatar(avatarImg) {
6674export function initUserAvatar(avatar) {
6775 user_avatar = avatar;
6876 reloadUserAvatar();
6977 highlightSelectedAvatarupdatePersonaUIStates();
7078}
7179
7280/**
@@ -74,19 +82,14 @@ export function initUserAvatar(avatar) {
7482 * @param {string} imgfile Link to an image file
7583 */
7684export function setUserAvatar(imgfile, { toastPersonaNameChange = true } = {}) {
7785 user_avatar = imgfile && typeof imgfile === 'string' ? imgfile : $(this).attr('imgfiledata-avatar-id');
7886 reloadUserAvatar();
7987 highlightSelectedAvatarupdatePersonaUIStates();
8088 selectCurrentPersona({ toastPersonaNameChange: toastPersonaNameChange });
8189 saveSettingsDebounced();
8290 $('.zoomed_avatar[forchar]').remove();
8391}
8492
85-function highlightSelectedAvatar() {
86- $('#user_avatar_block .avatar-container').removeClass('selected');
87- $(`#user_avatar_block .avatar-container[imgfile="${user_avatar}"]`).addClass('selected');
88-}
89-
9093function reloadUserAvatar(force = false) {
9194 $('.mes').each(function () {
9295 const avatarImg = $(this).find('.avatar img');
@@ -146,24 +149,32 @@ function verifyPersonaSearchSortRule() {
146149
147150/**
148151 * Gets a rendered avatar block.
149152 * @param {string} nameavatarId Avatar file name
150153 * @returns {JQuery<HTMLElement>} Avatar block
151154 */
152155function getUserAvatarBlock(nameavatarId) {
153156 const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
154157 const template = $('#user_avatar_template .avatar-container').clone();
155158 const personaName = power_user.personas[nameavatarId];
156159 const personaDescription = power_user.persona_descriptions[nameavatarId]?.description;
160+
157161 template.find('.ch_name').text(personaName || '[Unnamed Persona]');
158162 template.find('.ch_description').text(personaDescription || $('#user_avatar_block').attr('no_desc_text')).toggleClass('text_muted', !personaDescription);
159163 template.attr('imgfiledata-avatar-id', nameavatarId);
160164 template.find('.avatar').attr('imgfiledata-avatar-id', nameavatarId).attr('title', nameavatarId);
161165 template.toggleClass('default_persona', nameavatarId === power_user.default_persona);
162166 let avatarUrl = getUserAvatar(nameavatarId);
163167 if (isFirefox) {
164168 avatarUrl += '?t=' + Date.now();
165169 }
166170 template.find('img').attr('src', avatarUrl);
171+
172+ // Make sure description block has at least three rows. Otherwise height looks inconsistent. I don't have a better idea for this.
173+ const currentText = template.find('.ch_description').text();
174+ if (currentText.split('\n').length < 3) {
175+ template.find('.ch_description').text(currentText + '\n\xa0\n\xa0');
176+ }
177+
167178 $('#user_avatar_block').append(template);
168179 return template;
169180}
@@ -218,7 +229,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
218229 for (const item of data) {
219230 $(listId).append(getUserAvatarBlock(item));
220231 }
221232 highlightSelectedAvatarupdatePersonaUIStates();
222233 },
223234 afterSizeSelectorChange: function (e) {
224235 localStorage.setItem(storageKey, e.target.value);
@@ -486,7 +497,7 @@ export function setPersonaDescription() {
486497 $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook);
487498 countPersonaDescriptionTokens();
488499
489500 updatePersonaLockIconsupdatePersonaUIStates();
490501 updatePersonaConnectionsAvatarList();
491502}
492503
@@ -780,7 +791,7 @@ function selectCurrentPersona({ toastPersonaNameChange = true } = {}) {
780791 toastr.success(`Persona ${personaName} selected and auto-locked to current chat`, t`Persona Selected`);
781792 }
782793 saveMetadataDebounced();
783794 updatePersonaLockIconsupdatePersonaUIStates();
784795 }
785796
786797 // As the last step, inform user if the persona is only temporarily chosen
@@ -806,8 +817,8 @@ function selectCurrentPersona({ toastPersonaNameChange = true } = {}) {
806817 * @returns {boolean} Whether the connection is locked
807818 */
808819export function isPersonaConnectionLocked(connection) {
809820 return (menu_type === 'character_edit'!selected_group && connection.type === 'character' && connection.id === characters[this_chid]?.avatar)
810821 || (menu_type === 'group_edit'selected_group && connection.type === 'group' && connection.id === selected_group);
811822}
812823
813824/**
@@ -894,7 +905,7 @@ async function unlockPersona(type = 'chat') {
894905 throw new Error(`Unknown persona lock type: ${type}`);
895906 }
896907
897908 updatePersonaLockIconsupdatePersonaUIStates();
898909}
899910
900911/**
@@ -969,7 +980,7 @@ async function lockPersona(type = 'chat') {
969980 throw new Error(`Unknown persona lock type: ${type}`);
970981 }
971982
972983 updatePersonaLockIconsupdatePersonaUIStates();
973984}
974985
975986
@@ -1149,7 +1160,7 @@ function getOrCreatePersonaDescriptor() {
11491160
11501161async function toggleDefaultPersonaClicked(e) {
11511162 e?.stopPropagation();
11521163 const avatarId = $(e.currentTarget).closest('.avatar-container').find('.avatar').attr('imgfiledata-avatar-id');
11531164 if (avatarId) {
11541165 await toggleDefaultPersona(avatarId);
11551166 } else {
@@ -1213,26 +1224,62 @@ async function toggleDefaultPersona(avatarId, { quiet: quiet = false } = {}) {
12131224
12141225 saveSettingsDebounced();
12151226 await getUserAvatars(true, avatarId);
12161227 updatePersonaLockIconsupdatePersonaUIStates();
12171228}
12181229
1219-function updatePersonaLockIcons() {
1230+/**
1220- const isDefaultPersona = power_user.default_persona === user_avatar;
1231+ * Returns an object with 3 properties that describe the state of the given persona
1221- $('#lock_persona_default').toggleClass('locked', isDefaultPersona);
1232+ *
1222-
1233+ * - default: Whether this persona is the default one for all new chats
1223- const hasChatLock = chat_metadata['persona'] == user_avatar;
1234+ * - locked: An object containing the lock states
1224- $('#lock_user_name').toggleClass('locked', hasChatLock);
1235+ * - chat: Whether the persona is locked to the currently open chat
1225- $('#lock_user_name i.icon').toggleClass('fa-lock', hasChatLock);
1236+ * - character: Whether the persona is locked to the currently open character or group
1226- $('#lock_user_name i.icon').toggleClass('fa-unlock', !hasChatLock);
1237+ * @param {string} avatarId - The avatar id of the persona to get the state for
1238+ * @returns {PersonaState} An object describing the state of the given persona
1239+ */
1240+function getPersonaStates(avatarId) {
1241+ const isDefaultPersona = power_user.default_persona === avatarId;
1242+ const hasChatLock = chat_metadata['persona'] == avatarId;
12271243
12281244 /** @type {PersonaConnection[]} */
12291245 const connections = power_user.persona_descriptions[user_avataravatarId]?.connections;
12301246 const hasCharLock = !!connections?.some(c =>
12311247 (menu_type === 'character_edit'!selected_group && c.type === 'character' && c.id === characters[this_chid]?.avatar)
12321248 || (menu_type === 'group_edit'selected_group && c.type === 'group' && c.id === selected_group));
1233- $('#lock_persona_to_char').toggleClass('locked', hasCharLock);
1249+
1234- $('#lock_persona_to_char i.icon').toggleClass('fa-lock', hasCharLock);
1250+ return {
1235- $('#lock_persona_to_char i.icon').toggleClass('fa-unlock', !hasCharLock);
1251+ avatarId: avatarId,
1252+ default: isDefaultPersona,
1253+ locked: {
1254+ chat: hasChatLock,
1255+ character: hasCharLock,
1256+ },
1257+ };
1258+}
1259+
1260+function updatePersonaUIStates() {
1261+ // Update the persona list
1262+ $('#user_avatar_block .avatar-container').each(function () {
1263+ const avatarId = $(this).attr('data-avatar-id');
1264+ const states = getPersonaStates(avatarId);
1265+ $(this).toggleClass('default_persona', states.default);
1266+ $(this).toggleClass('locked_to_chat', states.locked.chat);
1267+ $(this).toggleClass('locked_to_character', states.locked.character);
1268+ $(this).toggleClass('selected', avatarId === user_avatar);
1269+ });
1270+
1271+ // Buttons for the persona panel on the right
1272+ const personaStates = getPersonaStates(user_avatar);
1273+
1274+ $('#lock_persona_default').toggleClass('locked', personaStates.default);
1275+
1276+ $('#lock_user_name').toggleClass('locked', personaStates.locked.chat);
1277+ $('#lock_user_name i.icon').toggleClass('fa-lock', personaStates.locked.chat);
1278+ $('#lock_user_name i.icon').toggleClass('fa-unlock', !personaStates.locked.chat);
1279+
1280+ $('#lock_persona_to_char').toggleClass('locked', personaStates.locked.character);
1281+ $('#lock_persona_to_char i.icon').toggleClass('fa-lock', personaStates.locked.character);
1282+ $('#lock_persona_to_char i.icon').toggleClass('fa-unlock', !personaStates.locked.character);
12361283}
12371284
12381285async function loadPersonaForCurrentChat({ doRender = false } = {}) {
@@ -1315,7 +1362,7 @@ async function loadPersonaForCurrentChat({ doRender = false } = {}) {
13151362 }
13161363 }
13171364
13181365 updatePersonaLockIconsupdatePersonaUIStates();
13191366}
13201367
13211368/**
@@ -1325,7 +1372,7 @@ async function loadPersonaForCurrentChat({ doRender = false } = {}) {
13251372 * @returns {string[]} - An array of persona keys that are connected to the given character key
13261373 */
13271374export function getConnectedPersonas(characterKey = undefined) {
13281375 characterKey ??= menu_type === 'group_edit' ? selected_group :|| characters[this_chid]?.avatar;
13291376 const connectedPersonas = Object.entries(power_user.persona_descriptions)
13301377 .filter(([_, desc]) => desc.connections?.some(conn => conn.type === 'character' && conn.id === characterKey))
13311378 .map(([key, _]) => key);
@@ -1339,9 +1386,9 @@ export function getConnectedPersonas(characterKey = undefined) {
13391386 */
13401387
13411388export function getCurrentConnectionObj() {
13421389 if (menu_type === 'group_edit'selected_group)
13431390 return { type: 'group', id: selected_group };
1344- if (menu_type == 'character_edit')
1391+ if (characters[this_chid]?.avatar)
13451392 return { type: 'character', id: characters[this_chid]?.avatar };
13461393 return null;
13471394}
@@ -1538,7 +1585,7 @@ export function initPersonas() {
15381585 $('#avatar_upload_file').on('change', changeUserAvatar);
15391586
15401587 $(document).on('click', '#user_avatar_block .avatar-container', function () {
15411588 const imgfile = $(this).attr('imgfiledata-avatar-id');
15421589 setUserAvatar(imgfile);
15431590
15441591 // force firstMes {{user}} update on persona switch
public/style.css+58 -30
@@ -222,6 +222,37 @@ table.responsiveTable {
222222 animation-name: flash;
223223}
224224
225+.has_hover_label .label_icon {
226+ transition: opacity var(--animation-duration) ease, max-width var(--animation-duration) ease;
227+}
228+.has_hover_label .label {
229+ transition: opacity var(--animation-duration-slow) ease, max-width var(--animation-duration-slow) ease;
230+ /* Prevent double gap on hidden icon */
231+ margin-left: -5px;
232+}
233+
234+.has_hover_label .label_icon,
235+.has_hover_label .label {
236+ transition-delay: var(--animation-duration-slow);
237+}
238+.has_hover_label.fast .label_icon,
239+.has_hover_label.fast .label {
240+ transition-delay: var(--animation-duration);
241+}
242+
243+.has_hover_label .label_icon,
244+.has_hover_label:hover .label {
245+ opacity: 1;
246+ max-width: 100px;
247+}
248+
249+.has_hover_label:hover .label_icon,
250+.has_hover_label .label {
251+ opacity: 0;
252+ max-width: 0;
253+ overflow: hidden;
254+}
255+
225256/* Keyboard/focus navigation styling */
226257/* Mimic the outline of keyboard navigation for most most focusable controls */
227258.interactable {
@@ -2805,6 +2836,7 @@ select option:not(:checked) {
28052836.menu_button.disabled {
28062837 filter: brightness(75%) grayscale(1);
28072838 opacity: 0.5;
2839+ cursor: inherit;
28082840 pointer-events: none;
28092841}
28102842
@@ -2880,8 +2912,7 @@ select option:not(:checked) {
28802912}
28812913
28822914#form_character_search_form .menu_button,
28832915#GroupFavDelOkBack .menu_button, {
2884-.avatar-container .menu_button {
28852916 margin: 0;
28862917 height: fit-content;
28872918 padding: 5px;
@@ -2915,8 +2946,9 @@ select option:not(:checked) {
29152946 width: max-content;
29162947}
29172948
29182949#persona-management-block .avatar_container_states .menu_button {
2919- filter: grayscale(0.5);
2950+ padding: 3px 5px;
2951+ pointer-events: initial;
29202952}
29212953
29222954#persona_controls .persona_name {
@@ -3321,28 +3353,6 @@ grammarly-extension {
33213353 z-index: 35;
33223354}
33233355
3324-.avatar-container .avatar-buttons {
3325- display: flex;
3326- flex-direction: row;
3327- gap: 3px;
3328- opacity: 0.3;
3329- transition: opacity 0.25s ease-in-out;
3330-}
3331-
3332-.avatar-container .avatar-buttons:hover {
3333- opacity: 1;
3334-}
3335-
3336-.avatar-container .avatar-buttons .menu_button {
3337- padding: 3px;
3338-}
3339-
3340-/* Ross should be able to handle this later */
3341-/*.big-avatars .avatar-buttons{
3342- justify-content: center;
3343- width: fit-content;
3344-}*/
3345-
33463356.avatar_div .avatar {
33473357 /* margin-left: 4px;
33483358 margin-right: 10px;
@@ -3603,6 +3613,7 @@ grammarly-extension {
36033613
36043614.menu_button {
36053615 color: var(--SmartThemeBodyColor);
3616+ filter: grayscale(0.5);
36063617 background-color: var(--SmartThemeBlurTintColor);
36073618 border: 1px solid var(--SmartThemeBorderColor);
36083619 border-radius: 5px;
@@ -3622,8 +3633,8 @@ grammarly-extension {
36223633 min-width: calc(1.25em + 12px);
36233634}
36243635
36253636.menu_button:not(.disabled):hover,
36263637.menu_button:not(.disabled).active {
36273638 background-color: var(--white30a);
36283639}
36293640
@@ -3876,11 +3887,28 @@ input[type='checkbox'].del_checkbox {
38763887 color: var(--golden);
38773888}
38783889
3890+.avatar-container .avatar_state .fa-lock {
3891+ color: var(--active);
3892+}
3893+
3894+.avatar-container:not(.locked_to_chat) .locked_to_chat_label {
3895+ display: none;
3896+}
3897+.avatar-container:not(.locked_to_character) .locked_to_character_label {
3898+ display: none;
3899+}
3900+
38793901#lock_persona_default.locked i.icon {
38803902 color: var(--golden);
38813903}
3882-#lock_user_name.locked i.icon,
3904+
38833905#lock_persona_to_charlock_user_name.locked i.icon {,
3906+.avatar-container.locked_to_chat .locked_to_chat_label .icon {
3907+ color: var(--SmartThemeQuoteColor);
3908+}
3909+
3910+#lock_persona_to_char.locked .icon,
3911+.avatar-container.locked_to_character .locked_to_character_label .icon {
38843912 color: var(--active);
38853913}
38863914