diff --git a/public/script.js b/public/script.js index f1b9bbed7..fbf712317 100644 --- a/public/script.js +++ b/public/script.js @@ -6383,7 +6383,7 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t } avatarTemplate.attr('data-type', entity.type); - avatarTemplate.attr({ 'chid': id, 'id': `CharID${id}` }); + avatarTemplate.attr('data-chid', id); avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entity.item.name); avatarTemplate.attr('title', `[Character] ${entity.item.name}\nFile: ${entity.item.avatar}`); if (highlightFavs) { @@ -6398,8 +6398,14 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t avatarTemplate.addClass(grpTemplate.attr('class')); avatarTemplate.empty(); avatarTemplate.append(grpTemplate.children()); + avatarTemplate.attr({ 'data-grid': id, 'data-chid': null }); avatarTemplate.attr('title', `[Group] ${entity.item.name}`); } + else if (entity.type === 'persona') { + avatarTemplate.attr({ 'data-pid': id, 'data-chid': null }); + avatarTemplate.find('img').attr('src', getUserAvatar(entity.item.avatar)); + avatarTemplate.attr('title', `[Persona] ${entity.item.name}\nFile: ${entity.item.avatar}`); + } if (interactable) { avatarTemplate.addClass(INTERACTABLE_CONTROL_CLASS); diff --git a/public/scripts/personas.js b/public/scripts/personas.js index f2c592b3d..90e308560 100644 --- a/public/scripts/personas.js +++ b/public/scripts/personas.js @@ -485,6 +485,32 @@ export function setPersonaDescription() { updatePersonaConnectionsAvatarList(); } + +/** + * Builds a list of persona avatars and populates the given block element with them. + * + * @param {HTMLElement} block - The HTML element where the avatar list will be rendered + * @param {string[]} personas - An array of persona identifiers + * @param {Object} [options] - Optional settings for building the avatar list + * @param {boolean} [options.empty=true] - Whether to clear the block element before adding avatars + * @param {boolean} [options.interactable=false] - Whether the avatars should be interactable + * @param {boolean} [options.highlightFavs=true] - Whether to highlight favorite avatars + */ +export function buildPersonaAvatarList(block, personas, { empty = true, interactable = false, highlightFavs = true } = {}) { + const personaEntities = personas.map(avatar => ({ + type: 'persona', + id: avatar, + item: { + name: power_user.personas[avatar], + description: power_user.persona_descriptions[avatar]?.description || '', + avatar: avatar, + fav: power_user.default_persona === avatar, + }, + })); + + buildAvatarList($(block), personaEntities, { empty: empty, interactable: interactable, highlightFavs: highlightFavs }); +} + /** * Displays avatar connections for the current persona. * Converts connections to entities and populates the avatar list. Shows a message if no connections are found. @@ -510,6 +536,42 @@ export function updatePersonaConnectionsAvatarList() { $('#persona_connections_list').text('[No connections]'); } + +/** + * Displays a popup for persona selection and returns the selected persona. + * + * @param {string} title - The title to display in the popup + * @param {string} text - The text to display in the popup + * @param {string[]} personas - An array of persona ids to display for selection + * @returns {Promise} - A promise that resolves to the selected persona id or null if no selection was made + */ +export async function askForPersonaSelection(title, text, personas) { + const content = document.createElement('div'); + const titleElement = document.createElement('h3'); + titleElement.textContent = title; + content.appendChild(titleElement); + + const textElement = document.createElement('div'); + textElement.textContent = text; + content.appendChild(textElement); + + const personaListBlock = document.createElement('div'); + personaListBlock.classList.add('persona-list', 'avatars_inline', 'flex-container'); + content.appendChild(personaListBlock); + + buildPersonaAvatarList(personaListBlock, personas, { interactable: true }); + + // Make the persona blocks clickable and close the popup + personaListBlock.querySelectorAll('.avatar[data-type="persona"]').forEach(block => { + if (!(block instanceof HTMLElement)) return; + block.dataset.result = String(100 + personas.indexOf(block.dataset.pid)); + }); + + const popup = new Popup(content, POPUP_TYPE.TEXT, '', {}); + const result = await popup.show(); + return Number(result) > 100 ? personas[Number(result) - 100] : null; +} + export function autoSelectPersona(name) { for (const [key, value] of Object.entries(power_user.personas)) { if (value === name) { @@ -1068,34 +1130,59 @@ function updatePersonaLockIcons() { } async function setChatLockedPersona() { + // Cache persona list to check if they exist + const userAvatars = await getUserAvatars(false); + // Define a persona for this chat let chatPersona = ''; + // If persona is locked in chat metadata, select it if (chat_metadata['persona']) { - // If persona is locked in chat metadata, select it console.log(`Using locked persona ${chat_metadata['persona']}`); chatPersona = chat_metadata['persona']; - } else if (power_user.default_persona) { - // If default persona is set, select it + + // Verify it exists + if (!userAvatars.includes(chatPersona)) { + console.warn('Chat-locked persona avatar not found, unlocking persona'); + delete chat_metadata['persona']; + updatePersonaLockIcons(); + chatPersona = ''; + return; + } + } + + // TODO: TEMP + power_user.persona_allow_multi_connections = true; + + // Check if we have any persona connected to the current character + if (!chatPersona) { + const characterKey = menu_type === 'group_edit' ? 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); + + if (connectedPersonas.length > 0) { + if (!power_user.persona_allow_multi_connections || connectedPersonas.length === 1) { + chatPersona = connectedPersonas[0]; + toastr.warning(t`More than one persona is connected to this character. Using the first available persona for this chat.`, t`Automatic persona selection`); + } else { + chatPersona = await askForPersonaSelection(t`Select Persona`, + t`Select one of multiple with this character connected persona to use for this chat`, + connectedPersonas); + } + } + } + + // Last check if default persona is set, select it + if (!chatPersona && power_user.default_persona) { console.log(`Using default persona ${power_user.default_persona}`); chatPersona = power_user.default_persona; } - // No persona set: user current settings - if (!chatPersona) { - console.debug('No default or locked persona set for this chat'); - return; - } - - // Find the avatar file - const userAvatars = await getUserAvatars(false); - - // Avatar missing (persona deleted) - if (chat_metadata['persona'] && !userAvatars.includes(chatPersona)) { + // Whatever way we selected a persona, if it doesn't exist, unlock this chat + if (chat_metadata['persona'] && !userAvatars.includes(chat_metadata['persona'])) { console.warn('Persona avatar not found, unlocking persona'); delete chat_metadata['persona']; - updatePersonaLockIcons(); - return; } // Default persona missing @@ -1103,11 +1190,13 @@ async function setChatLockedPersona() { console.warn('Default persona avatar not found, clearing default persona'); power_user.default_persona = null; saveSettingsDebounced(); - return; } // Persona avatar found, select it - setUserAvatar(chatPersona); + if (chatPersona) { + setUserAvatar(chatPersona); + } + updatePersonaLockIcons(); }