Add button to show connections for current char

This commit is contained in:
Wolfsblvt
2025-01-25 02:52:41 +01:00
parent 7c12a286c3
commit 608e1c195b
4 changed files with 84 additions and 11 deletions

View File

@ -5091,6 +5091,7 @@
<div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div> <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div>
<div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore&#10;&#10;Click to load&#10;Shift-click to open 'Link to World Info' popup" data-i18n="[title]world_button_title"></div> <div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore&#10;&#10;Click to load&#10;Shift-click to open 'Link to World Info' popup" data-i18n="[title]world_button_title"></div>
<div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore&#10;Alt+Click to open the lorebook" data-i18n="[title]Chat Lore Alt+Click to open the lorebook"></div> <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore&#10;Alt+Click to open the lorebook" data-i18n="[title]Chat Lore Alt+Click to open the lorebook"></div>
<div id="char_connections_button" class="menu_button fa-solid fa-face-smile" title="Connected Personas" data-i18n="[title]Connected Personas"></div>
<div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div> <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div>
<!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> --> <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> -->
<!-- <div id="set_character_world" class="menu_button fa-solid fa-globe" title="Set a character World Info / Lorebook"></div> --> <!-- <div id="set_character_world" class="menu_button fa-solid fa-globe" title="Set a character World Info / Lorebook"></div> -->

View File

@ -234,6 +234,8 @@ import {
setPersonaDescription, setPersonaDescription,
initUserAvatar, initUserAvatar,
updatePersonaConnectionsAvatarList, updatePersonaConnectionsAvatarList,
getConnectedPersonas,
askForPersonaSelection,
} from './scripts/personas.js'; } from './scripts/personas.js';
import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
import { hideLoader, showLoader } from './scripts/loader.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 () { $('#character_cross').click(function () {
is_advanced_char_open = false; is_advanced_char_open = false;
$('#character_popup').transition({ $('#character_popup').transition({

View File

@ -544,32 +544,46 @@ export function updatePersonaConnectionsAvatarList() {
* @param {string} title - The title to display in the popup * @param {string} title - The title to display in the popup
* @param {string} text - The text 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 {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<string?>} - A promise that resolves to the selected persona id or null if no selection was made * @returns {Promise<string?>} - 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 content = document.createElement('div');
const titleElement = document.createElement('h3'); const titleElement = document.createElement('h3');
titleElement.textContent = title; titleElement.textContent = title;
content.appendChild(titleElement); content.appendChild(titleElement);
const textElement = document.createElement('div'); const textElement = document.createElement('div');
textElement.classList.add('m-b-1'); textElement.classList.add('multiline', 'm-b-1');
textElement.textContent = text; textElement.textContent = text;
content.appendChild(textElement); content.appendChild(textElement);
const personaListBlock = document.createElement('div'); 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); 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 // Make the persona blocks clickable and close the popup
personaListBlock.querySelectorAll('.avatar[data-type="persona"]').forEach(block => { personaListBlock.querySelectorAll('.avatar[data-type="persona"]').forEach(block => {
if (!(block instanceof HTMLElement)) return; if (!(block instanceof HTMLElement)) return;
block.dataset.result = String(100 + personas.indexOf(block.dataset.pid)); 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(); const result = await popup.show();
return Number(result) > 100 ? personas[Number(result) - 100] : null; return Number(result) > 100 ? personas[Number(result) - 100] : null;
} }
@ -730,7 +744,7 @@ function selectCurrentPersona() {
* @param {PersonaConnection} connection - Connection to check * @param {PersonaConnection} connection - Connection to check
* @returns {boolean} Whether the connection is locked * @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) 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); || (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 // Check if we have any persona connected to the current character
if (!chatPersona) { if (!chatPersona) {
const characterKey = menu_type === 'group_edit' ? selected_group : characters[this_chid]?.avatar; const connectedPersonas = getConnectedPersonas();
const connectedPersonas = Object.entries(power_user.persona_descriptions)
.filter(([_, desc]) => desc.connections?.some(conn => conn.type === 'character' && conn.id === characterKey))
.map(([key, _]) => key);
if (connectedPersonas.length > 0) { if (connectedPersonas.length > 0) {
if (connectedPersonas.length === 1) { 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`); 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 { } else {
chatPersona = await askForPersonaSelection(t`Select Persona`, 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); connectedPersonas);
} }
} }
@ -1253,6 +1264,20 @@ async function loadPersonaForCurrentChat() {
updatePersonaLockIcons(); 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() { function onBackupPersonas() {
const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, ''); const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, '');
const filename = `personas_${timestamp}.json`; const filename = `personas_${timestamp}.json`;

View File

@ -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) { .mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) {
flex: 1; flex: 1;
} }
.multiline {
white-space: pre-wrap;
}