Add button to show connections for current char

608e1c195bafab6944a06799bcf76708e1bd6ac1

Wolfsblvt <wolfsblvt@gmail.com>

4 files changed, +84 -11Ignore whitespace
public/index.html+1 -0
@@ -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&#10;&#10;Click to load&#10;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&#10;&#10;Click to load&#10;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&#10;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&#10;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> -->
public/script.js+43 -0
@@ -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';
238import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';240import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
239import { hideLoader, showLoader } from './scripts/loader.js';241import { hideLoader, showLoader } from './scripts/loader.js';
@@ -9985,6 +9987,47 @@ jQuery(async function () {
9985 }9987 }
9986 });9988 });
99879989
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({
public/scripts/personas.js+36 -11
@@ -544,32 +544,46 @@ export function updatePersonaConnectionsAvatarList() {
544 * @param {string} title - The title to display in the popup544 * @param {string} title - The title to display in the popup
545 * @param {string} text - The text to display in the popup545 * @param {string} text - The text to display in the popup
546 * @param {string[]} personas - An array of persona ids to display for selection546 * @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 made550 * @returns {Promise<string?>} - A promise that resolves to the selected persona id or null if no selection was made
548 */551 */
549export async function askForPersonaSelection(title, text, personas) {552export 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);
554557
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);
559562
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);
563566
564 buildPersonaAvatarList(personaListBlock, personas, { interactable: true });567 if (personas.length > 0)
568 buildPersonaAvatarList(personaListBlock, personas, { interactable: true });
569 else
570 personaListBlock.textContent = '[No personas]';
565571
566 // Make the persona blocks clickable and close the popup572 // 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 });
571585
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 check744 * @param {PersonaConnection} connection - Connection to check
731 * @returns {boolean} Whether the connection is locked745 * @returns {boolean} Whether the connection is locked
732 */746 */
733function isPersonaConnectionLocked(connection) {747export 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() {
11991213
1200 // Check if we have any persona connected to the current character1214 // 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);
12061217
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}
12551266
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 */
1273export 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
1256function onBackupPersonas() {1281function 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`;
public/style.css+4 -0
@@ -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}