From 608e1c195bafab6944a06799bcf76708e1bd6ac1 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sat, 25 Jan 2025 02:52:41 +0100 Subject: [PATCH] Add button to show connections for current char --- public/index.html | 1 + public/script.js | 43 ++++++++++++++++++++++++++++++++++ public/scripts/personas.js | 47 +++++++++++++++++++++++++++++--------- public/style.css | 4 ++++ 4 files changed, 84 insertions(+), 11 deletions(-) diff --git a/public/index.html b/public/index.html index 6b3de8598..403df0230 100644 --- a/public/index.html +++ b/public/index.html @@ -5091,6 +5091,7 @@ + diff --git a/public/script.js b/public/script.js index 104b98ca2..2fa0f19fe 100644 --- a/public/script.js +++ b/public/script.js @@ -234,6 +234,8 @@ import { setPersonaDescription, initUserAvatar, updatePersonaConnectionsAvatarList, + getConnectedPersonas, + askForPersonaSelection, } from './scripts/personas.js'; import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; import { hideLoader, showLoader } from './scripts/loader.js'; @@ -9985,6 +9987,47 @@ jQuery(async function () { } }); + $('#char_connections_button').on('click', async () => { + let isRemoving = false; + + const connections = getConnectedPersonas(); + 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.`; + const selectedPersona = await askForPersonaSelection(t`Persona Connections`, message, connections, { + okButton: t`Ok`, + shiftClickHandler: (element, ev) => { + + const personaId = $(element).attr('data-pid'); + + /** @type {import('./scripts/personas.js').PersonaConnection[]} */ + const connections = power_user.persona_descriptions[personaId]?.connections; + if (connections) { + console.log(`Unlocking persona ${personaId} from current character ${name2}`); + power_user.persona_descriptions[personaId].connections = connections.filter(c => { + if (menu_type == 'group_edit' && c.type == 'group' && c.id == selected_group) return false; + else if (c.type == 'character' && c.id == characters[this_chid]?.avatar) return false; + return true; + }); + saveSettingsDebounced(); + updatePersonaConnectionsAvatarList(); + if (power_user.persona_show_notifications) { + toastr.info(t`User persona ${power_user.personas[personaId]} is now unlocked from the current character ${name2}.`, t`Persona unlocked`); + } + + isRemoving = true; + $('#char_connections_button').trigger('click'); + } + }, + }); + + // One of the persona was selected. So load it. + if (!isRemoving && selectedPersona) { + setUserAvatar(selectedPersona); + if (power_user.persona_show_notifications) { + toastr.info(t`Selected persona ${power_user.personas[selectedPersona]} for current chat.`, t`Connected Persona Selected`); + } + } + }); + $('#character_cross').click(function () { is_advanced_char_open = false; $('#character_popup').transition({ diff --git a/public/scripts/personas.js b/public/scripts/personas.js index f254feb8d..11b2bafe1 100644 --- a/public/scripts/personas.js +++ b/public/scripts/personas.js @@ -544,32 +544,46 @@ export function updatePersonaConnectionsAvatarList() { * @param {string} title - The title to display in the popup * @param {string} text - The text to display in the popup * @param {string[]} personas - An array of persona ids to display for selection + * @param {Object} [options] - Optional settings for the popup + * @param {string} [options.okButton='None'] - The label for the OK button + * @param {(element: HTMLElement, ev: MouseEvent) => any} [options.shiftClickHandler] - A function to handle shift-click * @returns {Promise} - A promise that resolves to the selected persona id or null if no selection was made */ -export async function askForPersonaSelection(title, text, personas) { +export async function askForPersonaSelection(title, text, personas, { okButton = 'None', shiftClickHandler = undefined } = {}) { const content = document.createElement('div'); const titleElement = document.createElement('h3'); titleElement.textContent = title; content.appendChild(titleElement); const textElement = document.createElement('div'); - textElement.classList.add('m-b-1'); + textElement.classList.add('multiline', 'm-b-1'); textElement.textContent = text; content.appendChild(textElement); const personaListBlock = document.createElement('div'); - personaListBlock.classList.add('persona-list', 'avatars_inline', 'avatars_multiline'); + personaListBlock.classList.add('persona-list', 'avatars_inline', 'avatars_multiline', 'text_muted'); content.appendChild(personaListBlock); - buildPersonaAvatarList(personaListBlock, personas, { interactable: true }); + if (personas.length > 0) + buildPersonaAvatarList(personaListBlock, personas, { interactable: true }); + else + personaListBlock.textContent = '[No personas]'; // Make the persona blocks clickable and close the popup personaListBlock.querySelectorAll('.avatar[data-type="persona"]').forEach(block => { if (!(block instanceof HTMLElement)) return; block.dataset.result = String(100 + personas.indexOf(block.dataset.pid)); + + if (shiftClickHandler) { + block.addEventListener('click', function (ev) { + if (ev.shiftKey) { + shiftClickHandler(this, ev); + } + }); + } }); - const popup = new Popup(content, POPUP_TYPE.TEXT, '', { okButton: 'None' }); + const popup = new Popup(content, POPUP_TYPE.TEXT, '', { okButton: okButton }); const result = await popup.show(); return Number(result) > 100 ? personas[Number(result) - 100] : null; } @@ -730,7 +744,7 @@ function selectCurrentPersona() { * @param {PersonaConnection} connection - Connection to check * @returns {boolean} Whether the connection is locked */ -function isPersonaConnectionLocked(connection) { +export function isPersonaConnectionLocked(connection) { return (menu_type === 'character_edit' && connection.type === 'character' && connection.id === characters[this_chid]?.avatar) || (menu_type === 'group_edit' && connection.type === 'group' && connection.id === selected_group); } @@ -1199,10 +1213,7 @@ async function loadPersonaForCurrentChat() { // Check if we have any persona connected to the current character if (!chatPersona) { - const characterKey = menu_type === 'group_edit' ? selected_group : characters[this_chid]?.avatar; - const connectedPersonas = Object.entries(power_user.persona_descriptions) - .filter(([_, desc]) => desc.connections?.some(conn => conn.type === 'character' && conn.id === characterKey)) - .map(([key, _]) => key); + const connectedPersonas = getConnectedPersonas(); if (connectedPersonas.length > 0) { if (connectedPersonas.length === 1) { @@ -1212,7 +1223,7 @@ async function loadPersonaForCurrentChat() { toastr.warning(t`More than one persona is connected to this character. Using the first available persona for this chat.`, t`Automatic Persona Selection`); } else { chatPersona = await askForPersonaSelection(t`Select Persona`, - t`Multiple personas are connected to this character. Select a persona to use for this chat.`, + t`Multiple personas are connected to this character.\nSelect a persona to use for this chat.`, connectedPersonas); } } @@ -1253,6 +1264,20 @@ async function loadPersonaForCurrentChat() { updatePersonaLockIcons(); } +/** + * Returns an array of persona keys that are connected to the given character key. + * If the character key is not provided, it defaults to the currently selected group or character. + * @param {string} [characterKey] - The character key to query + * @returns {string[]} - An array of persona keys that are connected to the given character key + */ +export function getConnectedPersonas(characterKey = undefined) { + characterKey ??= menu_type === 'group_edit' ? selected_group : characters[this_chid]?.avatar; + const connectedPersonas = Object.entries(power_user.persona_descriptions) + .filter(([_, desc]) => desc.connections?.some(conn => conn.type === 'character' && conn.id === characterKey)) + .map(([key, _]) => key); + return connectedPersonas; +} + function onBackupPersonas() { const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, ''); const filename = `personas_${timestamp}.json`; diff --git a/public/style.css b/public/style.css index 413a6273f..750af7e88 100644 --- a/public/style.css +++ b/public/style.css @@ -5793,3 +5793,7 @@ body:not(.movingUI) .drawer-content.maximized { .mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) { flex: 1; } + +.multiline { + white-space: pre-wrap; +}