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

08a4cee48fe7d6e6b4645a0391a5d8bc18a1ff89

Wolfsblvt <wolfsblvt@gmail.com>

2 files changed, +111 -16Showing whitespace changes
public/script.js+7 -1
@@ -6383,7 +6383,7 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t
6383 }6383 }
63846384
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 }
64036409
6404 if (interactable) {6410 if (interactable) {
6405 avatarTemplate.addClass(INTERACTABLE_CONTROL_CLASS);6411 avatarTemplate.addClass(INTERACTABLE_CONTROL_CLASS);
public/scripts/personas.js+104 -15
@@ -485,6 +485,32 @@ export function setPersonaDescription() {
485 updatePersonaConnectionsAvatarList();485 updatePersonaConnectionsAvatarList();
486}486}
487487
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 */
499export 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}
512538
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 */
548export 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
513export function autoSelectPersona(name) {575export 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}
10691131
1070async function setChatLockedPersona() {1132async function setChatLockedPersona() {
1133 // Cache persona list to check if they exist
1134 const userAvatars = await getUserAvatars(false);
1135
1071 // Define a persona for this chat1136 // Define a persona for this chat
1072 let chatPersona = '';1137 let chatPersona = '';
10731138
1074 if (chat_metadata['persona']) {
1075 // If persona is locked in chat metadata, select it1139 // If persona is locked in chat metadata, select it
1140 if (chat_metadata['persona']) {
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 it1144 // 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 }
10831153
1084 // No persona set: user current settings1154 // 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 }
10891175
1090 // Find the avatar file1176 // 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 }
10921181
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 }
11001187
1101 // Default persona missing1188 // 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 }
11081194
1109 // Persona avatar found, select it1195 // Persona avatar found, select it
1196 if (chatPersona) {
1110 setUserAvatar(chatPersona);1197 setUserAvatar(chatPersona);
1198 }
1199
1111 updatePersonaLockIcons();1200 updatePersonaLockIcons();
1112}1201}
11131202