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 @@
50915091 <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div>
50925092 <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>
50935093 <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>
50945095 <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div>
50955096 <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> -->
50965097 <!-- <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 {
234234 setPersonaDescription,
235235 initUserAvatar,
236236 updatePersonaConnectionsAvatarList,
237+ getConnectedPersonas,
238+ askForPersonaSelection,
237239} from './scripts/personas.js';
238240import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
239241import { hideLoader, showLoader } from './scripts/loader.js';
@@ -9985,6 +9987,47 @@ jQuery(async function () {
99859987 }
99869988 });
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+
998810031 $('#character_cross').click(function () {
998910032 is_advanced_char_open = false;
999010033 $('#character_popup').transition({
public/scripts/personas.js+36 -11
@@ -544,32 +544,46 @@ export function updatePersonaConnectionsAvatarList() {
544544 * @param {string} title - The title to display in the popup
545545 * @param {string} text - The text to display in the popup
546546 * @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
547550 * @returns {Promise<string?>} - A promise that resolves to the selected persona id or null if no selection was made
548551 */
549552export async function askForPersonaSelection(title, text, personas, { okButton = 'None', shiftClickHandler = undefined } = {}) {
550553 const content = document.createElement('div');
551554 const titleElement = document.createElement('h3');
552555 titleElement.textContent = title;
553556 content.appendChild(titleElement);
554557
555558 const textElement = document.createElement('div');
556559 textElement.classList.add('multiline', 'm-b-1');
557560 textElement.textContent = text;
558561 content.appendChild(textElement);
559562
560563 const personaListBlock = document.createElement('div');
561564 personaListBlock.classList.add('persona-list', 'avatars_inline', 'avatars_multiline', 'text_muted');
562565 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
566572 // Make the persona blocks clickable and close the popup
567573 personaListBlock.querySelectorAll('.avatar[data-type="persona"]').forEach(block => {
568574 if (!(block instanceof HTMLElement)) return;
569575 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+ }
570584 });
571585
572586 const popup = new Popup(content, POPUP_TYPE.TEXT, '', { okButton: 'None'okButton });
573587 const result = await popup.show();
574588 return Number(result) > 100 ? personas[Number(result) - 100] : null;
575589}
@@ -730,7 +744,7 @@ function selectCurrentPersona() {
730744 * @param {PersonaConnection} connection - Connection to check
731745 * @returns {boolean} Whether the connection is locked
732746 */
733747export function isPersonaConnectionLocked(connection) {
734748 return (menu_type === 'character_edit' && connection.type === 'character' && connection.id === characters[this_chid]?.avatar)
735749 || (menu_type === 'group_edit' && connection.type === 'group' && connection.id === selected_group);
736750}
@@ -1199,10 +1213,7 @@ async function loadPersonaForCurrentChat() {
11991213
12001214 // Check if we have any persona connected to the current character
12011215 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
12071218 if (connectedPersonas.length > 0) {
12081219 if (connectedPersonas.length === 1) {
@@ -1212,7 +1223,7 @@ async function loadPersonaForCurrentChat() {
12121223 toastr.warning(t`More than one persona is connected to this character. Using the first available persona for this chat.`, t`Automatic Persona Selection`);
12131224 } else {
12141225 chatPersona = await askForPersonaSelection(t`Select Persona`,
12151226 t`Multiple personas are connected to this character. Select\nSelect a persona to use for this chat.`,
12161227 connectedPersonas);
12171228 }
12181229 }
@@ -1253,6 +1264,20 @@ async function loadPersonaForCurrentChat() {
12531264 updatePersonaLockIcons();
12541265}
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+ */
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+
12561281function onBackupPersonas() {
12571282 const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, '');
12581283 const filename = `personas_${timestamp}.json`;
public/style.css+4 -0
@@ -5793,3 +5793,7 @@ body:not(.movingUI) .drawer-content.maximized {
57935793.mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) {
57945794 flex: 1;
57955795}
5796+
5797+.multiline {
5798+ white-space: pre-wrap;
5799+}