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 | avatarTemplate.attr('data-type', entity.type); | 6385 | avatarTemplate.attr('data-type', entity.type); |
| 6386 | avatarTemplate.attr({ 'chid': id, 'id': `CharID${id}` }); | 6386 | avatarTemplate.attr('data-chid', id); |
| 6387 | avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entity.item.name); | 6387 | avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entity.item.name); |
| 6388 | avatarTemplate.attr('title', `[Character] ${entity.item.name}\nFile: ${entity.item.avatar}`); | 6388 | avatarTemplate.attr('title', `[Character] ${entity.item.name}\nFile: ${entity.item.avatar}`); |
| 6389 | if (highlightFavs) { | 6389 | if (highlightFavs) { |
| @@ -6398,8 +6398,14 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t | |||
| 6398 | avatarTemplate.addClass(grpTemplate.attr('class')); | 6398 | avatarTemplate.addClass(grpTemplate.attr('class')); |
| 6399 | avatarTemplate.empty(); | 6399 | avatarTemplate.empty(); |
| 6400 | avatarTemplate.append(grpTemplate.children()); | 6400 | avatarTemplate.append(grpTemplate.children()); |
| 6401 | avatarTemplate.attr({ 'data-grid': id, 'data-chid': null }); | ||
| 6401 | avatarTemplate.attr('title', `[Group] ${entity.item.name}`); | 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 | if (interactable) { | 6410 | if (interactable) { |
| 6405 | avatarTemplate.addClass(INTERACTABLE_CONTROL_CLASS); | 6411 | avatarTemplate.addClass(INTERACTABLE_CONTROL_CLASS); |
| @@ -485,6 +485,32 @@ export function setPersonaDescription() { | |||
| 485 | updatePersonaConnectionsAvatarList(); | 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 | * Displays avatar connections for the current persona. | 515 | * Displays avatar connections for the current persona. |
| 490 | * Converts connections to entities and populates the avatar list. Shows a message if no connections are found. | 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 | $('#persona_connections_list').text('[No connections]'); | 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 | export function autoSelectPersona(name) { | 575 | export function autoSelectPersona(name) { |
| 514 | for (const [key, value] of Object.entries(power_user.personas)) { | 576 | for (const [key, value] of Object.entries(power_user.personas)) { |
| 515 | if (value === name) { | 577 | if (value === name) { |
| @@ -1068,34 +1130,59 @@ function updatePersonaLockIcons() { | |||
| 1068 | } | 1130 | } |
| 1069 | 1131 | ||
| 1070 | async function setChatLockedPersona() { | 1132 | async function setChatLockedPersona() { |
| 1133 | // Cache persona list to check if they exist | ||
| 1134 | const userAvatars = await getUserAvatars(false); | ||
| 1135 | |||
| 1071 | // Define a persona for this chat | 1136 | // Define a persona for this chat |
| 1072 | let chatPersona = ''; | 1137 | let chatPersona = ''; |
| 1073 | 1138 | ||
| 1139 | // If persona is locked in chat metadata, select it | ||
| 1074 | if (chat_metadata['persona']) { | 1140 | if (chat_metadata['persona']) { |
| 1075 | // If persona is locked in chat metadata, select it | ||
| 1076 | console.log(`Using locked persona ${chat_metadata['persona']}`); | 1141 | console.log(`Using locked persona ${chat_metadata['persona']}`); |
| 1077 | chatPersona = chat_metadata['persona']; | 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 | // No persona set: user current settings | 1154 | // TODO: TEMP |
| 1155 | power_user.persona_allow_multi_connections = true; | ||
| 1156 | |||
| 1157 | // Check if we have any persona connected to the current character | ||
| 1085 | if (!chatPersona) { | 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 | if (chat_metadata['persona'] && !userAvatars.includes(chatPersona)) { | 1183 | if (chat_metadata['persona'] && !userAvatars.includes(chat_metadata['persona'])) { |
| 1095 | console.warn('Persona avatar not found, unlocking persona'); | 1184 | console.warn('Persona avatar not found, unlocking persona'); |
| 1096 | delete chat_metadata['persona']; | 1185 | delete chat_metadata['persona']; |
| 1097 | updatePersonaLockIcons(); | ||
| 1098 | return; | ||
| 1099 | } | 1186 | } |
| 1100 | 1187 | ||
| 1101 | // Default persona missing | 1188 | // Default persona missing |
| @@ -1103,11 +1190,13 @@ async function setChatLockedPersona() { | |||
| 1103 | console.warn('Default persona avatar not found, clearing default persona'); | 1190 | console.warn('Default persona avatar not found, clearing default persona'); |
| 1104 | power_user.default_persona = null; | 1191 | power_user.default_persona = null; |
| 1105 | saveSettingsDebounced(); | 1192 | saveSettingsDebounced(); |
| 1106 | return; | ||
| 1107 | } | 1193 | } |
| 1108 | 1194 | ||
| 1109 | // Persona avatar found, select it | 1195 | // Persona avatar found, select it |
| 1110 | setUserAvatar(chatPersona); | 1196 | if (chatPersona) { |
| 1197 | setUserAvatar(chatPersona); | ||
| 1198 | } | ||
| 1199 | |||
| 1111 | updatePersonaLockIcons(); | 1200 | updatePersonaLockIcons(); |
| 1112 | } | 1201 | } |
| 1113 | 1202 | ||