Add/expand persona auto selection from char / chat - Persona selection on chat based on three levels: 1. Chat Locking 2. Char connected personas 3. Default persona - Add popup if multiple personas are connected to char - Add utility function to print persona avatar lists
| @@ -6383,7 +6383,7 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t | ||
| 6383 | 6383 | } |
| 6384 | 6384 | |
| 6385 | 6385 | avatarTemplate.attr('data-type', entity.type); |
| 6386 | 6386 | avatarTemplate.attr({ 'data-chid': id, 'id': `CharID${id}` }); |
| 6387 | 6387 | avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entity.item.name); |
| 6388 | 6388 | avatarTemplate.attr('title', `[Character] ${entity.item.name}\nFile: ${entity.item.avatar}`); |
| 6389 | 6389 | if (highlightFavs) { |
| @@ -6398,8 +6398,14 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t | ||
| 6398 | 6398 | avatarTemplate.addClass(grpTemplate.attr('class')); |
| 6399 | 6399 | avatarTemplate.empty(); |
| 6400 | 6400 | avatarTemplate.append(grpTemplate.children()); |
| 6401 | + avatarTemplate.attr({ 'data-grid': id, 'data-chid': null }); | |
| 6401 | 6402 | avatarTemplate.attr('title', `[Group] ${entity.item.name}`); |
| 6402 | 6403 | } |
| 6404 | + else if (entity.type === 'persona') { | |
| 6405 | + avatarTemplate.attr({ 'data-pid': id, 'data-chid': null }); | |
| 6406 | + avatarTemplate.find('img').attr('src', getUserAvatar(entity.item.avatar)); | |
| 6407 | + avatarTemplate.attr('title', `[Persona] ${entity.item.name}\nFile: ${entity.item.avatar}`); | |
| 6408 | + } | |
| 6403 | 6409 | |
| 6404 | 6410 | if (interactable) { |
| 6405 | 6411 | avatarTemplate.addClass(INTERACTABLE_CONTROL_CLASS); |
| @@ -485,6 +485,32 @@ export function setPersonaDescription() { | ||
| 485 | 485 | updatePersonaConnectionsAvatarList(); |
| 486 | 486 | } |
| 487 | 487 | |
| 488 | + | |
| 489 | +/** | |
| 490 | + * Builds a list of persona avatars and populates the given block element with them. | |
| 491 | + * | |
| 492 | + * @param {HTMLElement} block - The HTML element where the avatar list will be rendered | |
| 493 | + * @param {string[]} personas - An array of persona identifiers | |
| 494 | + * @param {Object} [options] - Optional settings for building the avatar list | |
| 495 | + * @param {boolean} [options.empty=true] - Whether to clear the block element before adding avatars | |
| 496 | + * @param {boolean} [options.interactable=false] - Whether the avatars should be interactable | |
| 497 | + * @param {boolean} [options.highlightFavs=true] - Whether to highlight favorite avatars | |
| 498 | + */ | |
| 499 | +export function buildPersonaAvatarList(block, personas, { empty = true, interactable = false, highlightFavs = true } = {}) { | |
| 500 | + const personaEntities = personas.map(avatar => ({ | |
| 501 | + type: 'persona', | |
| 502 | + id: avatar, | |
| 503 | + item: { | |
| 504 | + name: power_user.personas[avatar], | |
| 505 | + description: power_user.persona_descriptions[avatar]?.description || '', | |
| 506 | + avatar: avatar, | |
| 507 | + fav: power_user.default_persona === avatar, | |
| 508 | + }, | |
| 509 | + })); | |
| 510 | + | |
| 511 | + buildAvatarList($(block), personaEntities, { empty: empty, interactable: interactable, highlightFavs: highlightFavs }); | |
| 512 | +} | |
| 513 | + | |
| 488 | 514 | /** |
| 489 | 515 | * Displays avatar connections for the current persona. |
| 490 | 516 | * Converts connections to entities and populates the avatar list. Shows a message if no connections are found. |
| @@ -510,6 +536,42 @@ export function updatePersonaConnectionsAvatarList() { | ||
| 510 | 536 | $('#persona_connections_list').text('[No connections]'); |
| 511 | 537 | } |
| 512 | 538 | |
| 539 | + | |
| 540 | +/** | |
| 541 | + * Displays a popup for persona selection and returns the selected persona. | |
| 542 | + * | |
| 543 | + * @param {string} title - The title to display in the popup | |
| 544 | + * @param {string} text - The text to display in the popup | |
| 545 | + * @param {string[]} personas - An array of persona ids to display for selection | |
| 546 | + * @returns {Promise<string?>} - A promise that resolves to the selected persona id or null if no selection was made | |
| 547 | + */ | |
| 548 | +export async function askForPersonaSelection(title, text, personas) { | |
| 549 | + const content = document.createElement('div'); | |
| 550 | + const titleElement = document.createElement('h3'); | |
| 551 | + titleElement.textContent = title; | |
| 552 | + content.appendChild(titleElement); | |
| 553 | + | |
| 554 | + const textElement = document.createElement('div'); | |
| 555 | + textElement.textContent = text; | |
| 556 | + content.appendChild(textElement); | |
| 557 | + | |
| 558 | + const personaListBlock = document.createElement('div'); | |
| 559 | + personaListBlock.classList.add('persona-list', 'avatars_inline', 'flex-container'); | |
| 560 | + content.appendChild(personaListBlock); | |
| 561 | + | |
| 562 | + buildPersonaAvatarList(personaListBlock, personas, { interactable: true }); | |
| 563 | + | |
| 564 | + // Make the persona blocks clickable and close the popup | |
| 565 | + personaListBlock.querySelectorAll('.avatar[data-type="persona"]').forEach(block => { | |
| 566 | + if (!(block instanceof HTMLElement)) return; | |
| 567 | + block.dataset.result = String(100 + personas.indexOf(block.dataset.pid)); | |
| 568 | + }); | |
| 569 | + | |
| 570 | + const popup = new Popup(content, POPUP_TYPE.TEXT, '', {}); | |
| 571 | + const result = await popup.show(); | |
| 572 | + return Number(result) > 100 ? personas[Number(result) - 100] : null; | |
| 573 | +} | |
| 574 | + | |
| 513 | 575 | export function autoSelectPersona(name) { |
| 514 | 576 | for (const [key, value] of Object.entries(power_user.personas)) { |
| 515 | 577 | if (value === name) { |
| @@ -1068,34 +1130,59 @@ function updatePersonaLockIcons() { | ||
| 1068 | 1130 | } |
| 1069 | 1131 | |
| 1070 | 1132 | async function setChatLockedPersona() { |
| 1133 | + // Cache persona list to check if they exist | |
| 1134 | + const userAvatars = await getUserAvatars(false); | |
| 1135 | + | |
| 1071 | 1136 | // Define a persona for this chat |
| 1072 | 1137 | let chatPersona = ''; |
| 1073 | 1138 | |
| 1074 | - if (chat_metadata['persona']) { | |
| 1075 | 1139 | // If persona is locked in chat metadata, select it |
| 1140 | + if (chat_metadata['persona']) { | |
| 1076 | 1141 | console.log(`Using locked persona ${chat_metadata['persona']}`); |
| 1077 | 1142 | chatPersona = chat_metadata['persona']; |
| 1078 | - } else if (power_user.default_persona) { | |
| 1143 | + | |
| 1079 | - // If default persona is set, select it | |
| 1144 | + // Verify it exists | |
| 1080 | - console.log(`Using default persona ${power_user.default_persona}`); | |
| 1145 | + if (!userAvatars.includes(chatPersona)) { | |
| 1081 | - chatPersona = power_user.default_persona; | |
| 1146 | + console.warn('Chat-locked persona avatar not found, unlocking persona'); | |
| 1147 | + delete chat_metadata['persona']; | |
| 1148 | + updatePersonaLockIcons(); | |
| 1149 | + chatPersona = ''; | |
| 1150 | + return; | |
| 1151 | + } | |
| 1082 | 1152 | } |
| 1083 | 1153 | |
| 1084 | 1154 | // No persona setTODO: user current settingsTEMP |
| 1155 | + power_user.persona_allow_multi_connections = true; | |
| 1156 | + | |
| 1157 | + // Check if we have any persona connected to the current character | |
| 1085 | 1158 | if (!chatPersona) { |
| 1086 | - console.debug('No default or locked persona set for this chat'); | |
| 1159 | + const characterKey = menu_type === 'group_edit' ? selected_group : characters[this_chid]?.avatar; | |
| 1087 | - return; | |
| 1160 | + const connectedPersonas = Object.entries(power_user.persona_descriptions) | |
| 1161 | + .filter(([_, desc]) => desc.connections?.some(conn => conn.type === 'character' && conn.id === characterKey)) | |
| 1162 | + .map(([key, _]) => key); | |
| 1163 | + | |
| 1164 | + if (connectedPersonas.length > 0) { | |
| 1165 | + if (!power_user.persona_allow_multi_connections || connectedPersonas.length === 1) { | |
| 1166 | + chatPersona = connectedPersonas[0]; | |
| 1167 | + toastr.warning(t`More than one persona is connected to this character. Using the first available persona for this chat.`, t`Automatic persona selection`); | |
| 1168 | + } else { | |
| 1169 | + chatPersona = await askForPersonaSelection(t`Select Persona`, | |
| 1170 | + t`Select one of multiple with this character connected persona to use for this chat`, | |
| 1171 | + connectedPersonas); | |
| 1172 | + } | |
| 1173 | + } | |
| 1088 | 1174 | } |
| 1089 | 1175 | |
| 1090 | - // Find the avatar file | |
| 1176 | + // Last check if default persona is set, select it | |
| 1091 | - const userAvatars = await getUserAvatars(false); | |
| 1177 | + if (!chatPersona && power_user.default_persona) { | |
| 1178 | + console.log(`Using default persona ${power_user.default_persona}`); | |
| 1179 | + chatPersona = power_user.default_persona; | |
| 1180 | + } | |
| 1092 | 1181 | |
| 1093 | - // Avatar missing (persona deleted) | |
| 1182 | + // Whatever way we selected a persona, if it doesn't exist, unlock this chat | |
| 1094 | 1183 | if (chat_metadata['persona'] && !userAvatars.includes(chatPersonachat_metadata['persona'])) { |
| 1095 | 1184 | console.warn('Persona avatar not found, unlocking persona'); |
| 1096 | 1185 | delete chat_metadata['persona']; |
| 1097 | - updatePersonaLockIcons(); | |
| 1098 | - return; | |
| 1099 | 1186 | } |
| 1100 | 1187 | |
| 1101 | 1188 | // Default persona missing |
| @@ -1103,11 +1190,13 @@ async function setChatLockedPersona() { | ||
| 1103 | 1190 | console.warn('Default persona avatar not found, clearing default persona'); |
| 1104 | 1191 | power_user.default_persona = null; |
| 1105 | 1192 | saveSettingsDebounced(); |
| 1106 | - return; | |
| 1107 | 1193 | } |
| 1108 | 1194 | |
| 1109 | 1195 | // Persona avatar found, select it |
| 1196 | + if (chatPersona) { | |
| 1110 | 1197 | setUserAvatar(chatPersona); |
| 1198 | + } | |
| 1199 | + | |
| 1111 | 1200 | updatePersonaLockIcons(); |
| 1112 | 1201 | } |
| 1113 | 1202 | |