mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			1979 lines
		
	
	
		
			78 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1979 lines
		
	
	
		
			78 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import {
 | 
						|
    buildAvatarList,
 | 
						|
    characterToEntity,
 | 
						|
    characters,
 | 
						|
    chat,
 | 
						|
    chat_metadata,
 | 
						|
    default_user_avatar,
 | 
						|
    eventSource,
 | 
						|
    event_types,
 | 
						|
    getRequestHeaders,
 | 
						|
    getThumbnailUrl,
 | 
						|
    groupToEntity,
 | 
						|
    menu_type,
 | 
						|
    name1,
 | 
						|
    name2,
 | 
						|
    reloadCurrentChat,
 | 
						|
    saveChatConditional,
 | 
						|
    saveMetadata,
 | 
						|
    saveSettingsDebounced,
 | 
						|
    setUserName,
 | 
						|
    this_chid,
 | 
						|
} from '../script.js';
 | 
						|
import { persona_description_positions, power_user } from './power-user.js';
 | 
						|
import { getTokenCountAsync } from './tokenizers.js';
 | 
						|
import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, parseJsonFile, setInfoBlock } from './utils.js';
 | 
						|
import { debounce_timeout } from './constants.js';
 | 
						|
import { FILTER_TYPES, FilterHelper } from './filters.js';
 | 
						|
import { groups, selected_group } from './group-chats.js';
 | 
						|
import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
 | 
						|
import { t } from './i18n.js';
 | 
						|
import { openWorldInfoEditor, world_names } from './world-info.js';
 | 
						|
import { renderTemplateAsync } from './templates.js';
 | 
						|
import { saveMetadataDebounced } from './extensions.js';
 | 
						|
import { accountStorage } from './util/AccountStorage.js';
 | 
						|
import { SlashCommand } from './slash-commands/SlashCommand.js';
 | 
						|
import { SlashCommandNamedArgument, ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
 | 
						|
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
 | 
						|
import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
 | 
						|
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
 | 
						|
 | 
						|
/**
 | 
						|
 * @typedef {object} PersonaConnection A connection between a character and a character or group entity
 | 
						|
 * @property {'character' | 'group'} type - Type of connection
 | 
						|
 * @property {string} id - ID of the connection (character key (avatar url), group id)
 | 
						|
 */
 | 
						|
 | 
						|
/** @typedef {'chat' | 'character' | 'default'} PersonaLockType Type of the persona lock */
 | 
						|
 | 
						|
/**
 | 
						|
 * @typedef {object} PersonaState
 | 
						|
 * @property {string} avatarId - The avatar id of the persona
 | 
						|
 * @property {boolean} default - Whether this persona is the default one for all new chats
 | 
						|
 * @property {object} locked - An object containing the lock states
 | 
						|
 * @property {boolean} locked.chat - Whether the persona is locked to the currently open chat
 | 
						|
 * @property {boolean} locked.character - Whether the persona is locked to the currently open character or group
 | 
						|
 */
 | 
						|
 | 
						|
const USER_AVATAR_PATH = 'User Avatars/';
 | 
						|
 | 
						|
let savePersonasPage = 0;
 | 
						|
const GRID_STORAGE_KEY = 'Personas_GridView';
 | 
						|
const DEFAULT_DEPTH = 2;
 | 
						|
const DEFAULT_ROLE = 0;
 | 
						|
 | 
						|
/** @type {string} The currently selected persona (identified by its avatar) */
 | 
						|
export let user_avatar = '';
 | 
						|
 | 
						|
/** @type {FilterHelper} Filter helper for the persona list */
 | 
						|
export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick));
 | 
						|
 | 
						|
/** @type {function(string): void} */
 | 
						|
let navigateToAvatar = () => { };
 | 
						|
 | 
						|
/**
 | 
						|
 * Checks if the Persona Management panel is currently open
 | 
						|
 * @returns {boolean}
 | 
						|
 */
 | 
						|
export function isPersonaPanelOpen() {
 | 
						|
    return document.querySelector('#persona-management-button .drawer-content')?.classList.contains('openDrawer') ?? false;
 | 
						|
}
 | 
						|
 | 
						|
function switchPersonaGridView() {
 | 
						|
    const state = accountStorage.getItem(GRID_STORAGE_KEY) === 'true';
 | 
						|
    $('#user_avatar_block').toggleClass('gridView', state);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns the URL of the avatar for the given user avatar Id.
 | 
						|
 * @param {string} avatarImg User avatar Id
 | 
						|
 * @returns {string} User avatar URL
 | 
						|
 */
 | 
						|
export function getUserAvatar(avatarImg) {
 | 
						|
    return `${USER_AVATAR_PATH}${avatarImg}`;
 | 
						|
}
 | 
						|
 | 
						|
export function initUserAvatar(avatar) {
 | 
						|
    user_avatar = avatar;
 | 
						|
    reloadUserAvatar();
 | 
						|
    updatePersonaUIStates();
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Sets a user avatar file
 | 
						|
 * @param {string} imgfile Link to an image file
 | 
						|
 * @param {object} [options] Optional settings
 | 
						|
 * @param {boolean} [options.toastPersonaNameChange=true] Whether to show a toast when the persona name is changed
 | 
						|
 * @param {boolean} [options.navigateToCurrent=false] Whether to navigate to the current persona after setting the avatar
 | 
						|
 */
 | 
						|
export function setUserAvatar(imgfile, { toastPersonaNameChange = true, navigateToCurrent = false } = {}) {
 | 
						|
    user_avatar = imgfile && typeof imgfile === 'string' ? imgfile : $(this).attr('data-avatar-id');
 | 
						|
    reloadUserAvatar();
 | 
						|
    updatePersonaUIStates({ navigateToCurrent: navigateToCurrent });
 | 
						|
    selectCurrentPersona({ toastPersonaNameChange: toastPersonaNameChange });
 | 
						|
    saveSettingsDebounced();
 | 
						|
    $('.zoomed_avatar[forchar]').remove();
 | 
						|
}
 | 
						|
 | 
						|
function reloadUserAvatar(force = false) {
 | 
						|
    $('.mes').each(function () {
 | 
						|
        const avatarImg = $(this).find('.avatar img');
 | 
						|
        if (force) {
 | 
						|
            avatarImg.attr('src', avatarImg.attr('src'));
 | 
						|
        }
 | 
						|
 | 
						|
        if ($(this).attr('is_user') == 'true' && $(this).attr('force_avatar') == 'false') {
 | 
						|
            avatarImg.attr('src', getUserAvatar(user_avatar));
 | 
						|
        }
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Sort the given personas
 | 
						|
 * @param {string[]} personas - The persona names to sort
 | 
						|
 * @returns {string[]} The sorted persona names arrray, same reference as passed in
 | 
						|
 */
 | 
						|
function sortPersonas(personas) {
 | 
						|
    const option = $('#persona_sort_order').find(':selected');
 | 
						|
    if (option.attr('value') === 'search') {
 | 
						|
        personas.sort((a, b) => {
 | 
						|
            const aScore = personasFilter.getScore(FILTER_TYPES.PERSONA_SEARCH, a);
 | 
						|
            const bScore = personasFilter.getScore(FILTER_TYPES.PERSONA_SEARCH, b);
 | 
						|
            return (aScore - bScore);
 | 
						|
        });
 | 
						|
    } else {
 | 
						|
        personas.sort((a, b) => {
 | 
						|
            const aName = String(power_user.personas[a] || a);
 | 
						|
            const bName = String(power_user.personas[b] || b);
 | 
						|
            return power_user.persona_sort_order === 'asc' ? aName.localeCompare(bName) : bName.localeCompare(aName);
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    return personas;
 | 
						|
}
 | 
						|
 | 
						|
/** Checks the state of the current search, and adds/removes the search sorting option accordingly */
 | 
						|
function verifyPersonaSearchSortRule() {
 | 
						|
    const searchTerm = personasFilter.getFilterData(FILTER_TYPES.PERSONA_SEARCH);
 | 
						|
    const searchOption = $('#persona_sort_order option[value="search"]');
 | 
						|
    const selector = $('#persona_sort_order');
 | 
						|
    const isHidden = searchOption.attr('hidden') !== undefined;
 | 
						|
 | 
						|
    // If we have a search term, we are displaying the sorting option for it
 | 
						|
    if (searchTerm && isHidden) {
 | 
						|
        searchOption.removeAttr('hidden');
 | 
						|
        selector.val(searchOption.attr('value'));
 | 
						|
        flashHighlight(selector);
 | 
						|
    }
 | 
						|
    // If search got cleared, we make sure to hide the option and go back to the one before
 | 
						|
    if (!searchTerm) {
 | 
						|
        searchOption.attr('hidden', '');
 | 
						|
        selector.val(power_user.persona_sort_order);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Gets a rendered avatar block.
 | 
						|
 * @param {string} avatarId Avatar file name
 | 
						|
 * @returns {JQuery<HTMLElement>} Avatar block
 | 
						|
 */
 | 
						|
function getUserAvatarBlock(avatarId) {
 | 
						|
    const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
 | 
						|
    const template = $('#user_avatar_template .avatar-container').clone();
 | 
						|
    const personaName = power_user.personas[avatarId];
 | 
						|
    const personaDescription = power_user.persona_descriptions[avatarId]?.description;
 | 
						|
 | 
						|
    template.find('.ch_name').text(personaName || '[Unnamed Persona]');
 | 
						|
    template.find('.ch_description').text(personaDescription || $('#user_avatar_block').attr('no_desc_text')).toggleClass('text_muted', !personaDescription);
 | 
						|
    template.attr('data-avatar-id', avatarId);
 | 
						|
    template.find('.avatar').attr('data-avatar-id', avatarId).attr('title', avatarId);
 | 
						|
    template.toggleClass('default_persona', avatarId === power_user.default_persona);
 | 
						|
    let avatarUrl = getUserAvatar(avatarId);
 | 
						|
    if (isFirefox) {
 | 
						|
        avatarUrl += '?t=' + Date.now();
 | 
						|
    }
 | 
						|
    template.find('img').attr('src', avatarUrl);
 | 
						|
 | 
						|
    // Make sure description block has at least three rows. Otherwise height looks inconsistent. I don't have a better idea for this.
 | 
						|
    const currentText = template.find('.ch_description').text();
 | 
						|
    if (currentText.split('\n').length < 3) {
 | 
						|
        template.find('.ch_description').text(currentText + '\n\xa0\n\xa0');
 | 
						|
    }
 | 
						|
 | 
						|
    $('#user_avatar_block').append(template);
 | 
						|
    return template;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Initialize missing personas in the power user settings.
 | 
						|
 * @param {string[]} avatarsList List of avatar file names
 | 
						|
 */
 | 
						|
function addMissingPersonas(avatarsList) {
 | 
						|
    for (const persona of avatarsList) {
 | 
						|
        if (!power_user.personas[persona]) {
 | 
						|
            initPersona(persona, '[Unnamed Persona]', '');
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Gets a list of user avatars.
 | 
						|
 * @param {boolean} doRender Whether to render the list
 | 
						|
 * @param {string} openPageAt Item to be opened at
 | 
						|
 * @returns {Promise<string[]>} List of avatar file names
 | 
						|
 */
 | 
						|
export async function getUserAvatars(doRender = true, openPageAt = '') {
 | 
						|
    const response = await fetch('/api/avatars/get', {
 | 
						|
        method: 'POST',
 | 
						|
        headers: getRequestHeaders(),
 | 
						|
    });
 | 
						|
    if (response.ok) {
 | 
						|
        const allEntities = await response.json();
 | 
						|
 | 
						|
        if (!Array.isArray(allEntities)) {
 | 
						|
            return [];
 | 
						|
        }
 | 
						|
 | 
						|
        if (!doRender) {
 | 
						|
            return allEntities;
 | 
						|
        }
 | 
						|
 | 
						|
        // If any persona is missing from the power user settings, we add it
 | 
						|
        addMissingPersonas(allEntities);
 | 
						|
        // Before printing the personas, we check if we should enable/disable search sorting
 | 
						|
        verifyPersonaSearchSortRule();
 | 
						|
 | 
						|
        let entities = personasFilter.applyFilters(allEntities);
 | 
						|
        entities = sortPersonas(entities);
 | 
						|
 | 
						|
        const storageKey = 'Personas_PerPage';
 | 
						|
        const listId = '#user_avatar_block';
 | 
						|
        const perPage = Number(accountStorage.getItem(storageKey)) || 5;
 | 
						|
 | 
						|
        $('#persona_pagination_container').pagination({
 | 
						|
            dataSource: entities,
 | 
						|
            pageSize: perPage,
 | 
						|
            sizeChangerOptions: [5, 10, 25, 50, 100, 250, 500, 1000],
 | 
						|
            pageRange: 1,
 | 
						|
            pageNumber: savePersonasPage || 1,
 | 
						|
            position: 'top',
 | 
						|
            showPageNumbers: false,
 | 
						|
            showSizeChanger: true,
 | 
						|
            prevText: '<',
 | 
						|
            nextText: '>',
 | 
						|
            formatNavigator: PAGINATION_TEMPLATE,
 | 
						|
            showNavigator: true,
 | 
						|
            callback: function (data) {
 | 
						|
                $(listId).empty();
 | 
						|
                for (const item of data) {
 | 
						|
                    $(listId).append(getUserAvatarBlock(item));
 | 
						|
                }
 | 
						|
                updatePersonaUIStates();
 | 
						|
            },
 | 
						|
            afterSizeSelectorChange: function (e) {
 | 
						|
                accountStorage.setItem(storageKey, e.target.value);
 | 
						|
            },
 | 
						|
            afterPaging: function (e) {
 | 
						|
                savePersonasPage = e;
 | 
						|
            },
 | 
						|
            afterRender: function () {
 | 
						|
                $(listId).scrollTop(0);
 | 
						|
            },
 | 
						|
        });
 | 
						|
 | 
						|
        navigateToAvatar = (avatarId) => {
 | 
						|
            const avatarIndex = entities.indexOf(avatarId);
 | 
						|
            const page = Math.floor(avatarIndex / perPage) + 1;
 | 
						|
 | 
						|
            if (avatarIndex !== -1) {
 | 
						|
                $('#persona_pagination_container').pagination('go', page);
 | 
						|
            }
 | 
						|
        };
 | 
						|
 | 
						|
        openPageAt && navigateToAvatar(openPageAt);
 | 
						|
 | 
						|
        return allEntities;
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Uploads an avatar file to the server
 | 
						|
 * @param {string} url URL for the avatar file
 | 
						|
 * @param {string} [name] Optional name for the avatar file
 | 
						|
 * @returns {Promise} Promise that resolves when the avatar is uploaded
 | 
						|
 */
 | 
						|
async function uploadUserAvatar(url, name) {
 | 
						|
    const fetchResult = await fetch(url);
 | 
						|
    const blob = await fetchResult.blob();
 | 
						|
    const file = new File([blob], 'avatar.png', { type: 'image/png' });
 | 
						|
    const formData = new FormData();
 | 
						|
    formData.append('avatar', file);
 | 
						|
 | 
						|
    if (name) {
 | 
						|
        formData.append('overwrite_name', name);
 | 
						|
    }
 | 
						|
 | 
						|
    const headers = getRequestHeaders();
 | 
						|
    delete headers['Content-Type'];
 | 
						|
 | 
						|
    await fetch('/api/avatars/upload', {
 | 
						|
        method: 'POST',
 | 
						|
        headers: headers,
 | 
						|
        cache: 'no-cache',
 | 
						|
        body: formData,
 | 
						|
    });
 | 
						|
 | 
						|
    await getUserAvatars(true, name);
 | 
						|
}
 | 
						|
 | 
						|
async function changeUserAvatar(e) {
 | 
						|
    const form = document.getElementById('form_upload_avatar');
 | 
						|
 | 
						|
    if (!(form instanceof HTMLFormElement)) {
 | 
						|
        console.error('Form not found');
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const file = e.target.files[0];
 | 
						|
 | 
						|
    if (!file) {
 | 
						|
        form.reset();
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const formData = new FormData(form);
 | 
						|
    const dataUrl = await getBase64Async(file);
 | 
						|
    let url = '/api/avatars/upload';
 | 
						|
 | 
						|
    if (!power_user.never_resize_avatars) {
 | 
						|
        const dlg = new Popup(t`Set the crop position of the avatar image`, POPUP_TYPE.CROP, '', { cropImage: dataUrl });
 | 
						|
        const result = await dlg.show();
 | 
						|
 | 
						|
        if (!result) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        if (dlg.cropData !== undefined) {
 | 
						|
            url += `?crop=${encodeURIComponent(JSON.stringify(dlg.cropData))}`;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    const rawFile = formData.get('avatar');
 | 
						|
    if (rawFile instanceof File) {
 | 
						|
        const convertedFile = await ensureImageFormatSupported(rawFile);
 | 
						|
        formData.set('avatar', convertedFile);
 | 
						|
    }
 | 
						|
 | 
						|
    const headers = getRequestHeaders();
 | 
						|
    delete headers['Content-Type'];
 | 
						|
 | 
						|
    const response = await fetch(url, {
 | 
						|
        method: 'POST',
 | 
						|
        headers: headers,
 | 
						|
        cache: 'no-cache',
 | 
						|
        body: formData,
 | 
						|
    });
 | 
						|
 | 
						|
    if (response.ok) {
 | 
						|
        const data = await response.json();
 | 
						|
 | 
						|
        // If the user uploaded a new avatar, we want to make sure it's not cached
 | 
						|
        const name = formData.get('overwrite_name');
 | 
						|
        if (name) {
 | 
						|
            await fetch(getUserAvatar(String(name)), { cache: 'no-cache' });
 | 
						|
            reloadUserAvatar(true);
 | 
						|
        }
 | 
						|
 | 
						|
        if (!name && data.path) {
 | 
						|
            await getUserAvatars();
 | 
						|
            await delay(500);
 | 
						|
            await createPersona(data.path);
 | 
						|
        }
 | 
						|
 | 
						|
        await getUserAvatars(true, name || data.path);
 | 
						|
    }
 | 
						|
 | 
						|
    // Will allow to select the same file twice in a row
 | 
						|
    form.reset();
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Prompts the user to create a persona for the uploaded avatar.
 | 
						|
 * @param {string} avatarId User avatar id
 | 
						|
 * @returns {Promise} Promise that resolves when the persona is set
 | 
						|
 */
 | 
						|
export async function createPersona(avatarId) {
 | 
						|
    const personaName = await Popup.show.input(t`Enter a name for this persona:`, t`Cancel if you're just uploading an avatar.`, '');
 | 
						|
 | 
						|
    if (!personaName) {
 | 
						|
        console.debug('User cancelled creating a persona');
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const personaDescription = await Popup.show.input(t`Enter a description for this persona:`, t`You can always add or change it later.`, '', { rows: 4 });
 | 
						|
 | 
						|
    initPersona(avatarId, personaName, personaDescription);
 | 
						|
    if (power_user.persona_show_notifications) {
 | 
						|
        toastr.success(t`You can now pick ${personaName} as a persona in the Persona Management menu.`, t`Persona Created`);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
async function createDummyPersona() {
 | 
						|
    const personaName = await Popup.show.input(t`Enter a name for this persona:`, null);
 | 
						|
 | 
						|
    if (!personaName) {
 | 
						|
        console.debug('User cancelled creating dummy persona');
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Date + name (only ASCII) to make it unique
 | 
						|
    const avatarId = `${Date.now()}-${personaName.replace(/[^a-zA-Z0-9]/g, '')}.png`;
 | 
						|
    initPersona(avatarId, personaName, '');
 | 
						|
    await uploadUserAvatar(default_user_avatar, avatarId);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Initializes a persona for the given avatar id.
 | 
						|
 * @param {string} avatarId User avatar id
 | 
						|
 * @param {string} personaName Name for the persona
 | 
						|
 * @param {string} personaDescription Optional description for the persona
 | 
						|
 * @returns {void}
 | 
						|
 */
 | 
						|
export function initPersona(avatarId, personaName, personaDescription) {
 | 
						|
    power_user.personas[avatarId] = personaName;
 | 
						|
    power_user.persona_descriptions[avatarId] = {
 | 
						|
        description: personaDescription || '',
 | 
						|
        position: persona_description_positions.IN_PROMPT,
 | 
						|
        depth: DEFAULT_DEPTH,
 | 
						|
        role: DEFAULT_ROLE,
 | 
						|
        lorebook: '',
 | 
						|
    };
 | 
						|
 | 
						|
    saveSettingsDebounced();
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Converts a character given character (either by character id or the current character) to a persona.
 | 
						|
 *
 | 
						|
 * If a persona with the same name already exists, the user is prompted to confirm whether or not to overwrite it.
 | 
						|
 * If the character description contains {{char}} or {{user}} macros, the user is prompted to confirm whether or not to swap them for persona macros.
 | 
						|
 *
 | 
						|
 * The function creates a new persona with the same name as the character, and sets the persona description to the character description with the macros swapped.
 | 
						|
 * The function also saves the settings and refreshes the persona selector.
 | 
						|
 *
 | 
						|
 * @param {number} [characterId] - The ID of the character to convert to a persona. Defaults to the current character ID.
 | 
						|
 * @returns {Promise<boolean>} A promise that resolves to true if the character was converted, false otherwise.
 | 
						|
 */
 | 
						|
export async function convertCharacterToPersona(characterId = null) {
 | 
						|
    if (null === characterId) characterId = this_chid;
 | 
						|
 | 
						|
    const avatarUrl = characters[characterId]?.avatar;
 | 
						|
    if (!avatarUrl) {
 | 
						|
        console.log('No avatar found for this character');
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    const name = characters[characterId]?.name;
 | 
						|
    let description = characters[characterId]?.description;
 | 
						|
    const overwriteName = `${name} (Persona).png`;
 | 
						|
 | 
						|
    if (overwriteName in power_user.personas) {
 | 
						|
        const confirm = await Popup.show.confirm(t`Overwrite Existing Persona`, t`This character exists as a persona already. Do you want to overwrite it?`);
 | 
						|
        if (!confirm) {
 | 
						|
            console.log('User cancelled the overwrite of the persona');
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    if (description.includes('{{char}}') || description.includes('{{user}}')) {
 | 
						|
        const confirm = await Popup.show.confirm(t`Persona Description Macros`, t`This character has a description that uses <code>{{char}}</code> or <code>{{user}}</code> macros. Do you want to swap them in the persona description?`);
 | 
						|
        if (confirm) {
 | 
						|
            description = description.replace(/{{char}}/gi, '{{personaChar}}').replace(/{{user}}/gi, '{{personaUser}}');
 | 
						|
            description = description.replace(/{{personaUser}}/gi, '{{char}}').replace(/{{personaChar}}/gi, '{{user}}');
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    const thumbnailAvatar = getThumbnailUrl('avatar', avatarUrl);
 | 
						|
    await uploadUserAvatar(thumbnailAvatar, overwriteName);
 | 
						|
 | 
						|
    power_user.personas[overwriteName] = name;
 | 
						|
    power_user.persona_descriptions[overwriteName] = {
 | 
						|
        description: description,
 | 
						|
        position: persona_description_positions.IN_PROMPT,
 | 
						|
        depth: DEFAULT_DEPTH,
 | 
						|
        role: DEFAULT_ROLE,
 | 
						|
        lorebook: '',
 | 
						|
    };
 | 
						|
 | 
						|
    // If the user is currently using this persona, update the description
 | 
						|
    if (user_avatar === overwriteName) {
 | 
						|
        power_user.persona_description = description;
 | 
						|
    }
 | 
						|
 | 
						|
    saveSettingsDebounced();
 | 
						|
 | 
						|
    console.log('Persona for character created');
 | 
						|
    toastr.success(t`You can now pick ${name} as a persona in the Persona Management menu.`, t`Persona Created`);
 | 
						|
 | 
						|
    // Refresh the persona selector
 | 
						|
    await getUserAvatars(true, overwriteName);
 | 
						|
    // Reload the persona description
 | 
						|
    setPersonaDescription();
 | 
						|
    return true;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Counts the number of tokens in a persona description.
 | 
						|
 */
 | 
						|
const countPersonaDescriptionTokens = debounce(async () => {
 | 
						|
    const description = String($('#persona_description').val());
 | 
						|
    const count = await getTokenCountAsync(description);
 | 
						|
    $('#persona_description_token_count').text(String(count));
 | 
						|
}, debounce_timeout.relaxed);
 | 
						|
 | 
						|
/**
 | 
						|
 * Updates the UI for the Persona Management page with the current persona values
 | 
						|
 */
 | 
						|
export function setPersonaDescription() {
 | 
						|
    $('#your_name').text(name1);
 | 
						|
 | 
						|
    if (power_user.persona_description_position === persona_description_positions.AFTER_CHAR) {
 | 
						|
        power_user.persona_description_position = persona_description_positions.IN_PROMPT;
 | 
						|
    }
 | 
						|
 | 
						|
    $('#persona_depth_position_settings').toggle(power_user.persona_description_position === persona_description_positions.AT_DEPTH);
 | 
						|
    $('#persona_description').val(power_user.persona_description);
 | 
						|
    $('#persona_depth_value').val(power_user.persona_description_depth ?? DEFAULT_DEPTH);
 | 
						|
    $('#persona_description_position')
 | 
						|
        .val(power_user.persona_description_position)
 | 
						|
        .find(`option[value="${power_user.persona_description_position}"]`)
 | 
						|
        .attr('selected', String(true));
 | 
						|
    $('#persona_depth_role')
 | 
						|
        .val(power_user.persona_description_role)
 | 
						|
        .find(`option[value="${power_user.persona_description_role}"]`)
 | 
						|
        .prop('selected', String(true));
 | 
						|
    $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook);
 | 
						|
    countPersonaDescriptionTokens();
 | 
						|
 | 
						|
    updatePersonaUIStates();
 | 
						|
    updatePersonaConnectionsAvatarList();
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Gets a list of all personas in the current chat.
 | 
						|
 *
 | 
						|
 * @returns {string[]} An array of persona identifiers
 | 
						|
 */
 | 
						|
function getPersonasOfCurrentChat() {
 | 
						|
    const personas = chat.filter(message => String(message.force_avatar).startsWith(USER_AVATAR_PATH))
 | 
						|
        .map(message => message.force_avatar.replace(USER_AVATAR_PATH, ''))
 | 
						|
        .filter(onlyUnique);
 | 
						|
    return personas;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Builds a list of persona avatars and populates the given block element with them.
 | 
						|
 *
 | 
						|
 * @param {HTMLElement} block - The HTML element where the avatar list will be rendered
 | 
						|
 * @param {string[]} personas - An array of persona identifiers
 | 
						|
 * @param {Object} [options] - Optional settings for building the avatar list
 | 
						|
 * @param {boolean} [options.empty=true] - Whether to clear the block element before adding avatars
 | 
						|
 * @param {boolean} [options.interactable=false] - Whether the avatars should be interactable
 | 
						|
 * @param {boolean} [options.highlightFavs=true] - Whether to highlight favorite avatars
 | 
						|
 */
 | 
						|
export function buildPersonaAvatarList(block, personas, { empty = true, interactable = false, highlightFavs = true } = {}) {
 | 
						|
    const personaEntities = personas.map(avatar => ({
 | 
						|
        type: 'persona',
 | 
						|
        id: avatar,
 | 
						|
        item: {
 | 
						|
            name: power_user.personas[avatar],
 | 
						|
            description: power_user.persona_descriptions[avatar]?.description || '',
 | 
						|
            avatar: avatar,
 | 
						|
            fav: power_user.default_persona === avatar,
 | 
						|
        },
 | 
						|
    }));
 | 
						|
 | 
						|
    buildAvatarList($(block), personaEntities, { empty: empty, interactable: interactable, highlightFavs: highlightFavs });
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Displays avatar connections for the current persona.
 | 
						|
 * Converts connections to entities and populates the avatar list. Shows a message if no connections are found.
 | 
						|
 */
 | 
						|
export function updatePersonaConnectionsAvatarList() {
 | 
						|
    /** @type {PersonaConnection[]} */
 | 
						|
    const connections = power_user.persona_descriptions[user_avatar]?.connections ?? [];
 | 
						|
    const entities = connections.map(connection => {
 | 
						|
        if (connection.type === 'character') {
 | 
						|
            const character = characters.find(c => c.avatar === connection.id);
 | 
						|
            if (character) return characterToEntity(character, getCharIndex(character));
 | 
						|
        }
 | 
						|
        if (connection.type === 'group') {
 | 
						|
            const group = groups.find(g => g.id === connection.id);
 | 
						|
            if (group) return groupToEntity(group);
 | 
						|
        }
 | 
						|
        return undefined;
 | 
						|
    }).filter(entity => entity?.item !== undefined);
 | 
						|
 | 
						|
    if (entities.length)
 | 
						|
        buildAvatarList($('#persona_connections_list'), entities, { interactable: true });
 | 
						|
    else
 | 
						|
        $('#persona_connections_list').text(t`[No character connections. Click one of the buttons above to connect this persona.]`);
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
/**
 | 
						|
 * Displays a popup for persona selection and returns the selected persona.
 | 
						|
 *
 | 
						|
 * @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
 | 
						|
 * @param {boolean|string[]} [options.highlightPersonas=false] - Whether to highlight personas - either by providing a list of persona keys, or true to highlight all present in current chat
 | 
						|
 * @param {PersonaConnection} [options.targetedChar] - The targeted character or gorup for this persona selection
 | 
						|
 * @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, { okButton = 'None', shiftClickHandler = undefined, highlightPersonas = false, targetedChar = 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('multiline', 'm-b-1');
 | 
						|
    textElement.textContent = text;
 | 
						|
    content.appendChild(textElement);
 | 
						|
 | 
						|
    const personaListBlock = document.createElement('div');
 | 
						|
    personaListBlock.classList.add('persona-list', 'avatars_inline', 'avatars_multiline', 'text_muted');
 | 
						|
    content.appendChild(personaListBlock);
 | 
						|
 | 
						|
    if (personas.length > 0)
 | 
						|
        buildPersonaAvatarList(personaListBlock, personas, { interactable: true });
 | 
						|
    else
 | 
						|
        personaListBlock.textContent = t`[Currently no personas connected]`;
 | 
						|
 | 
						|
    const personasToHighlight = highlightPersonas instanceof Array ? highlightPersonas : (highlightPersonas ? getPersonasOfCurrentChat() : []);
 | 
						|
 | 
						|
    // 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);
 | 
						|
                }
 | 
						|
            });
 | 
						|
        }
 | 
						|
 | 
						|
        if (personasToHighlight && personasToHighlight.includes(block.dataset.pid)) {
 | 
						|
            block.classList.add('is_active');
 | 
						|
            block.title = block.title + '\n\n' + t`Was used in current chat.`;
 | 
						|
            if (block.classList.contains('is_fav')) block.title = block.title + '\n' + t`Is your default persona.`;
 | 
						|
        }
 | 
						|
    });
 | 
						|
 | 
						|
    /** @type {import('./popup.js').CustomPopupButton[]} */
 | 
						|
    const customButtons = [];
 | 
						|
    if (targetedChar) {
 | 
						|
        customButtons.push({
 | 
						|
            text: t`Remove All Connections`,
 | 
						|
            result: 2,
 | 
						|
            action: () => {
 | 
						|
                for (const [personaId, description] of Object.entries(power_user.persona_descriptions)) {
 | 
						|
                    /** @type {PersonaConnection[]} */
 | 
						|
                    const connections = description.connections;
 | 
						|
                    if (connections) {
 | 
						|
                        power_user.persona_descriptions[personaId].connections = connections.filter(c => {
 | 
						|
                            if (targetedChar.type == c.type && targetedChar.id == c.id) return false;
 | 
						|
                            return true;
 | 
						|
                        });
 | 
						|
                    }
 | 
						|
                }
 | 
						|
 | 
						|
                saveSettingsDebounced();
 | 
						|
                updatePersonaConnectionsAvatarList();
 | 
						|
                if (power_user.persona_show_notifications) {
 | 
						|
                    const name = targetedChar.type == 'character' ? characters[targetedChar.id]?.name : groups[targetedChar.id]?.name;
 | 
						|
                    toastr.info(t`All connections to ${name} have been removed.`, t`Personas Unlocked`);
 | 
						|
                }
 | 
						|
            },
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    const popup = new Popup(content, POPUP_TYPE.TEXT, '', { okButton: okButton, customButtons: customButtons });
 | 
						|
    const result = await popup.show();
 | 
						|
    return Number(result) >= 100 ? personas[Number(result) - 100] : null;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Automatically selects a persona based on the given name if a matching persona exists.
 | 
						|
 * @param {string} name - The name to search for
 | 
						|
 * @returns {boolean} True if a matching persona was found and selected, false otherwise
 | 
						|
 */
 | 
						|
export function autoSelectPersona(name) {
 | 
						|
    for (const [key, value] of Object.entries(power_user.personas)) {
 | 
						|
        if (value === name) {
 | 
						|
            console.log(`Auto-selecting persona ${key} for name ${name}`);
 | 
						|
            setUserAvatar(key);
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    return false;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Renames the persona with the given avatar ID by showing a popup to enter a new name.
 | 
						|
 * @param {string} avatarId - ID of the avatar to rename
 | 
						|
 * @returns {Promise<boolean>} A promise that resolves to true if the persona was renamed, false otherwise
 | 
						|
 */
 | 
						|
async function renamePersona(avatarId) {
 | 
						|
    const currentName = power_user.personas[avatarId];
 | 
						|
    const newName = await Popup.show.input(t`Rename Persona`, t`Enter a new name for this persona:`, currentName);
 | 
						|
    if (!newName || newName === currentName) {
 | 
						|
        console.debug('User cancelled renaming persona or name is unchanged');
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    power_user.personas[avatarId] = newName;
 | 
						|
    console.log(`Renamed persona ${avatarId} to ${newName}`);
 | 
						|
 | 
						|
    if (avatarId === user_avatar) {
 | 
						|
        setUserName(newName);
 | 
						|
    }
 | 
						|
 | 
						|
    saveSettingsDebounced();
 | 
						|
    await getUserAvatars(true, avatarId);
 | 
						|
    updatePersonaUIStates();
 | 
						|
    setPersonaDescription();
 | 
						|
    return true;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Selects the persona with the currently set avatar ID by updating the user name and persona description, and updating the locked persona if the setting is enabled.
 | 
						|
 * @param {object} [options={}] - Optional settings
 | 
						|
 * @param {boolean} [options.toastPersonaNameChange=true] - Whether to show a toast when the persona name is changed
 | 
						|
 * @returns {Promise<void>}
 | 
						|
 */
 | 
						|
async function selectCurrentPersona({ toastPersonaNameChange = true } = {}) {
 | 
						|
    const personaName = power_user.personas[user_avatar];
 | 
						|
    if (personaName) {
 | 
						|
        const shouldAutoLock = power_user.persona_auto_lock && user_avatar !== chat_metadata['persona'];
 | 
						|
 | 
						|
        if (personaName !== name1) {
 | 
						|
            console.log(`Auto-updating user name to ${personaName}`);
 | 
						|
            setUserName(personaName, { toastPersonaNameChange: !shouldAutoLock && toastPersonaNameChange });
 | 
						|
        }
 | 
						|
 | 
						|
        const descriptor = power_user.persona_descriptions[user_avatar];
 | 
						|
 | 
						|
        if (descriptor) {
 | 
						|
            power_user.persona_description = descriptor.description ?? '';
 | 
						|
            power_user.persona_description_position = descriptor.position ?? persona_description_positions.IN_PROMPT;
 | 
						|
            power_user.persona_description_depth = descriptor.depth ?? DEFAULT_DEPTH;
 | 
						|
            power_user.persona_description_role = descriptor.role ?? DEFAULT_ROLE;
 | 
						|
            power_user.persona_description_lorebook = descriptor.lorebook ?? '';
 | 
						|
        } else {
 | 
						|
            power_user.persona_description = '';
 | 
						|
            power_user.persona_description_position = persona_description_positions.IN_PROMPT;
 | 
						|
            power_user.persona_description_depth = DEFAULT_DEPTH;
 | 
						|
            power_user.persona_description_role = DEFAULT_ROLE;
 | 
						|
            power_user.persona_description_lorebook = '';
 | 
						|
            power_user.persona_descriptions[user_avatar] = {
 | 
						|
                description: '',
 | 
						|
                position: persona_description_positions.IN_PROMPT,
 | 
						|
                depth: DEFAULT_DEPTH,
 | 
						|
                role: DEFAULT_ROLE,
 | 
						|
                lorebook: '',
 | 
						|
                connections: [],
 | 
						|
            };
 | 
						|
        }
 | 
						|
 | 
						|
        setPersonaDescription();
 | 
						|
 | 
						|
        // Update the locked persona if setting is enabled
 | 
						|
        if (shouldAutoLock) {
 | 
						|
            chat_metadata['persona'] = user_avatar;
 | 
						|
            console.log(`Auto locked persona to ${user_avatar}`);
 | 
						|
            if (toastPersonaNameChange && power_user.persona_show_notifications) {
 | 
						|
                toastr.success(`Persona ${personaName} selected and auto-locked to current chat`, t`Persona Selected`);
 | 
						|
            }
 | 
						|
            saveMetadataDebounced();
 | 
						|
            updatePersonaUIStates();
 | 
						|
        }
 | 
						|
 | 
						|
        // As the last step, inform user if the persona is only temporarily chosen
 | 
						|
        if (power_user.persona_show_notifications && !isPersonaPanelOpen()) {
 | 
						|
            const temporary = getPersonaTemporaryLockInfo();
 | 
						|
            if (temporary.isTemporary) {
 | 
						|
                toastr.info(t`This persona is only temporarily chosen. Click for more info.`, t`Temporary Persona`, {
 | 
						|
                    preventDuplicates: true, onclick: () => {
 | 
						|
                        toastr.info(temporary.info.replaceAll('\n', '<br />'), t`Temporary Persona`, { escapeHtml: false });
 | 
						|
                    },
 | 
						|
                });
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Checks if a connection is locked for the current character or group edit menu
 | 
						|
 * @param {PersonaConnection} connection - Connection to check
 | 
						|
 * @returns {boolean} Whether the connection is locked
 | 
						|
 */
 | 
						|
export function isPersonaConnectionLocked(connection) {
 | 
						|
    return (!selected_group && connection.type === 'character' && connection.id === characters[this_chid]?.avatar)
 | 
						|
        || (selected_group && connection.type === 'group' && connection.id === selected_group);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Checks if the persona is locked
 | 
						|
 * @param {PersonaLockType} type - Lock type
 | 
						|
 * @returns {boolean} Whether the persona is locked
 | 
						|
 */
 | 
						|
export function isPersonaLocked(type = 'chat') {
 | 
						|
    switch (type) {
 | 
						|
        case 'default':
 | 
						|
            return power_user.default_persona === user_avatar;
 | 
						|
        case 'chat':
 | 
						|
            return chat_metadata['persona'] == user_avatar;
 | 
						|
        case 'character': {
 | 
						|
            return !!power_user.persona_descriptions[user_avatar]?.connections?.some(isPersonaConnectionLocked);
 | 
						|
        }
 | 
						|
        default: throw new Error(`Unknown persona lock type: ${type}`);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Locks or unlocks the persona
 | 
						|
 * @param {boolean} state Desired lock state
 | 
						|
 * @param {PersonaLockType} type - Lock type
 | 
						|
 * @returns {Promise<void>}
 | 
						|
 */
 | 
						|
export async function setPersonaLockState(state, type = 'chat') {
 | 
						|
    return state ? await lockPersona(type) : await unlockPersona(type);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Toggle the persona lock state
 | 
						|
 * @param {PersonaLockType} type - Lock type
 | 
						|
 * @returns {Promise<boolean>} - Whether the persona was locked
 | 
						|
 */
 | 
						|
export async function togglePersonaLock(type = 'chat') {
 | 
						|
    if (isPersonaLocked(type)) {
 | 
						|
        await unlockPersona(type);
 | 
						|
        return false;
 | 
						|
    } else {
 | 
						|
        await lockPersona(type);
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Unlock the persona
 | 
						|
 * @param {PersonaLockType} type - Lock type
 | 
						|
 * @returns {Promise<void>}
 | 
						|
 */
 | 
						|
async function unlockPersona(type = 'chat') {
 | 
						|
    switch (type) {
 | 
						|
        case 'default': {
 | 
						|
            // TODO: Make this toggle-able
 | 
						|
            await toggleDefaultPersona(user_avatar, { quiet: true });
 | 
						|
            break;
 | 
						|
        }
 | 
						|
        case 'chat': {
 | 
						|
            if (chat_metadata['persona']) {
 | 
						|
                console.log(`Unlocking persona ${user_avatar} from this chat`);
 | 
						|
                delete chat_metadata['persona'];
 | 
						|
                await saveMetadata();
 | 
						|
                if (power_user.persona_show_notifications && !isPersonaPanelOpen()) {
 | 
						|
                    toastr.info(t`Persona ${name1} is now unlocked from this chat.`, t`Persona Unlocked`);
 | 
						|
                }
 | 
						|
            }
 | 
						|
            break;
 | 
						|
        }
 | 
						|
        case 'character': {
 | 
						|
            /** @type {PersonaConnection[]} */
 | 
						|
            const connections = power_user.persona_descriptions[user_avatar]?.connections;
 | 
						|
            if (connections) {
 | 
						|
                console.log(`Unlocking persona ${user_avatar} from this character ${name2}`);
 | 
						|
                power_user.persona_descriptions[user_avatar].connections = connections.filter(c => !isPersonaConnectionLocked(c));
 | 
						|
                saveSettingsDebounced();
 | 
						|
                updatePersonaConnectionsAvatarList();
 | 
						|
                if (power_user.persona_show_notifications && !isPersonaPanelOpen()) {
 | 
						|
                    toastr.info(t`Persona ${name1} is now unlocked from character ${name2}.`, t`Persona Unlocked`);
 | 
						|
                }
 | 
						|
            }
 | 
						|
            break;
 | 
						|
        }
 | 
						|
        default:
 | 
						|
            throw new Error(`Unknown persona lock type: ${type}`);
 | 
						|
    }
 | 
						|
 | 
						|
    updatePersonaUIStates();
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Lock the persona
 | 
						|
 * @param {PersonaLockType} type - Lock type
 | 
						|
 */
 | 
						|
async function lockPersona(type = 'chat') {
 | 
						|
    // First make sure that user_avatar is actually a persona
 | 
						|
    if (!(user_avatar in power_user.personas)) {
 | 
						|
        console.log(`Creating a new persona ${user_avatar}`);
 | 
						|
        if (power_user.persona_show_notifications) {
 | 
						|
            toastr.info(t`Creating a new persona for currently selected user name and avatar...`, t`Persona Not Found`);
 | 
						|
        }
 | 
						|
        power_user.personas[user_avatar] = name1;
 | 
						|
        power_user.persona_descriptions[user_avatar] = {
 | 
						|
            description: '',
 | 
						|
            position: persona_description_positions.IN_PROMPT,
 | 
						|
            depth: DEFAULT_DEPTH,
 | 
						|
            role: DEFAULT_ROLE,
 | 
						|
            lorebook: '',
 | 
						|
            connections: [],
 | 
						|
        };
 | 
						|
    }
 | 
						|
 | 
						|
    switch (type) {
 | 
						|
        case 'default': {
 | 
						|
            await toggleDefaultPersona(user_avatar, { quiet: true });
 | 
						|
            break;
 | 
						|
        }
 | 
						|
        case 'chat': {
 | 
						|
            console.log(`Locking persona ${user_avatar} to this chat`);
 | 
						|
            chat_metadata['persona'] = user_avatar;
 | 
						|
            saveMetadataDebounced();
 | 
						|
            if (power_user.persona_show_notifications && !isPersonaPanelOpen()) {
 | 
						|
                toastr.success(t`User persona ${name1} is locked to ${name2} in this chat`, t`Persona Locked`);
 | 
						|
            }
 | 
						|
            break;
 | 
						|
        }
 | 
						|
        case 'character': {
 | 
						|
            const newConnection = getCurrentConnectionObj();
 | 
						|
            /** @type {PersonaConnection[]} */
 | 
						|
            const connections = power_user.persona_descriptions[user_avatar].connections?.filter(c => !isPersonaConnectionLocked(c)) ?? [];
 | 
						|
            if (newConnection && newConnection.id) {
 | 
						|
                console.log(`Locking persona ${user_avatar} to this character ${name2}`);
 | 
						|
                power_user.persona_descriptions[user_avatar].connections = [...connections, newConnection];
 | 
						|
 | 
						|
                const unlinkedCharacters = [];
 | 
						|
                if (!power_user.persona_allow_multi_connections) {
 | 
						|
                    for (const [avatarId, description] of Object.entries(power_user.persona_descriptions)) {
 | 
						|
                        if (avatarId === user_avatar) continue;
 | 
						|
 | 
						|
                        const filteredConnections = description.connections?.filter(c => !(c.type === newConnection.type && c.id === newConnection.id)) ?? [];
 | 
						|
                        if (filteredConnections.length !== description.connections?.length) {
 | 
						|
                            description.connections = filteredConnections;
 | 
						|
                            unlinkedCharacters.push(power_user.personas[avatarId]);
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                }
 | 
						|
 | 
						|
                saveSettingsDebounced();
 | 
						|
                updatePersonaConnectionsAvatarList();
 | 
						|
                if (power_user.persona_show_notifications) {
 | 
						|
                    let additional = '';
 | 
						|
                    if (unlinkedCharacters.length)
 | 
						|
                        additional += `<br /><br />${t`Unlinked existing persona${unlinkedCharacters.length > 1 ? 's' : ''}: ${unlinkedCharacters.join(', ')}`}`;
 | 
						|
                    if (additional || !isPersonaPanelOpen()) {
 | 
						|
                        toastr.success(t`User persona ${name1} is locked to character ${name2}${additional}`, t`Persona Locked`, { escapeHtml: false });
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
            break;
 | 
						|
        }
 | 
						|
        default:
 | 
						|
            throw new Error(`Unknown persona lock type: ${type}`);
 | 
						|
    }
 | 
						|
 | 
						|
    updatePersonaUIStates();
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
async function deleteUserAvatar() {
 | 
						|
    const avatarId = user_avatar;
 | 
						|
 | 
						|
    if (!avatarId) {
 | 
						|
        console.warn('No avatar id found');
 | 
						|
        return;
 | 
						|
    }
 | 
						|
    const confirm = await Popup.show.confirm(t`Delete Persona`,
 | 
						|
        t`Are you sure you want to delete this avatar?` + '<br />' + t`All information associated with its linked persona will be lost.`);
 | 
						|
 | 
						|
    if (!confirm) {
 | 
						|
        console.debug('User cancelled deleting avatar');
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const request = await fetch('/api/avatars/delete', {
 | 
						|
        method: 'POST',
 | 
						|
        headers: getRequestHeaders(),
 | 
						|
        body: JSON.stringify({
 | 
						|
            'avatar': avatarId,
 | 
						|
        }),
 | 
						|
    });
 | 
						|
 | 
						|
    if (request.ok) {
 | 
						|
        console.log(`Deleted avatar ${avatarId}`);
 | 
						|
        delete power_user.personas[avatarId];
 | 
						|
        delete power_user.persona_descriptions[avatarId];
 | 
						|
 | 
						|
        if (avatarId === power_user.default_persona) {
 | 
						|
            toastr.warning(t`The default persona was deleted. You will need to set a new default persona.`, t`Default Persona Deleted`);
 | 
						|
            power_user.default_persona = null;
 | 
						|
        }
 | 
						|
 | 
						|
        if (avatarId === chat_metadata['persona']) {
 | 
						|
            toastr.warning(t`The locked persona was deleted. You will need to set a new persona for this chat.`, t`Persona Deleted`);
 | 
						|
            delete chat_metadata['persona'];
 | 
						|
            await saveMetadata();
 | 
						|
        }
 | 
						|
 | 
						|
        saveSettingsDebounced();
 | 
						|
 | 
						|
        // Use the existing mechanism to re-render the persona list and choose the next persona here
 | 
						|
        await loadPersonaForCurrentChat({ doRender: true });
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
function onPersonaDescriptionInput() {
 | 
						|
    power_user.persona_description = String($('#persona_description').val());
 | 
						|
    countPersonaDescriptionTokens();
 | 
						|
 | 
						|
    if (power_user.personas[user_avatar]) {
 | 
						|
        let object = power_user.persona_descriptions[user_avatar];
 | 
						|
 | 
						|
        if (!object) {
 | 
						|
            object = {
 | 
						|
                description: power_user.persona_description,
 | 
						|
                position: Number($('#persona_description_position').find(':selected').val()),
 | 
						|
                depth: Number($('#persona_depth_value').val()),
 | 
						|
                role: Number($('#persona_depth_role').find(':selected').val()),
 | 
						|
                lorebook: '',
 | 
						|
            };
 | 
						|
            power_user.persona_descriptions[user_avatar] = object;
 | 
						|
        }
 | 
						|
 | 
						|
        object.description = power_user.persona_description;
 | 
						|
    }
 | 
						|
 | 
						|
    $(`.avatar-container[imgfile="${user_avatar}"] .ch_description`)
 | 
						|
        .text(power_user.persona_description || $('#user_avatar_block').attr('no_desc_text'))
 | 
						|
        .toggleClass('text_muted', !power_user.persona_description);
 | 
						|
    saveSettingsDebounced();
 | 
						|
}
 | 
						|
 | 
						|
function onPersonaDescriptionDepthValueInput() {
 | 
						|
    power_user.persona_description_depth = Number($('#persona_depth_value').val());
 | 
						|
 | 
						|
    if (power_user.personas[user_avatar]) {
 | 
						|
        const object = getOrCreatePersonaDescriptor();
 | 
						|
        object.depth = power_user.persona_description_depth;
 | 
						|
    }
 | 
						|
 | 
						|
    saveSettingsDebounced();
 | 
						|
}
 | 
						|
 | 
						|
function onPersonaDescriptionDepthRoleInput() {
 | 
						|
    power_user.persona_description_role = Number($('#persona_depth_role').find(':selected').val());
 | 
						|
 | 
						|
    if (power_user.personas[user_avatar]) {
 | 
						|
        const object = getOrCreatePersonaDescriptor();
 | 
						|
        object.role = power_user.persona_description_role;
 | 
						|
    }
 | 
						|
 | 
						|
    saveSettingsDebounced();
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Opens a popup to set the lorebook for the current persona.
 | 
						|
 * @param {JQuery.ClickEvent} event Click event
 | 
						|
 */
 | 
						|
async function onPersonaLoreButtonClick(event) {
 | 
						|
    const personaName = power_user.personas[user_avatar];
 | 
						|
    const selectedLorebook = power_user.persona_description_lorebook;
 | 
						|
 | 
						|
    if (!personaName) {
 | 
						|
        toastr.warning(t`You must bind a name to this persona before you can set a lorebook.`, t`Persona Name Not Set`);
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (event.altKey && selectedLorebook) {
 | 
						|
        openWorldInfoEditor(selectedLorebook);
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const template = $(await renderTemplateAsync('personaLorebook'));
 | 
						|
 | 
						|
    const worldSelect = template.find('select');
 | 
						|
    template.find('.persona_name').text(personaName);
 | 
						|
 | 
						|
    for (const worldName of world_names) {
 | 
						|
        const option = document.createElement('option');
 | 
						|
        option.value = worldName;
 | 
						|
        option.innerText = worldName;
 | 
						|
        option.selected = selectedLorebook === worldName;
 | 
						|
        worldSelect.append(option);
 | 
						|
    }
 | 
						|
 | 
						|
    worldSelect.on('change', function () {
 | 
						|
        power_user.persona_description_lorebook = String($(this).val());
 | 
						|
 | 
						|
        if (power_user.personas[user_avatar]) {
 | 
						|
            const object = getOrCreatePersonaDescriptor();
 | 
						|
            object.lorebook = power_user.persona_description_lorebook;
 | 
						|
        }
 | 
						|
 | 
						|
        $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook);
 | 
						|
        saveSettingsDebounced();
 | 
						|
    });
 | 
						|
 | 
						|
    await callGenericPopup(template, POPUP_TYPE.TEXT);
 | 
						|
}
 | 
						|
 | 
						|
function onPersonaDescriptionPositionInput() {
 | 
						|
    power_user.persona_description_position = Number(
 | 
						|
        $('#persona_description_position').find(':selected').val(),
 | 
						|
    );
 | 
						|
 | 
						|
    if (power_user.personas[user_avatar]) {
 | 
						|
        const object = getOrCreatePersonaDescriptor();
 | 
						|
        object.position = power_user.persona_description_position;
 | 
						|
    }
 | 
						|
 | 
						|
    saveSettingsDebounced();
 | 
						|
    $('#persona_depth_position_settings').toggle(power_user.persona_description_position === persona_description_positions.AT_DEPTH);
 | 
						|
}
 | 
						|
 | 
						|
function getOrCreatePersonaDescriptor() {
 | 
						|
    let object = power_user.persona_descriptions[user_avatar];
 | 
						|
 | 
						|
    if (!object) {
 | 
						|
        object = {
 | 
						|
            description: power_user.persona_description,
 | 
						|
            position: power_user.persona_description_position,
 | 
						|
            depth: power_user.persona_description_depth,
 | 
						|
            role: power_user.persona_description_role,
 | 
						|
            lorebook: power_user.persona_description_lorebook,
 | 
						|
            connections: [],
 | 
						|
        };
 | 
						|
        power_user.persona_descriptions[user_avatar] = object;
 | 
						|
    }
 | 
						|
    return object;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Sets a persona as the default one to be used for all new chats and unlocked existing chats
 | 
						|
 * @param {string} avatarId The avatar id of the persona to set as the default
 | 
						|
 * @param {object} [options] Optional arguments
 | 
						|
 * @param {boolean} [options.quiet=false] If true, no confirmation popups will be shown
 | 
						|
 * @returns {Promise<void>}
 | 
						|
 */
 | 
						|
async function toggleDefaultPersona(avatarId, { quiet = false } = {}) {
 | 
						|
    if (!avatarId) {
 | 
						|
        console.warn('No avatar id found');
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const currentDefault = power_user.default_persona;
 | 
						|
 | 
						|
    if (power_user.personas[avatarId] === undefined) {
 | 
						|
        console.warn(`No persona name found for avatar ${avatarId}`);
 | 
						|
        toastr.warning(t`You must bind a name to this persona before you can set it as the default.`, t`Persona Name Not Set`);
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
    if (avatarId === currentDefault) {
 | 
						|
        if (!quiet) {
 | 
						|
            const confirm = await Popup.show.confirm(t`Are you sure you want to remove the default persona?`, power_user.personas[avatarId]);
 | 
						|
            if (!confirm) {
 | 
						|
                console.debug('User cancelled removing default persona');
 | 
						|
                return;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        console.log(`Removing default persona ${avatarId}`);
 | 
						|
        if (power_user.persona_show_notifications && !isPersonaPanelOpen()) {
 | 
						|
            toastr.info(t`This persona will no longer be used by default when you open a new chat.`, t`Default Persona Removed`);
 | 
						|
        }
 | 
						|
        delete power_user.default_persona;
 | 
						|
    } else {
 | 
						|
        if (!quiet) {
 | 
						|
            const confirm = await Popup.show.confirm(t`Set Default Persona`,
 | 
						|
                t`Are you sure you want to set \"${power_user.personas[avatarId]}\" as the default persona?`
 | 
						|
                + '<br /><br />'
 | 
						|
                + t`This name and avatar will be used for all new chats, as well as existing chats where the user persona is not locked.`);
 | 
						|
            if (!confirm) {
 | 
						|
                console.debug('User cancelled setting default persona');
 | 
						|
                return;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        power_user.default_persona = avatarId;
 | 
						|
        if (power_user.persona_show_notifications && !isPersonaPanelOpen()) {
 | 
						|
            toastr.success(t`Set to ${power_user.personas[avatarId]}.This persona will be used by default when you open a new chat.`, t`Default Persona`);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    saveSettingsDebounced();
 | 
						|
    await getUserAvatars(true, avatarId);
 | 
						|
    updatePersonaUIStates();
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns an object with 3 properties that describe the state of the given persona
 | 
						|
 *
 | 
						|
 * - default: Whether this persona is the default one for all new chats
 | 
						|
 * - locked: An object containing the lock states
 | 
						|
 *   - chat: Whether the persona is locked to the currently open chat
 | 
						|
 *   - character: Whether the persona is locked to the currently open character or group
 | 
						|
 * @param {string} avatarId - The avatar id of the persona to get the state for
 | 
						|
 * @returns {PersonaState} An object describing the state of the given persona
 | 
						|
 */
 | 
						|
function getPersonaStates(avatarId) {
 | 
						|
    const isDefaultPersona = power_user.default_persona === avatarId;
 | 
						|
    const hasChatLock = chat_metadata['persona'] == avatarId;
 | 
						|
 | 
						|
    /** @type {PersonaConnection[]} */
 | 
						|
    const connections = power_user.persona_descriptions[avatarId]?.connections;
 | 
						|
    const hasCharLock = !!connections?.some(c =>
 | 
						|
        (!selected_group && c.type === 'character' && c.id === characters[this_chid]?.avatar)
 | 
						|
        || (selected_group && c.type === 'group' && c.id === selected_group));
 | 
						|
 | 
						|
    return {
 | 
						|
        avatarId: avatarId,
 | 
						|
        default: isDefaultPersona,
 | 
						|
        locked: {
 | 
						|
            chat: hasChatLock,
 | 
						|
            character: hasCharLock,
 | 
						|
        },
 | 
						|
    };
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Updates the UI to reflect the current states of all personas and the selected user's persona.
 | 
						|
 * This includes updating class states on avatar containers to indicate default status, chat lock,
 | 
						|
 * and character lock, as well as updating icons and labels in the persona management panel to reflect
 | 
						|
 * the current state of the user's persona.
 | 
						|
 * Additionally, it manages the display of temporary persona lock information.
 | 
						|
 * @param {Object} [options={}] - Optional settings
 | 
						|
 * @param {boolean} [options.navigateToCurrent=false] - Whether to navigate to the current persona in the persona list
 | 
						|
 */
 | 
						|
 | 
						|
function updatePersonaUIStates({ navigateToCurrent = false } = {}) {
 | 
						|
    if (navigateToCurrent) {
 | 
						|
        navigateToAvatar(user_avatar);
 | 
						|
    }
 | 
						|
 | 
						|
    // Update the persona list
 | 
						|
    $('#user_avatar_block .avatar-container').each(function () {
 | 
						|
        const avatarId = $(this).attr('data-avatar-id');
 | 
						|
        const states = getPersonaStates(avatarId);
 | 
						|
        $(this).toggleClass('default_persona', states.default);
 | 
						|
        $(this).toggleClass('locked_to_chat', states.locked.chat);
 | 
						|
        $(this).toggleClass('locked_to_character', states.locked.character);
 | 
						|
        $(this).toggleClass('selected', avatarId === user_avatar);
 | 
						|
    });
 | 
						|
 | 
						|
    // Buttons for the persona panel on the right
 | 
						|
    const personaStates = getPersonaStates(user_avatar);
 | 
						|
 | 
						|
    $('#lock_persona_default').toggleClass('locked', personaStates.default);
 | 
						|
 | 
						|
    $('#lock_user_name').toggleClass('locked', personaStates.locked.chat);
 | 
						|
    $('#lock_user_name i.icon').toggleClass('fa-lock', personaStates.locked.chat);
 | 
						|
    $('#lock_user_name i.icon').toggleClass('fa-unlock', !personaStates.locked.chat);
 | 
						|
 | 
						|
    $('#lock_persona_to_char').toggleClass('locked', personaStates.locked.character);
 | 
						|
    $('#lock_persona_to_char i.icon').toggleClass('fa-lock', personaStates.locked.character);
 | 
						|
    $('#lock_persona_to_char i.icon').toggleClass('fa-unlock', !personaStates.locked.character);
 | 
						|
 | 
						|
    // Persona panel info block
 | 
						|
    const { isTemporary, info } = getPersonaTemporaryLockInfo();
 | 
						|
    if (isTemporary) {
 | 
						|
        const messageContainer = document.createElement('div');
 | 
						|
        const messageSpan = document.createElement('span');
 | 
						|
        messageSpan.textContent = t`Temporary persona in use.`;
 | 
						|
        messageContainer.appendChild(messageSpan);
 | 
						|
        messageContainer.classList.add('flex-container', 'alignItemsBaseline');
 | 
						|
 | 
						|
        const infoIcon = document.createElement('i');
 | 
						|
        infoIcon.classList.add('fa-solid', 'fa-circle-info', 'opacity50p');
 | 
						|
        infoIcon.title = info;
 | 
						|
        messageContainer.appendChild(infoIcon);
 | 
						|
 | 
						|
        // Set the info block content
 | 
						|
        setInfoBlock('#persona_connections_info_block', messageContainer, 'hint');
 | 
						|
    } else {
 | 
						|
        // Clear the info block if no condition applies
 | 
						|
        clearInfoBlock('#persona_connections_info_block');
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @typedef {Object} PersonaLockInfo
 | 
						|
 * @property {boolean} isTemporary - Whether the selected persona is temporary based on current locks.
 | 
						|
 * @property {boolean} hasDifferentChatLock - True if the chat persona is set and differs from the user avatar.
 | 
						|
 * @property {boolean} hasDifferentDefaultLock - True if the default persona is set and differs from the user avatar.
 | 
						|
 * @property {string} info - Detailed information about the current, chat, and default personas.
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * Computes temporary lock information for the current persona.
 | 
						|
 *
 | 
						|
 * This function checks whether the currently selected persona is temporary by comparing
 | 
						|
 * the chat persona and the default persona to the user avatar. If either is different,
 | 
						|
 * the currently selected persona is considered temporary and a detailed message is generated.
 | 
						|
 *
 | 
						|
 * @returns {PersonaLockInfo} An object containing flags and a message describing the persona lock status.
 | 
						|
 */
 | 
						|
function getPersonaTemporaryLockInfo() {
 | 
						|
    const hasDifferentChatLock = !!chat_metadata['persona'] && chat_metadata['persona'] !== user_avatar;
 | 
						|
    const hasDifferentDefaultLock = power_user.default_persona && power_user.default_persona !== user_avatar;
 | 
						|
    const isTemporary = hasDifferentChatLock || (!chat_metadata['persona'] && hasDifferentDefaultLock);
 | 
						|
    const info = isTemporary ? t`A different persona is locked to this chat, or you have a different default persona set. The currently selected persona will only be temporary, and resets on reload. Consider locking this persona to the chat if you want to permanently use it.`
 | 
						|
        + '\n\n'
 | 
						|
        + t`Current Persona: ${power_user.personas[user_avatar]}`
 | 
						|
        + (hasDifferentChatLock ? '\n' + t`Chat persona: ${power_user.personas[chat_metadata['persona']]}` : '')
 | 
						|
        + (hasDifferentDefaultLock ? '\n' + t`Default persona: ${power_user.personas[power_user.default_persona]}` : '') : '';
 | 
						|
 | 
						|
    return {
 | 
						|
        isTemporary: isTemporary,
 | 
						|
        hasDifferentChatLock: hasDifferentChatLock,
 | 
						|
        hasDifferentDefaultLock: hasDifferentDefaultLock,
 | 
						|
        info: info,
 | 
						|
    };
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Loads the appropriate persona for the current chat session based on locks (chat lock, char lock, default persona)
 | 
						|
 *
 | 
						|
 * @param {Object} [options={}] - Optional arguments
 | 
						|
 * @param {boolean} [options.doRender=false] - Whether to render the persona immediately
 | 
						|
 * @returns {Promise<boolean>} - A promise that resolves to a boolean indicating whether a persona was selected
 | 
						|
 */
 | 
						|
async function loadPersonaForCurrentChat({ doRender = false } = {}) {
 | 
						|
    // Cache persona list to check if they exist
 | 
						|
    const userAvatars = await getUserAvatars(doRender);
 | 
						|
 | 
						|
    // Define a persona for this chat
 | 
						|
    let chatPersona = '';
 | 
						|
 | 
						|
    /** @type {'chat' | 'character' | 'default' | null} */
 | 
						|
    let connectType = null;
 | 
						|
 | 
						|
    // If persona is locked in chat metadata, select it
 | 
						|
    if (chat_metadata['persona']) {
 | 
						|
        console.log(`Using locked persona ${chat_metadata['persona']}`);
 | 
						|
        chatPersona = chat_metadata['persona'];
 | 
						|
 | 
						|
        // Verify it exists
 | 
						|
        if (!userAvatars.includes(chatPersona)) {
 | 
						|
            console.warn('Chat-locked persona avatar not found, unlocking persona');
 | 
						|
            delete chat_metadata['persona'];
 | 
						|
            saveSettingsDebounced();
 | 
						|
            chatPersona = '';
 | 
						|
        }
 | 
						|
        if (chatPersona) connectType = 'chat';
 | 
						|
    }
 | 
						|
 | 
						|
    // If the persona panel is open when the chat changes, this is likely because a character was selected from that panel.
 | 
						|
    // In that case, we are not automatically switching persona - but need to make changes if there is any chat-bound connection
 | 
						|
    /*
 | 
						|
    if (isPersonaPanelOpen()) {
 | 
						|
        if (chatPersona) {
 | 
						|
            // If the chat-bound persona is the currently selected one, we can simply exit out
 | 
						|
            if (chatPersona === user_avatar) {
 | 
						|
                return false;
 | 
						|
            }
 | 
						|
            // Otherwise ask if we want to switch
 | 
						|
            const autoLock = power_user.persona_auto_lock;
 | 
						|
            const result = await Popup.show.confirm(t`Switch Persona?`,
 | 
						|
                t`You have a connected persona for the current chat (${power_user.personas[chatPersona]}). Do you want to stick to the current persona (${power_user.personas[user_avatar]}) ${(autoLock ? t`and lock that to the chat` : '')}, or switch to ${power_user.personas[chatPersona]} instead?`,
 | 
						|
                { okButton: autoLock ? t`Keep and Lock` : t`Keep`, cancelButton: t`Switch` });
 | 
						|
            if (result === POPUP_RESULT.AFFIRMATIVE) {
 | 
						|
                if (autoLock) {
 | 
						|
                    lockPersona('chat');
 | 
						|
                }
 | 
						|
                return false;
 | 
						|
            }
 | 
						|
        } else {
 | 
						|
            // If we don't have a chat-bound persona, we simply return and keep the current one we have
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    */
 | 
						|
 | 
						|
    // Check if we have any persona connected to the current character
 | 
						|
    if (!chatPersona) {
 | 
						|
        const connectedPersonas = getConnectedPersonas();
 | 
						|
 | 
						|
        if (connectedPersonas.length > 0) {
 | 
						|
            if (connectedPersonas.length === 1) {
 | 
						|
                chatPersona = connectedPersonas[0];
 | 
						|
            } else if (!power_user.persona_allow_multi_connections) {
 | 
						|
                console.warn('More than one persona is connected to this character.Using the first available persona for this chat.');
 | 
						|
                chatPersona = connectedPersonas[0];
 | 
						|
            } else {
 | 
						|
                chatPersona = await askForPersonaSelection(t`Select Persona`,
 | 
						|
                    t`Multiple personas are connected to this character.\nSelect a persona to use for this chat.`,
 | 
						|
                    connectedPersonas, { highlightPersonas: true, targetedChar: getCurrentConnectionObj() });
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (chatPersona) connectType = 'character';
 | 
						|
    }
 | 
						|
 | 
						|
    // Last check if default persona is set, select it
 | 
						|
    if (!chatPersona && power_user.default_persona) {
 | 
						|
        console.log(`Using default persona ${power_user.default_persona}`);
 | 
						|
        chatPersona = power_user.default_persona;
 | 
						|
 | 
						|
        if (chatPersona) connectType = 'default';
 | 
						|
    }
 | 
						|
 | 
						|
    // Whatever way we selected a persona, if it doesn't exist, unlock this chat
 | 
						|
    if (chat_metadata['persona'] && !userAvatars.includes(chat_metadata['persona'])) {
 | 
						|
        console.warn('Persona avatar not found, unlocking persona');
 | 
						|
        delete chat_metadata['persona'];
 | 
						|
    }
 | 
						|
 | 
						|
    // Default persona missing
 | 
						|
    if (power_user.default_persona && !userAvatars.includes(power_user.default_persona)) {
 | 
						|
        console.warn('Default persona avatar not found, clearing default persona');
 | 
						|
        power_user.default_persona = null;
 | 
						|
        saveSettingsDebounced();
 | 
						|
    }
 | 
						|
 | 
						|
    // Persona avatar found, select it
 | 
						|
    if (chatPersona && user_avatar !== chatPersona) {
 | 
						|
        const willAutoLock = power_user.persona_auto_lock && user_avatar !== chat_metadata['persona'];
 | 
						|
        setUserAvatar(chatPersona, { toastPersonaNameChange: false, navigateToCurrent: true });
 | 
						|
 | 
						|
        if (power_user.persona_show_notifications) {
 | 
						|
            let message = t`Auto-selected persona based on ${connectType} connection.<br />Your messages will now be sent as ${power_user.personas[chatPersona]}.`;
 | 
						|
            if (willAutoLock) {
 | 
						|
                message += '<br /><br />' + t`Auto-locked this persona to current chat.`;
 | 
						|
            }
 | 
						|
            toastr.success(message, t`Persona Auto Selected`, { escapeHtml: false });
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    updatePersonaUIStates();
 | 
						|
 | 
						|
    return !!chatPersona;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * 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 ??= 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;
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
/**
 | 
						|
 * Shows a popup with all personas connected to the currently selected character or group.
 | 
						|
 * In the popup, the user can select a persona to load for the current character or group, or shift-click to remove the connection.
 | 
						|
 * @return {Promise<void>}
 | 
						|
 */
 | 
						|
export async function showCharConnections() {
 | 
						|
    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`,
 | 
						|
        highlightPersonas: true,
 | 
						|
        targetedChar: getCurrentConnectionObj(),
 | 
						|
        shiftClickHandler: (element, ev) => {
 | 
						|
 | 
						|
            const personaId = $(element).attr('data-pid');
 | 
						|
 | 
						|
            /** @type {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, { toastPersonaNameChange: false });
 | 
						|
        if (power_user.persona_show_notifications) {
 | 
						|
            toastr.success(t`Selected persona ${power_user.personas[selectedPersona]} for current chat.`, t`Connected Persona Selected`);
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Retrieves the current connection object based on whether the current chat is with a char or a group.
 | 
						|
 *
 | 
						|
 * @returns {PersonaConnection} An object representing the current connection
 | 
						|
 */
 | 
						|
export function getCurrentConnectionObj() {
 | 
						|
    if (selected_group)
 | 
						|
        return { type: 'group', id: selected_group };
 | 
						|
    if (characters[this_chid]?.avatar)
 | 
						|
        return { type: 'character', id: characters[this_chid]?.avatar };
 | 
						|
    return null;
 | 
						|
}
 | 
						|
 | 
						|
function onBackupPersonas() {
 | 
						|
    const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, '');
 | 
						|
    const filename = `personas_${timestamp}.json`;
 | 
						|
    const data = JSON.stringify({
 | 
						|
        'personas': power_user.personas,
 | 
						|
        'persona_descriptions': power_user.persona_descriptions,
 | 
						|
        'default_persona': power_user.default_persona,
 | 
						|
    }, null, 2);
 | 
						|
 | 
						|
    const blob = new Blob([data], { type: 'application/json' });
 | 
						|
    download(blob, filename, 'application/json');
 | 
						|
}
 | 
						|
 | 
						|
async function onPersonasRestoreInput(e) {
 | 
						|
    const file = e.target.files[0];
 | 
						|
 | 
						|
    if (!file) {
 | 
						|
        console.debug('No file selected');
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const data = await parseJsonFile(file);
 | 
						|
 | 
						|
    if (!data) {
 | 
						|
        toastr.warning(t`Invalid file selected`, t`Persona Management`);
 | 
						|
        console.debug('Invalid file selected');
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (!data.personas || !data.persona_descriptions || typeof data.personas !== 'object' || typeof data.persona_descriptions !== 'object') {
 | 
						|
        toastr.warning(t`Invalid file format`, t`Persona Management`);
 | 
						|
        console.debug('Invalid file selected');
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const avatarsList = await getUserAvatars(false);
 | 
						|
    const warnings = [];
 | 
						|
 | 
						|
    // Merge personas with existing ones
 | 
						|
    for (const [key, value] of Object.entries(data.personas)) {
 | 
						|
        if (key in power_user.personas) {
 | 
						|
            warnings.push(`Persona "${key}" (${value}) already exists, skipping`);
 | 
						|
            continue;
 | 
						|
        }
 | 
						|
 | 
						|
        power_user.personas[key] = value;
 | 
						|
 | 
						|
        // If the avatar is missing, upload it
 | 
						|
        if (!avatarsList.includes(key)) {
 | 
						|
            warnings.push(`Persona image "${key}" (${value}) is missing, uploading default avatar`);
 | 
						|
            await uploadUserAvatar(default_user_avatar, key);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // Merge persona descriptions with existing ones
 | 
						|
    for (const [key, value] of Object.entries(data.persona_descriptions)) {
 | 
						|
        if (key in power_user.persona_descriptions) {
 | 
						|
            warnings.push(`Persona description for "${key}" (${power_user.personas[key]}) already exists, skipping`);
 | 
						|
            continue;
 | 
						|
        }
 | 
						|
 | 
						|
        if (!power_user.personas[key]) {
 | 
						|
            warnings.push(`Persona for "${key}" does not exist, skipping`);
 | 
						|
            continue;
 | 
						|
        }
 | 
						|
 | 
						|
        power_user.persona_descriptions[key] = value;
 | 
						|
    }
 | 
						|
 | 
						|
    if (data.default_persona) {
 | 
						|
        if (data.default_persona in power_user.personas) {
 | 
						|
            power_user.default_persona = data.default_persona;
 | 
						|
        } else {
 | 
						|
            warnings.push(`Default persona "${data.default_persona}" does not exist, skipping`);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    if (warnings.length) {
 | 
						|
        toastr.success(t`Personas restored with warnings. Check console for details.`, t`Persona Management`);
 | 
						|
        console.warn(`PERSONA RESTORE REPORT\n====================\n${warnings.join('\n')}`);
 | 
						|
    } else {
 | 
						|
        toastr.success(t`Personas restored successfully.`, t`Persona Management`);
 | 
						|
    }
 | 
						|
 | 
						|
    await getUserAvatars();
 | 
						|
    setPersonaDescription();
 | 
						|
    saveSettingsDebounced();
 | 
						|
    $('#personas_restore_input').val('');
 | 
						|
}
 | 
						|
 | 
						|
async function syncUserNameToPersona() {
 | 
						|
    const confirmation = await Popup.show.confirm(t`Are you sure?`, t`All user-sent messages in this chat will be attributed to ${name1}.`);
 | 
						|
 | 
						|
    if (!confirmation) {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    for (const mes of chat) {
 | 
						|
        if (mes.is_user) {
 | 
						|
            mes.name = name1;
 | 
						|
            mes.force_avatar = getUserAvatar(user_avatar);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    await saveChatConditional();
 | 
						|
    await reloadCurrentChat();
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Retriggers the first message to reload it from the char definition.
 | 
						|
 *
 | 
						|
 * Only works if only the first message is present, and not in group mode.
 | 
						|
 */
 | 
						|
export function retriggerFirstMessageOnEmptyChat() {
 | 
						|
    if (this_chid >= 0 && !selected_group && chat.length === 1) {
 | 
						|
        $('#firstmessage_textarea').trigger('input');
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Duplicates a persona.
 | 
						|
 * @param {string} avatarId
 | 
						|
 * @returns {Promise<void>}
 | 
						|
 */
 | 
						|
async function duplicatePersona(avatarId) {
 | 
						|
    const personaName = power_user.personas[avatarId];
 | 
						|
 | 
						|
    if (!personaName) {
 | 
						|
        toastr.warning('Chosen avatar is not a persona', t`Persona Management`);
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const confirm = await Popup.show.confirm(t`Are you sure you want to duplicate this persona?`, personaName);
 | 
						|
 | 
						|
    if (!confirm) {
 | 
						|
        console.debug('User cancelled duplicating persona');
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const newAvatarId = `${Date.now()}-${personaName.replace(/[^a-zA-Z0-9]/g, '')}.png`;
 | 
						|
    const descriptor = power_user.persona_descriptions[avatarId];
 | 
						|
 | 
						|
    power_user.personas[newAvatarId] = personaName;
 | 
						|
    power_user.persona_descriptions[newAvatarId] = {
 | 
						|
        description: descriptor?.description ?? '',
 | 
						|
        position: descriptor?.position ?? persona_description_positions.IN_PROMPT,
 | 
						|
        depth: descriptor?.depth ?? DEFAULT_DEPTH,
 | 
						|
        role: descriptor?.role ?? DEFAULT_ROLE,
 | 
						|
        lorebook: descriptor?.lorebook ?? '',
 | 
						|
    };
 | 
						|
 | 
						|
    await uploadUserAvatar(getUserAvatar(avatarId), newAvatarId);
 | 
						|
    await getUserAvatars(true, newAvatarId);
 | 
						|
    saveSettingsDebounced();
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * If a current user avatar is not bound to persona, bind it.
 | 
						|
 */
 | 
						|
async function migrateNonPersonaUser() {
 | 
						|
    if (user_avatar in power_user.personas) {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    initPersona(user_avatar, name1, '');
 | 
						|
    setPersonaDescription();
 | 
						|
    await getUserAvatars(true, user_avatar);
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
/**
 | 
						|
 * Locks or unlocks the persona of the current chat.
 | 
						|
 * @param {{type: string}} _args Named arguments
 | 
						|
 * @param {string} value The value to set the lock to
 | 
						|
 * @returns {Promise<string>} The value of the lock after setting
 | 
						|
 */
 | 
						|
async function lockPersonaCallback(_args, value) {
 | 
						|
    const type = /** @type {PersonaLockType} */ (_args.type ?? 'chat');
 | 
						|
 | 
						|
    if (!['chat', 'character', 'default'].includes(type)) {
 | 
						|
        toastr.warning(t`Unknown lock type "${type}"`, t`Persona Management`);
 | 
						|
        return '';
 | 
						|
    }
 | 
						|
 | 
						|
    if (!value) {
 | 
						|
        return String(isPersonaLocked(type));
 | 
						|
    }
 | 
						|
 | 
						|
    if (['toggle', 't'].includes(value.trim().toLowerCase())) {
 | 
						|
        const result = await togglePersonaLock(type);
 | 
						|
        return String(result);
 | 
						|
    }
 | 
						|
 | 
						|
    if (isTrueBoolean(value)) {
 | 
						|
        await setPersonaLockState(true, type);
 | 
						|
        return 'true';
 | 
						|
    }
 | 
						|
 | 
						|
    if (isFalseBoolean(value)) {
 | 
						|
        await setPersonaLockState(false, type);
 | 
						|
        return 'false';
 | 
						|
 | 
						|
    }
 | 
						|
 | 
						|
    return '';
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Sets a persona name and optionally an avatar.
 | 
						|
 * @param {{mode: 'lookup' | 'temp' | 'all'}} namedArgs Named arguments
 | 
						|
 * @param {string} name Name to set
 | 
						|
 * @returns {string}
 | 
						|
 */
 | 
						|
function setNameCallback({ mode = 'all' }, name) {
 | 
						|
    if (!name) {
 | 
						|
        toastr.warning('You must specify a name to change to');
 | 
						|
        return '';
 | 
						|
    }
 | 
						|
 | 
						|
    if (!['lookup', 'temp', 'all'].includes(mode)) {
 | 
						|
        toastr.warning('Mode must be one of "lookup", "temp" or "all"');
 | 
						|
        return '';
 | 
						|
    }
 | 
						|
 | 
						|
    name = name.trim();
 | 
						|
 | 
						|
    // If the name matches a persona avatar, or a name, auto-select it
 | 
						|
    if (['lookup', 'all'].includes(mode)) {
 | 
						|
        let persona = Object.entries(power_user.personas).find(([avatar, _]) => avatar === name)?.[1];
 | 
						|
        if (!persona) persona = Object.entries(power_user.personas).find(([_, personaName]) => personaName.toLowerCase() === name.toLowerCase())?.[1];
 | 
						|
        if (persona) {
 | 
						|
            autoSelectPersona(persona);
 | 
						|
            retriggerFirstMessageOnEmptyChat();
 | 
						|
            return '';
 | 
						|
        } else if (mode === 'lookup') {
 | 
						|
            toastr.warning(`Persona ${name} not found`);
 | 
						|
            return '';
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    if (['temp', 'all'].includes(mode)) {
 | 
						|
        // Otherwise, set just the name
 | 
						|
        setUserName(name); //this prevented quickReply usage
 | 
						|
        retriggerFirstMessageOnEmptyChat();
 | 
						|
    }
 | 
						|
 | 
						|
    return '';
 | 
						|
}
 | 
						|
 | 
						|
function syncCallback() {
 | 
						|
    $('#sync_name_button').trigger('click');
 | 
						|
    return '';
 | 
						|
}
 | 
						|
 | 
						|
function registerPersonaSlashCommands() {
 | 
						|
    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
 | 
						|
        name: 'persona-lock',
 | 
						|
        callback: lockPersonaCallback,
 | 
						|
        returns: 'The current lock state for the given type',
 | 
						|
        helpString: 'Locks/unlocks a persona (name and avatar) to the current chat. Gets the current lock state for the given type if no state is provided.',
 | 
						|
        namedArgumentList: [
 | 
						|
            SlashCommandNamedArgument.fromProps({
 | 
						|
                name: 'type',
 | 
						|
                description: 'The type of the lock, where it should apply to',
 | 
						|
                typeList: [ARGUMENT_TYPE.STRING],
 | 
						|
                defaultValue: 'chat',
 | 
						|
                enumList: [
 | 
						|
                    new SlashCommandEnumValue('chat', 'Lock the persona to the current chat.'),
 | 
						|
                    new SlashCommandEnumValue('character', 'Lock this persona to the currently selected character. If the setting is enabled, mutliple personas can be locked to the same character.'),
 | 
						|
                    new SlashCommandEnumValue('default', 'Lock this persona as the default persona for all new chats.'),
 | 
						|
                ],
 | 
						|
            }),
 | 
						|
        ],
 | 
						|
        unnamedArgumentList: [
 | 
						|
            SlashCommandArgument.fromProps({
 | 
						|
                description: 'state',
 | 
						|
                typeList: [ARGUMENT_TYPE.STRING],
 | 
						|
                enumProvider: commonEnumProviders.boolean('onOffToggle'),
 | 
						|
            }),
 | 
						|
        ],
 | 
						|
    }));
 | 
						|
    // TODO: Legacy command. Might be removed in the future and replaced by /persona-lock with aliases.
 | 
						|
    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
 | 
						|
        name: 'lock',
 | 
						|
        /** @type {(args: { type: string }, value: string) => Promise<string>} */
 | 
						|
        callback: (args, value) => {
 | 
						|
            if (!value) {
 | 
						|
                value = 'toggle';
 | 
						|
                toastr.warning(t`Using /lock without a provided state to toggle the persona is deprecated. Please use /persona-lock instead.
 | 
						|
                        In the future this command with no state provided will return the current state, instead of toggling it.`, t`Deprecation Warning`);
 | 
						|
            }
 | 
						|
            return lockPersonaCallback(args, value);
 | 
						|
        },
 | 
						|
        returns: 'The current lock state for the given type',
 | 
						|
        aliases: ['bind'],
 | 
						|
        helpString: 'Locks/unlocks a persona (name and avatar) to the current chat. Gets the current lock state for the given type if no state is provided.',
 | 
						|
        namedArgumentList: [
 | 
						|
            SlashCommandNamedArgument.fromProps({
 | 
						|
                name: 'type',
 | 
						|
                description: 'The type of the lock, where it should apply to',
 | 
						|
                typeList: [ARGUMENT_TYPE.STRING],
 | 
						|
                defaultValue: 'chat',
 | 
						|
                enumList: [
 | 
						|
                    new SlashCommandEnumValue('chat', 'Lock the persona to the current chat.'),
 | 
						|
                    new SlashCommandEnumValue('character', 'Lock this persona to the currently selected character. If the setting is enabled, mutliple personas can be locked to the same character.'),
 | 
						|
                    new SlashCommandEnumValue('default', 'Lock this persona as the default persona for all new chats.'),
 | 
						|
                ],
 | 
						|
            }),
 | 
						|
        ],
 | 
						|
        unnamedArgumentList: [
 | 
						|
            SlashCommandArgument.fromProps({
 | 
						|
                description: 'state',
 | 
						|
                typeList: [ARGUMENT_TYPE.STRING],
 | 
						|
                defaultValue: 'toggle',
 | 
						|
                enumProvider: commonEnumProviders.boolean('onOffToggle'),
 | 
						|
            }),
 | 
						|
        ],
 | 
						|
    }));
 | 
						|
    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
 | 
						|
        name: 'persona-set',
 | 
						|
        callback: setNameCallback,
 | 
						|
        aliases: ['persona', 'name'],
 | 
						|
        namedArgumentList: [
 | 
						|
            new SlashCommandNamedArgument(
 | 
						|
                'mode', 'The mode for persona selection. ("lookup" = search for existing persona, "temp" = create a temporary name, set a temporary name, "all" = allow both in the same command)',
 | 
						|
                [ARGUMENT_TYPE.STRING], false, false, 'all', ['lookup', 'temp', 'all'],
 | 
						|
            ),
 | 
						|
        ],
 | 
						|
        unnamedArgumentList: [
 | 
						|
            SlashCommandArgument.fromProps({
 | 
						|
                description: 'persona name',
 | 
						|
                typeList: [ARGUMENT_TYPE.STRING],
 | 
						|
                isRequired: true,
 | 
						|
                enumProvider: commonEnumProviders.personas,
 | 
						|
            }),
 | 
						|
        ],
 | 
						|
        helpString: 'Selects the given persona with its name and avatar (by name or avatar url). If no matching persona exists, applies a temporary name.',
 | 
						|
    }));
 | 
						|
    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
 | 
						|
        name: 'persona-sync',
 | 
						|
        aliases: ['sync'],
 | 
						|
        callback: syncCallback,
 | 
						|
        helpString: 'Syncs the user persona in user-attributed messages in the current chat.',
 | 
						|
    }));
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Initializes the persona management and all its functionality.
 | 
						|
 * This is called during the initialization of the page.
 | 
						|
 */
 | 
						|
export async function initPersonas() {
 | 
						|
    await migrateNonPersonaUser();
 | 
						|
    registerPersonaSlashCommands();
 | 
						|
    $('#persona_delete_button').on('click', deleteUserAvatar);
 | 
						|
    $('#lock_persona_default').on('click', () => togglePersonaLock('default'));
 | 
						|
    $('#lock_user_name').on('click', () => togglePersonaLock('chat'));
 | 
						|
    $('#lock_persona_to_char').on('click', () => togglePersonaLock('character'));
 | 
						|
    $('#create_dummy_persona').on('click', createDummyPersona);
 | 
						|
    $('#persona_description').on('input', onPersonaDescriptionInput);
 | 
						|
    $('#persona_description_position').on('input', onPersonaDescriptionPositionInput);
 | 
						|
    $('#persona_depth_value').on('input', onPersonaDescriptionDepthValueInput);
 | 
						|
    $('#persona_depth_role').on('input', onPersonaDescriptionDepthRoleInput);
 | 
						|
    $('#persona_lore_button').on('click', onPersonaLoreButtonClick);
 | 
						|
    $('#personas_backup').on('click', onBackupPersonas);
 | 
						|
    $('#personas_restore').on('click', () => $('#personas_restore_input').trigger('click'));
 | 
						|
    $('#personas_restore_input').on('change', onPersonasRestoreInput);
 | 
						|
    $('#persona_sort_order').val(power_user.persona_sort_order).on('input', function () {
 | 
						|
        const value = String($(this).val());
 | 
						|
        // Save sort order, but do not save search sorting, as this is a temporary sorting option
 | 
						|
        if (value !== 'search') power_user.persona_sort_order = value;
 | 
						|
        getUserAvatars(true, user_avatar);
 | 
						|
        saveSettingsDebounced();
 | 
						|
    });
 | 
						|
    $('#persona_grid_toggle').on('click', () => {
 | 
						|
        const state = accountStorage.getItem(GRID_STORAGE_KEY) === 'true';
 | 
						|
        accountStorage.setItem(GRID_STORAGE_KEY, String(!state));
 | 
						|
        switchPersonaGridView();
 | 
						|
    });
 | 
						|
 | 
						|
    const debouncedPersonaSearch = debounce((searchQuery) => {
 | 
						|
        personasFilter.setFilterData(FILTER_TYPES.PERSONA_SEARCH, searchQuery);
 | 
						|
    });
 | 
						|
 | 
						|
    $('#persona_search_bar').on('input', function () {
 | 
						|
        const searchQuery = String($(this).val());
 | 
						|
        debouncedPersonaSearch(searchQuery);
 | 
						|
    });
 | 
						|
 | 
						|
    $('#sync_name_button').on('click', syncUserNameToPersona);
 | 
						|
    $('#avatar_upload_file').on('change', changeUserAvatar);
 | 
						|
 | 
						|
    $(document).on('click', '#user_avatar_block .avatar-container', function () {
 | 
						|
        const imgfile = $(this).attr('data-avatar-id');
 | 
						|
        setUserAvatar(imgfile);
 | 
						|
 | 
						|
        // force firstMes {{user}} update on persona switch
 | 
						|
        retriggerFirstMessageOnEmptyChat();
 | 
						|
    });
 | 
						|
 | 
						|
    $('#persona_rename_button').on('click', () => renamePersona(user_avatar));
 | 
						|
 | 
						|
    $(document).on('click', '#user_avatar_block .avatar_upload', function () {
 | 
						|
        $('#avatar_upload_overwrite').val('');
 | 
						|
        $('#avatar_upload_file').trigger('click');
 | 
						|
    });
 | 
						|
 | 
						|
    $('#persona_duplicate_button').on('click', () => duplicatePersona(user_avatar));
 | 
						|
 | 
						|
    $('#persona_set_image_button').on('click', function () {
 | 
						|
        if (!user_avatar) {
 | 
						|
            console.log('no imgfile');
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        $('#avatar_upload_overwrite').val(user_avatar);
 | 
						|
        $('#avatar_upload_file').trigger('click');
 | 
						|
    });
 | 
						|
 | 
						|
    $('#char_connections_button').on('click', showCharConnections);
 | 
						|
 | 
						|
    eventSource.on('charManagementDropdown', (target) => {
 | 
						|
        if (target === 'convert_to_persona') {
 | 
						|
            convertCharacterToPersona();
 | 
						|
        }
 | 
						|
    });
 | 
						|
    eventSource.on(event_types.CHAT_CHANGED, updatePersonaUIStates);
 | 
						|
    eventSource.on(event_types.CHAT_CHANGED, loadPersonaForCurrentChat);
 | 
						|
    switchPersonaGridView();
 | 
						|
}
 | 
						|
 |