diff --git a/public/css/toggle-dependent.css b/public/css/toggle-dependent.css
index 7b7ccbee1..e08dfdf34 100644
--- a/public/css/toggle-dependent.css
+++ b/public/css/toggle-dependent.css
@@ -96,11 +96,6 @@ body.charListGrid #rm_print_characters_block .group_select .group_name_block,
flex-direction: column;
}
-#user_avatar_block.gridView .avatar-container .avatar-buttons {
- flex-wrap: wrap;
- justify-content: space-evenly;
-}
-
body.charListGrid #rm_print_characters_block .bogus_folder_select .character_select_container,
body.charListGrid #rm_print_characters_block .character_select .character_select_container,
body.charListGrid #rm_print_characters_block .group_select .group_select_container,
diff --git a/public/index.html b/public/index.html
index b3d1ed1f2..c07dfb653 100644
--- a/public/index.html
+++ b/public/index.html
@@ -5470,27 +5470,20 @@
diff --git a/public/scripts/personas.js b/public/scripts/personas.js
index 7afb3ef0c..c0d6a9a83 100644
--- a/public/scripts/personas.js
+++ b/public/scripts/personas.js
@@ -10,7 +10,6 @@ import {
getRequestHeaders,
getThumbnailUrl,
groupToEntity,
- menu_type,
name1,
name2,
reloadCurrentChat,
@@ -40,6 +39,15 @@ import { saveMetadataDebounced } from './extensions.js';
/** @typedef {'chat' | 'character' | 'default'} PersonaLockType Type of the persona lock */
+/**
+ * @typedef {object} PersonaState
+ * @property {string} avatarId - The avatar id of the persona
+ * @property {boolean} default - Whether this persona is the default one for all new chats
+ * @property {object} locked - An object containing the lock states
+ * @property {boolean} locked.chat - Whether the persona is locked to the currently open chat
+ * @property {boolean} locked.character - Whether the persona is locked to the currently open character or group
+ */
+
const USER_AVATAR_PATH = 'User Avatars/';
let savePersonasPage = 0;
@@ -66,7 +74,7 @@ export function getUserAvatar(avatarImg) {
export function initUserAvatar(avatar) {
user_avatar = avatar;
reloadUserAvatar();
- highlightSelectedAvatar();
+ updatePersonaUIStates();
}
/**
@@ -74,19 +82,14 @@ export function initUserAvatar(avatar) {
* @param {string} imgfile Link to an image file
*/
export function setUserAvatar(imgfile, { toastPersonaNameChange = true } = {}) {
- user_avatar = imgfile && typeof imgfile === 'string' ? imgfile : $(this).attr('imgfile');
+ user_avatar = imgfile && typeof imgfile === 'string' ? imgfile : $(this).attr('data-avatar-id');
reloadUserAvatar();
- highlightSelectedAvatar();
+ updatePersonaUIStates();
selectCurrentPersona({ toastPersonaNameChange: toastPersonaNameChange });
saveSettingsDebounced();
$('.zoomed_avatar[forchar]').remove();
}
-function highlightSelectedAvatar() {
- $('#user_avatar_block .avatar-container').removeClass('selected');
- $(`#user_avatar_block .avatar-container[imgfile="${user_avatar}"]`).addClass('selected');
-}
-
function reloadUserAvatar(force = false) {
$('.mes').each(function () {
const avatarImg = $(this).find('.avatar img');
@@ -146,24 +149,32 @@ function verifyPersonaSearchSortRule() {
/**
* Gets a rendered avatar block.
- * @param {string} name Avatar file name
+ * @param {string} avatarId Avatar file name
* @returns {JQuery} Avatar block
*/
-function getUserAvatarBlock(name) {
+function getUserAvatarBlock(avatarId) {
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
const template = $('#user_avatar_template .avatar-container').clone();
- const personaName = power_user.personas[name];
- const personaDescription = power_user.persona_descriptions[name]?.description;
+ const personaName = power_user.personas[avatarId];
+ const personaDescription = power_user.persona_descriptions[avatarId]?.description;
+
template.find('.ch_name').text(personaName || '[Unnamed Persona]');
template.find('.ch_description').text(personaDescription || $('#user_avatar_block').attr('no_desc_text')).toggleClass('text_muted', !personaDescription);
- template.attr('imgfile', name);
- template.find('.avatar').attr('imgfile', name).attr('title', name);
- template.toggleClass('default_persona', name === power_user.default_persona);
- let avatarUrl = getUserAvatar(name);
+ template.attr('data-avatar-id', avatarId);
+ template.find('.avatar').attr('data-avatar-id', avatarId).attr('title', avatarId);
+ template.toggleClass('default_persona', avatarId === power_user.default_persona);
+ let avatarUrl = getUserAvatar(avatarId);
if (isFirefox) {
avatarUrl += '?t=' + Date.now();
}
template.find('img').attr('src', avatarUrl);
+
+ // Make sure description block has at least three rows. Otherwise height looks inconsistent. I don't have a better idea for this.
+ const currentText = template.find('.ch_description').text();
+ if (currentText.split('\n').length < 3) {
+ template.find('.ch_description').text(currentText + '\n\xa0\n\xa0');
+ }
+
$('#user_avatar_block').append(template);
return template;
}
@@ -218,7 +229,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
for (const item of data) {
$(listId).append(getUserAvatarBlock(item));
}
- highlightSelectedAvatar();
+ updatePersonaUIStates();
},
afterSizeSelectorChange: function (e) {
localStorage.setItem(storageKey, e.target.value);
@@ -486,7 +497,7 @@ export function setPersonaDescription() {
$('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook);
countPersonaDescriptionTokens();
- updatePersonaLockIcons();
+ updatePersonaUIStates();
updatePersonaConnectionsAvatarList();
}
@@ -780,7 +791,7 @@ function selectCurrentPersona({ toastPersonaNameChange = true } = {}) {
toastr.success(`Persona ${personaName} selected and auto-locked to current chat`, t`Persona Selected`);
}
saveMetadataDebounced();
- updatePersonaLockIcons();
+ updatePersonaUIStates();
}
// As the last step, inform user if the persona is only temporarily chosen
@@ -806,8 +817,8 @@ function selectCurrentPersona({ toastPersonaNameChange = true } = {}) {
* @returns {boolean} Whether the connection is locked
*/
export function isPersonaConnectionLocked(connection) {
- return (menu_type === 'character_edit' && connection.type === 'character' && connection.id === characters[this_chid]?.avatar)
- || (menu_type === 'group_edit' && connection.type === 'group' && connection.id === selected_group);
+ return (!selected_group && connection.type === 'character' && connection.id === characters[this_chid]?.avatar)
+ || (selected_group && connection.type === 'group' && connection.id === selected_group);
}
/**
@@ -894,7 +905,7 @@ async function unlockPersona(type = 'chat') {
throw new Error(`Unknown persona lock type: ${type}`);
}
- updatePersonaLockIcons();
+ updatePersonaUIStates();
}
/**
@@ -969,7 +980,7 @@ async function lockPersona(type = 'chat') {
throw new Error(`Unknown persona lock type: ${type}`);
}
- updatePersonaLockIcons();
+ updatePersonaUIStates();
}
@@ -1149,7 +1160,7 @@ function getOrCreatePersonaDescriptor() {
async function toggleDefaultPersonaClicked(e) {
e?.stopPropagation();
- const avatarId = $(e.currentTarget).closest('.avatar-container').find('.avatar').attr('imgfile');
+ const avatarId = $(e.currentTarget).closest('.avatar-container').find('.avatar').attr('data-avatar-id');
if (avatarId) {
await toggleDefaultPersona(avatarId);
} else {
@@ -1213,26 +1224,62 @@ async function toggleDefaultPersona(avatarId, { quiet: quiet = false } = {}) {
saveSettingsDebounced();
await getUserAvatars(true, avatarId);
- updatePersonaLockIcons();
+ updatePersonaUIStates();
}
-function updatePersonaLockIcons() {
- const isDefaultPersona = power_user.default_persona === user_avatar;
- $('#lock_persona_default').toggleClass('locked', isDefaultPersona);
-
- const hasChatLock = chat_metadata['persona'] == user_avatar;
- $('#lock_user_name').toggleClass('locked', hasChatLock);
- $('#lock_user_name i.icon').toggleClass('fa-lock', hasChatLock);
- $('#lock_user_name i.icon').toggleClass('fa-unlock', !hasChatLock);
+/**
+ * Returns an object with 3 properties that describe the state of the given persona
+ *
+ * - default: Whether this persona is the default one for all new chats
+ * - locked: An object containing the lock states
+ * - chat: Whether the persona is locked to the currently open chat
+ * - character: Whether the persona is locked to the currently open character or group
+ * @param {string} avatarId - The avatar id of the persona to get the state for
+ * @returns {PersonaState} An object describing the state of the given persona
+ */
+function getPersonaStates(avatarId) {
+ const isDefaultPersona = power_user.default_persona === avatarId;
+ const hasChatLock = chat_metadata['persona'] == avatarId;
/** @type {PersonaConnection[]} */
- const connections = power_user.persona_descriptions[user_avatar]?.connections;
+ const connections = power_user.persona_descriptions[avatarId]?.connections;
const hasCharLock = !!connections?.some(c =>
- (menu_type === 'character_edit' && c.type === 'character' && c.id === characters[this_chid]?.avatar)
- || (menu_type === 'group_edit' && c.type === 'group' && c.id === selected_group));
- $('#lock_persona_to_char').toggleClass('locked', hasCharLock);
- $('#lock_persona_to_char i.icon').toggleClass('fa-lock', hasCharLock);
- $('#lock_persona_to_char i.icon').toggleClass('fa-unlock', !hasCharLock);
+ (!selected_group && c.type === 'character' && c.id === characters[this_chid]?.avatar)
+ || (selected_group && c.type === 'group' && c.id === selected_group));
+
+ return {
+ avatarId: avatarId,
+ default: isDefaultPersona,
+ locked: {
+ chat: hasChatLock,
+ character: hasCharLock,
+ },
+ };
+}
+
+function updatePersonaUIStates() {
+ // Update the persona list
+ $('#user_avatar_block .avatar-container').each(function () {
+ const avatarId = $(this).attr('data-avatar-id');
+ const states = getPersonaStates(avatarId);
+ $(this).toggleClass('default_persona', states.default);
+ $(this).toggleClass('locked_to_chat', states.locked.chat);
+ $(this).toggleClass('locked_to_character', states.locked.character);
+ $(this).toggleClass('selected', avatarId === user_avatar);
+ });
+
+ // Buttons for the persona panel on the right
+ const personaStates = getPersonaStates(user_avatar);
+
+ $('#lock_persona_default').toggleClass('locked', personaStates.default);
+
+ $('#lock_user_name').toggleClass('locked', personaStates.locked.chat);
+ $('#lock_user_name i.icon').toggleClass('fa-lock', personaStates.locked.chat);
+ $('#lock_user_name i.icon').toggleClass('fa-unlock', !personaStates.locked.chat);
+
+ $('#lock_persona_to_char').toggleClass('locked', personaStates.locked.character);
+ $('#lock_persona_to_char i.icon').toggleClass('fa-lock', personaStates.locked.character);
+ $('#lock_persona_to_char i.icon').toggleClass('fa-unlock', !personaStates.locked.character);
}
async function loadPersonaForCurrentChat({ doRender = false } = {}) {
@@ -1315,7 +1362,7 @@ async function loadPersonaForCurrentChat({ doRender = false } = {}) {
}
}
- updatePersonaLockIcons();
+ updatePersonaUIStates();
}
/**
@@ -1325,7 +1372,7 @@ async function loadPersonaForCurrentChat({ doRender = false } = {}) {
* @returns {string[]} - An array of persona keys that are connected to the given character key
*/
export function getConnectedPersonas(characterKey = undefined) {
- characterKey ??= menu_type === 'group_edit' ? selected_group : characters[this_chid]?.avatar;
+ characterKey ??= selected_group || characters[this_chid]?.avatar;
const connectedPersonas = Object.entries(power_user.persona_descriptions)
.filter(([_, desc]) => desc.connections?.some(conn => conn.type === 'character' && conn.id === characterKey))
.map(([key, _]) => key);
@@ -1339,9 +1386,9 @@ export function getConnectedPersonas(characterKey = undefined) {
*/
export function getCurrentConnectionObj() {
- if (menu_type === 'group_edit')
+ if (selected_group)
return { type: 'group', id: selected_group };
- if (menu_type == 'character_edit')
+ if (characters[this_chid]?.avatar)
return { type: 'character', id: characters[this_chid]?.avatar };
return null;
}
@@ -1538,7 +1585,7 @@ export function initPersonas() {
$('#avatar_upload_file').on('change', changeUserAvatar);
$(document).on('click', '#user_avatar_block .avatar-container', function () {
- const imgfile = $(this).attr('imgfile');
+ const imgfile = $(this).attr('data-avatar-id');
setUserAvatar(imgfile);
// force firstMes {{user}} update on persona switch
diff --git a/public/style.css b/public/style.css
index bba18ed6a..cce15e915 100644
--- a/public/style.css
+++ b/public/style.css
@@ -222,6 +222,37 @@ table.responsiveTable {
animation-name: flash;
}
+.has_hover_label .label_icon {
+ transition: opacity var(--animation-duration) ease, max-width var(--animation-duration) ease;
+}
+.has_hover_label .label {
+ transition: opacity var(--animation-duration-slow) ease, max-width var(--animation-duration-slow) ease;
+ /* Prevent double gap on hidden icon */
+ margin-left: -5px;
+}
+
+.has_hover_label .label_icon,
+.has_hover_label .label {
+ transition-delay: var(--animation-duration-slow);
+}
+.has_hover_label.fast .label_icon,
+.has_hover_label.fast .label {
+ transition-delay: var(--animation-duration);
+}
+
+.has_hover_label .label_icon,
+.has_hover_label:hover .label {
+ opacity: 1;
+ max-width: 100px;
+}
+
+.has_hover_label:hover .label_icon,
+.has_hover_label .label {
+ opacity: 0;
+ max-width: 0;
+ overflow: hidden;
+}
+
/* Keyboard/focus navigation styling */
/* Mimic the outline of keyboard navigation for most most focusable controls */
.interactable {
@@ -2805,6 +2836,7 @@ select option:not(:checked) {
.menu_button.disabled {
filter: brightness(75%) grayscale(1);
opacity: 0.5;
+ cursor: inherit;
pointer-events: none;
}
@@ -2880,8 +2912,7 @@ select option:not(:checked) {
}
#form_character_search_form .menu_button,
-#GroupFavDelOkBack .menu_button,
-.avatar-container .menu_button {
+#GroupFavDelOkBack .menu_button {
margin: 0;
height: fit-content;
padding: 5px;
@@ -2915,8 +2946,9 @@ select option:not(:checked) {
width: max-content;
}
-#persona-management-block .menu_button {
- filter: grayscale(0.5);
+#persona-management-block .avatar_container_states .menu_button {
+ padding: 3px 5px;
+ pointer-events: initial;
}
#persona_controls .persona_name {
@@ -3321,28 +3353,6 @@ grammarly-extension {
z-index: 35;
}
-.avatar-container .avatar-buttons {
- display: flex;
- flex-direction: row;
- gap: 3px;
- opacity: 0.3;
- transition: opacity 0.25s ease-in-out;
-}
-
-.avatar-container .avatar-buttons:hover {
- opacity: 1;
-}
-
-.avatar-container .avatar-buttons .menu_button {
- padding: 3px;
-}
-
-/* Ross should be able to handle this later */
-/*.big-avatars .avatar-buttons{
- justify-content: center;
- width: fit-content;
-}*/
-
.avatar_div .avatar {
/* margin-left: 4px;
margin-right: 10px;
@@ -3603,6 +3613,7 @@ grammarly-extension {
.menu_button {
color: var(--SmartThemeBodyColor);
+ filter: grayscale(0.5);
background-color: var(--SmartThemeBlurTintColor);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 5px;
@@ -3622,8 +3633,8 @@ grammarly-extension {
min-width: calc(1.25em + 12px);
}
-.menu_button:hover,
-.menu_button.active {
+.menu_button:not(.disabled):hover,
+.menu_button:not(.disabled).active {
background-color: var(--white30a);
}
@@ -3876,11 +3887,28 @@ input[type='checkbox'].del_checkbox {
color: var(--golden);
}
+.avatar-container .avatar_state .fa-lock {
+ color: var(--active);
+}
+
+.avatar-container:not(.locked_to_chat) .locked_to_chat_label {
+ display: none;
+}
+.avatar-container:not(.locked_to_character) .locked_to_character_label {
+ display: none;
+}
+
#lock_persona_default.locked i.icon {
color: var(--golden);
}
-#lock_user_name.locked i.icon,
-#lock_persona_to_char.locked i.icon {
+
+#lock_user_name.locked .icon,
+.avatar-container.locked_to_chat .locked_to_chat_label .icon {
+ color: var(--SmartThemeQuoteColor);
+}
+
+#lock_persona_to_char.locked .icon,
+.avatar-container.locked_to_character .locked_to_character_label .icon {
color: var(--active);
}