Add button to show connections for current char
| @@ -5091,6 +5091,7 @@ | |||
| 5091 | <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div> | 5091 | <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div> |
| 5092 | <div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore Click to load Shift-click to open 'Link to World Info' popup" data-i18n="[title]world_button_title"></div> | 5092 | <div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore Click to load Shift-click to open 'Link to World Info' popup" data-i18n="[title]world_button_title"></div> |
| 5093 | <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore Alt+Click to open the lorebook" data-i18n="[title]Chat Lore Alt+Click to open the lorebook"></div> | 5093 | <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore Alt+Click to open the lorebook" data-i18n="[title]Chat Lore Alt+Click to open the lorebook"></div> |
| 5094 | <div id="char_connections_button" class="menu_button fa-solid fa-face-smile" title="Connected Personas" data-i18n="[title]Connected Personas"></div> | ||
| 5094 | <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div> | 5095 | <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div> |
| 5095 | <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> --> | 5096 | <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> --> |
| 5096 | <!-- <div id="set_character_world" class="menu_button fa-solid fa-globe" title="Set a character World Info / Lorebook"></div> --> | 5097 | <!-- <div id="set_character_world" class="menu_button fa-solid fa-globe" title="Set a character World Info / Lorebook"></div> --> |
| @@ -234,6 +234,8 @@ import { | |||
| 234 | setPersonaDescription, | 234 | setPersonaDescription, |
| 235 | initUserAvatar, | 235 | initUserAvatar, |
| 236 | updatePersonaConnectionsAvatarList, | 236 | updatePersonaConnectionsAvatarList, |
| 237 | getConnectedPersonas, | ||
| 238 | askForPersonaSelection, | ||
| 237 | } from './scripts/personas.js'; | 239 | } from './scripts/personas.js'; |
| 238 | import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; | 240 | import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; |
| 239 | import { hideLoader, showLoader } from './scripts/loader.js'; | 241 | import { hideLoader, showLoader } from './scripts/loader.js'; |
| @@ -9985,6 +9987,47 @@ jQuery(async function () { | |||
| 9985 | } | 9987 | } |
| 9986 | }); | 9988 | }); |
| 9987 | 9989 | ||
| 9990 | $('#char_connections_button').on('click', async () => { | ||
| 9991 | let isRemoving = false; | ||
| 9992 | |||
| 9993 | const connections = getConnectedPersonas(); | ||
| 9994 | const message = t`The following personas are connected to the current character.\n\nClick on a persona to select it for the current character.\nShift + Click to unlink the persona from the character.`; | ||
| 9995 | const selectedPersona = await askForPersonaSelection(t`Persona Connections`, message, connections, { | ||
| 9996 | okButton: t`Ok`, | ||
| 9997 | shiftClickHandler: (element, ev) => { | ||
| 9998 | |||
| 9999 | const personaId = $(element).attr('data-pid'); | ||
| 10000 | |||
| 10001 | /** @type {import('./scripts/personas.js').PersonaConnection[]} */ | ||
| 10002 | const connections = power_user.persona_descriptions[personaId]?.connections; | ||
| 10003 | if (connections) { | ||
| 10004 | console.log(`Unlocking persona ${personaId} from current character ${name2}`); | ||
| 10005 | power_user.persona_descriptions[personaId].connections = connections.filter(c => { | ||
| 10006 | if (menu_type == 'group_edit' && c.type == 'group' && c.id == selected_group) return false; | ||
| 10007 | else if (c.type == 'character' && c.id == characters[this_chid]?.avatar) return false; | ||
| 10008 | return true; | ||
| 10009 | }); | ||
| 10010 | saveSettingsDebounced(); | ||
| 10011 | updatePersonaConnectionsAvatarList(); | ||
| 10012 | if (power_user.persona_show_notifications) { | ||
| 10013 | toastr.info(t`User persona ${power_user.personas[personaId]} is now unlocked from the current character ${name2}.`, t`Persona unlocked`); | ||
| 10014 | } | ||
| 10015 | |||
| 10016 | isRemoving = true; | ||
| 10017 | $('#char_connections_button').trigger('click'); | ||
| 10018 | } | ||
| 10019 | }, | ||
| 10020 | }); | ||
| 10021 | |||
| 10022 | // One of the persona was selected. So load it. | ||
| 10023 | if (!isRemoving && selectedPersona) { | ||
| 10024 | setUserAvatar(selectedPersona); | ||
| 10025 | if (power_user.persona_show_notifications) { | ||
| 10026 | toastr.info(t`Selected persona ${power_user.personas[selectedPersona]} for current chat.`, t`Connected Persona Selected`); | ||
| 10027 | } | ||
| 10028 | } | ||
| 10029 | }); | ||
| 10030 | |||
| 9988 | $('#character_cross').click(function () { | 10031 | $('#character_cross').click(function () { |
| 9989 | is_advanced_char_open = false; | 10032 | is_advanced_char_open = false; |
| 9990 | $('#character_popup').transition({ | 10033 | $('#character_popup').transition({ |
| @@ -544,32 +544,46 @@ export function updatePersonaConnectionsAvatarList() { | |||
| 544 | * @param {string} title - The title to display in the popup | 544 | * @param {string} title - The title to display in the popup |
| 545 | * @param {string} text - The text to display in the popup | 545 | * @param {string} text - The text to display in the popup |
| 546 | * @param {string[]} personas - An array of persona ids to display for selection | 546 | * @param {string[]} personas - An array of persona ids to display for selection |
| 547 | * @param {Object} [options] - Optional settings for the popup | ||
| 548 | * @param {string} [options.okButton='None'] - The label for the OK button | ||
| 549 | * @param {(element: HTMLElement, ev: MouseEvent) => any} [options.shiftClickHandler] - A function to handle shift-click | ||
| 547 | * @returns {Promise<string?>} - A promise that resolves to the selected persona id or null if no selection was made | 550 | * @returns {Promise<string?>} - A promise that resolves to the selected persona id or null if no selection was made |
| 548 | */ | 551 | */ |
| 549 | export async function askForPersonaSelection(title, text, personas) { | 552 | export async function askForPersonaSelection(title, text, personas, { okButton = 'None', shiftClickHandler = undefined } = {}) { |
| 550 | const content = document.createElement('div'); | 553 | const content = document.createElement('div'); |
| 551 | const titleElement = document.createElement('h3'); | 554 | const titleElement = document.createElement('h3'); |
| 552 | titleElement.textContent = title; | 555 | titleElement.textContent = title; |
| 553 | content.appendChild(titleElement); | 556 | content.appendChild(titleElement); |
| 554 | 557 | ||
| 555 | const textElement = document.createElement('div'); | 558 | const textElement = document.createElement('div'); |
| 556 | textElement.classList.add('m-b-1'); | 559 | textElement.classList.add('multiline', 'm-b-1'); |
| 557 | textElement.textContent = text; | 560 | textElement.textContent = text; |
| 558 | content.appendChild(textElement); | 561 | content.appendChild(textElement); |
| 559 | 562 | ||
| 560 | const personaListBlock = document.createElement('div'); | 563 | const personaListBlock = document.createElement('div'); |
| 561 | personaListBlock.classList.add('persona-list', 'avatars_inline', 'avatars_multiline'); | 564 | personaListBlock.classList.add('persona-list', 'avatars_inline', 'avatars_multiline', 'text_muted'); |
| 562 | content.appendChild(personaListBlock); | 565 | content.appendChild(personaListBlock); |
| 563 | 566 | ||
| 564 | buildPersonaAvatarList(personaListBlock, personas, { interactable: true }); | 567 | if (personas.length > 0) |
| 568 | buildPersonaAvatarList(personaListBlock, personas, { interactable: true }); | ||
| 569 | else | ||
| 570 | personaListBlock.textContent = '[No personas]'; | ||
| 565 | 571 | ||
| 566 | // Make the persona blocks clickable and close the popup | 572 | // Make the persona blocks clickable and close the popup |
| 567 | personaListBlock.querySelectorAll('.avatar[data-type="persona"]').forEach(block => { | 573 | personaListBlock.querySelectorAll('.avatar[data-type="persona"]').forEach(block => { |
| 568 | if (!(block instanceof HTMLElement)) return; | 574 | if (!(block instanceof HTMLElement)) return; |
| 569 | block.dataset.result = String(100 + personas.indexOf(block.dataset.pid)); | 575 | block.dataset.result = String(100 + personas.indexOf(block.dataset.pid)); |
| 576 | |||
| 577 | if (shiftClickHandler) { | ||
| 578 | block.addEventListener('click', function (ev) { | ||
| 579 | if (ev.shiftKey) { | ||
| 580 | shiftClickHandler(this, ev); | ||
| 581 | } | ||
| 582 | }); | ||
| 583 | } | ||
| 570 | }); | 584 | }); |
| 571 | 585 | ||
| 572 | const popup = new Popup(content, POPUP_TYPE.TEXT, '', { okButton: 'None' }); | 586 | const popup = new Popup(content, POPUP_TYPE.TEXT, '', { okButton: okButton }); |
| 573 | const result = await popup.show(); | 587 | const result = await popup.show(); |
| 574 | return Number(result) > 100 ? personas[Number(result) - 100] : null; | 588 | return Number(result) > 100 ? personas[Number(result) - 100] : null; |
| 575 | } | 589 | } |
| @@ -730,7 +744,7 @@ function selectCurrentPersona() { | |||
| 730 | * @param {PersonaConnection} connection - Connection to check | 744 | * @param {PersonaConnection} connection - Connection to check |
| 731 | * @returns {boolean} Whether the connection is locked | 745 | * @returns {boolean} Whether the connection is locked |
| 732 | */ | 746 | */ |
| 733 | function isPersonaConnectionLocked(connection) { | 747 | export function isPersonaConnectionLocked(connection) { |
| 734 | return (menu_type === 'character_edit' && connection.type === 'character' && connection.id === characters[this_chid]?.avatar) | 748 | return (menu_type === 'character_edit' && connection.type === 'character' && connection.id === characters[this_chid]?.avatar) |
| 735 | || (menu_type === 'group_edit' && connection.type === 'group' && connection.id === selected_group); | 749 | || (menu_type === 'group_edit' && connection.type === 'group' && connection.id === selected_group); |
| 736 | } | 750 | } |
| @@ -1199,10 +1213,7 @@ async function loadPersonaForCurrentChat() { | |||
| 1199 | 1213 | ||
| 1200 | // Check if we have any persona connected to the current character | 1214 | // Check if we have any persona connected to the current character |
| 1201 | if (!chatPersona) { | 1215 | if (!chatPersona) { |
| 1202 | const characterKey = menu_type === 'group_edit' ? selected_group : characters[this_chid]?.avatar; | 1216 | const connectedPersonas = getConnectedPersonas(); |
| 1203 | const connectedPersonas = Object.entries(power_user.persona_descriptions) | ||
| 1204 | .filter(([_, desc]) => desc.connections?.some(conn => conn.type === 'character' && conn.id === characterKey)) | ||
| 1205 | .map(([key, _]) => key); | ||
| 1206 | 1217 | ||
| 1207 | if (connectedPersonas.length > 0) { | 1218 | if (connectedPersonas.length > 0) { |
| 1208 | if (connectedPersonas.length === 1) { | 1219 | if (connectedPersonas.length === 1) { |
| @@ -1212,7 +1223,7 @@ async function loadPersonaForCurrentChat() { | |||
| 1212 | toastr.warning(t`More than one persona is connected to this character. Using the first available persona for this chat.`, t`Automatic Persona Selection`); | 1223 | toastr.warning(t`More than one persona is connected to this character. Using the first available persona for this chat.`, t`Automatic Persona Selection`); |
| 1213 | } else { | 1224 | } else { |
| 1214 | chatPersona = await askForPersonaSelection(t`Select Persona`, | 1225 | chatPersona = await askForPersonaSelection(t`Select Persona`, |
| 1215 | t`Multiple personas are connected to this character. Select a persona to use for this chat.`, | 1226 | t`Multiple personas are connected to this character.\nSelect a persona to use for this chat.`, |
| 1216 | connectedPersonas); | 1227 | connectedPersonas); |
| 1217 | } | 1228 | } |
| 1218 | } | 1229 | } |
| @@ -1253,6 +1264,20 @@ async function loadPersonaForCurrentChat() { | |||
| 1253 | updatePersonaLockIcons(); | 1264 | updatePersonaLockIcons(); |
| 1254 | } | 1265 | } |
| 1255 | 1266 | ||
| 1267 | /** | ||
| 1268 | * Returns an array of persona keys that are connected to the given character key. | ||
| 1269 | * If the character key is not provided, it defaults to the currently selected group or character. | ||
| 1270 | * @param {string} [characterKey] - The character key to query | ||
| 1271 | * @returns {string[]} - An array of persona keys that are connected to the given character key | ||
| 1272 | */ | ||
| 1273 | export function getConnectedPersonas(characterKey = undefined) { | ||
| 1274 | characterKey ??= menu_type === 'group_edit' ? selected_group : characters[this_chid]?.avatar; | ||
| 1275 | const connectedPersonas = Object.entries(power_user.persona_descriptions) | ||
| 1276 | .filter(([_, desc]) => desc.connections?.some(conn => conn.type === 'character' && conn.id === characterKey)) | ||
| 1277 | .map(([key, _]) => key); | ||
| 1278 | return connectedPersonas; | ||
| 1279 | } | ||
| 1280 | |||
| 1256 | function onBackupPersonas() { | 1281 | function onBackupPersonas() { |
| 1257 | const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, ''); | 1282 | const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, ''); |
| 1258 | const filename = `personas_${timestamp}.json`; | 1283 | const filename = `personas_${timestamp}.json`; |
| @@ -5793,3 +5793,7 @@ body:not(.movingUI) .drawer-content.maximized { | |||
| 5793 | .mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) { | 5793 | .mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) { |
| 5794 | flex: 1; | 5794 | flex: 1; |
| 5795 | } | 5795 | } |
| 5796 | |||
| 5797 | .multiline { | ||
| 5798 | white-space: pre-wrap; | ||
| 5799 | } | ||