Add button to show connections for current char
| @@ -5091,6 +5091,7 @@ | ||
| 5091 | 5091 | <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div> |
| 5092 | 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 | 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 | 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 | 5096 | <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> --> |
| 5096 | 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 | 234 | setPersonaDescription, |
| 235 | 235 | initUserAvatar, |
| 236 | 236 | updatePersonaConnectionsAvatarList, |
| 237 | + getConnectedPersonas, | |
| 238 | + askForPersonaSelection, | |
| 237 | 239 | } from './scripts/personas.js'; |
| 238 | 240 | import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; |
| 239 | 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 | 10031 | $('#character_cross').click(function () { |
| 9989 | 10032 | is_advanced_char_open = false; |
| 9990 | 10033 | $('#character_popup').transition({ |
| @@ -544,32 +544,46 @@ export function updatePersonaConnectionsAvatarList() { | ||
| 544 | 544 | * @param {string} title - The title to display in the popup |
| 545 | 545 | * @param {string} text - The text to display in the popup |
| 546 | 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 | 550 | * @returns {Promise<string?>} - A promise that resolves to the selected persona id or null if no selection was made |
| 548 | 551 | */ |
| 549 | 552 | export async function askForPersonaSelection(title, text, personas, { okButton = 'None', shiftClickHandler = undefined } = {}) { |
| 550 | 553 | const content = document.createElement('div'); |
| 551 | 554 | const titleElement = document.createElement('h3'); |
| 552 | 555 | titleElement.textContent = title; |
| 553 | 556 | content.appendChild(titleElement); |
| 554 | 557 | |
| 555 | 558 | const textElement = document.createElement('div'); |
| 556 | 559 | textElement.classList.add('multiline', 'm-b-1'); |
| 557 | 560 | textElement.textContent = text; |
| 558 | 561 | content.appendChild(textElement); |
| 559 | 562 | |
| 560 | 563 | const personaListBlock = document.createElement('div'); |
| 561 | 564 | personaListBlock.classList.add('persona-list', 'avatars_inline', 'avatars_multiline', 'text_muted'); |
| 562 | 565 | content.appendChild(personaListBlock); |
| 563 | 566 | |
| 567 | + if (personas.length > 0) | |
| 564 | 568 | buildPersonaAvatarList(personaListBlock, personas, { interactable: true }); |
| 569 | + else | |
| 570 | + personaListBlock.textContent = '[No personas]'; | |
| 565 | 571 | |
| 566 | 572 | // Make the persona blocks clickable and close the popup |
| 567 | 573 | personaListBlock.querySelectorAll('.avatar[data-type="persona"]').forEach(block => { |
| 568 | 574 | if (!(block instanceof HTMLElement)) return; |
| 569 | 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 | 586 | const popup = new Popup(content, POPUP_TYPE.TEXT, '', { okButton: 'None'okButton }); |
| 573 | 587 | const result = await popup.show(); |
| 574 | 588 | return Number(result) > 100 ? personas[Number(result) - 100] : null; |
| 575 | 589 | } |
| @@ -730,7 +744,7 @@ function selectCurrentPersona() { | ||
| 730 | 744 | * @param {PersonaConnection} connection - Connection to check |
| 731 | 745 | * @returns {boolean} Whether the connection is locked |
| 732 | 746 | */ |
| 733 | 747 | export function isPersonaConnectionLocked(connection) { |
| 734 | 748 | return (menu_type === 'character_edit' && connection.type === 'character' && connection.id === characters[this_chid]?.avatar) |
| 735 | 749 | || (menu_type === 'group_edit' && connection.type === 'group' && connection.id === selected_group); |
| 736 | 750 | } |
| @@ -1199,10 +1213,7 @@ async function loadPersonaForCurrentChat() { | ||
| 1199 | 1213 | |
| 1200 | 1214 | // Check if we have any persona connected to the current character |
| 1201 | 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 | 1218 | if (connectedPersonas.length > 0) { |
| 1208 | 1219 | if (connectedPersonas.length === 1) { |
| @@ -1212,7 +1223,7 @@ async function loadPersonaForCurrentChat() { | ||
| 1212 | 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 | 1224 | } else { |
| 1214 | 1225 | chatPersona = await askForPersonaSelection(t`Select Persona`, |
| 1215 | 1226 | t`Multiple personas are connected to this character. Select\nSelect a persona to use for this chat.`, |
| 1216 | 1227 | connectedPersonas); |
| 1217 | 1228 | } |
| 1218 | 1229 | } |
| @@ -1253,6 +1264,20 @@ async function loadPersonaForCurrentChat() { | ||
| 1253 | 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 | 1281 | function onBackupPersonas() { |
| 1257 | 1282 | const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, ''); |
| 1258 | 1283 | const filename = `personas_${timestamp}.json`; |
| @@ -5793,3 +5793,7 @@ body:not(.movingUI) .drawer-content.maximized { | ||
| 5793 | 5793 | .mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) { |
| 5794 | 5794 | flex: 1; |
| 5795 | 5795 | } |
| 5796 | + | |
| 5797 | +.multiline { | |
| 5798 | + white-space: pre-wrap; | |
| 5799 | +} | |