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); }