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
63836383 }
63846384
63856385 avatarTemplate.attr('data-type', entity.type);
63866386 avatarTemplate.attr({ 'data-chid': id, 'id': `CharID${id}` });
63876387 avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entity.item.name);
63886388 avatarTemplate.attr('title', `[Character] ${entity.item.name}\nFile: ${entity.item.avatar}`);
63896389 if (highlightFavs) {
@@ -6398,8 +6398,14 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t
63986398 avatarTemplate.addClass(grpTemplate.attr('class'));
63996399 avatarTemplate.empty();
64006400 avatarTemplate.append(grpTemplate.children());
6401+ avatarTemplate.attr({ 'data-grid': id, 'data-chid': null });
64016402 avatarTemplate.attr('title', `[Group] ${entity.item.name}`);
64026403 }
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
64046410 if (interactable) {
64056411 avatarTemplate.addClass(INTERACTABLE_CONTROL_CLASS);
public/scripts/personas.js+104 -15
@@ -485,6 +485,32 @@ export function setPersonaDescription() {
485485 updatePersonaConnectionsAvatarList();
486486}
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+ */
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+
488514/**
489515 * Displays avatar connections for the current persona.
490516 * Converts connections to entities and populates the avatar list. Shows a message if no connections are found.
@@ -510,6 +536,42 @@ export function updatePersonaConnectionsAvatarList() {
510536 $('#persona_connections_list').text('[No connections]');
511537}
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+ */
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+
513575export function autoSelectPersona(name) {
514576 for (const [key, value] of Object.entries(power_user.personas)) {
515577 if (value === name) {
@@ -1068,34 +1130,59 @@ function updatePersonaLockIcons() {
10681130}
10691131
10701132async function setChatLockedPersona() {
1133+ // Cache persona list to check if they exist
1134+ const userAvatars = await getUserAvatars(false);
1135+
10711136 // Define a persona for this chat
10721137 let chatPersona = '';
10731138
1074- if (chat_metadata['persona']) {
10751139 // If persona is locked in chat metadata, select it
1140+ if (chat_metadata['persona']) {
10761141 console.log(`Using locked persona ${chat_metadata['persona']}`);
10771142 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+ }
10821152 }
10831153
10841154 // 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
10851158 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+ }
10881174 }
10891175
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+ }
10921181
1093- // Avatar missing (persona deleted)
1182+ // Whatever way we selected a persona, if it doesn't exist, unlock this chat
10941183 if (chat_metadata['persona'] && !userAvatars.includes(chatPersonachat_metadata['persona'])) {
10951184 console.warn('Persona avatar not found, unlocking persona');
10961185 delete chat_metadata['persona'];
1097- updatePersonaLockIcons();
1098- return;
10991186 }
11001187
11011188 // Default persona missing
@@ -1103,11 +1190,13 @@ async function setChatLockedPersona() {
11031190 console.warn('Default persona avatar not found, clearing default persona');
11041191 power_user.default_persona = null;
11051192 saveSettingsDebounced();
1106- return;
11071193 }
11081194
11091195 // Persona avatar found, select it
1196+ if (chatPersona) {
11101197 setUserAvatar(chatPersona);
1198+ }
1199+
11111200 updatePersonaLockIcons();
11121201}
11131202