import { callPopup, characters, chat_metadata, default_avatar, eventSource, event_types, getRequestHeaders, getThumbnailUrl, getUserAvatars, name1, saveMetadata, saveSettingsDebounced, setUserName, this_chid, user_avatar } from "../script.js"; import { persona_description_positions, power_user } from "./power-user.js"; import { getTokenCount } from "./tokenizers.js"; import { debounce, delay } from "./utils.js"; /** * 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 object representing the AJAX request */ 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); } return jQuery.ajax({ type: "POST", url: "/uploaduseravatar", data: formData, beforeSend: () => { }, cache: false, contentType: false, processData: false, success: async function () { await getUserAvatars(); }, }); } /** * 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 callPopup('

Enter a name for this persona:

Cancel if you\'re just uploading an avatar.', 'input', ''); if (!personaName) { console.debug('User cancelled creating a persona'); return; } await delay(500); const personaDescription = await callPopup('

Enter a description for this persona:

You can always add or change it later.', 'input', '', { rows: 4 }); initPersona(avatarId, personaName, personaDescription); if (power_user.persona_show_notifications) { toastr.success(`You can now pick ${personaName} as a persona in the Persona Management menu.`, 'Persona Created'); } } async function createDummyPersona() { const personaName = await callPopup('

Enter a name for this persona:

', 'input', ''); 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_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, }; saveSettingsDebounced(); } 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; } const name = characters[characterId]?.name; let description = characters[characterId]?.description; const overwriteName = `${name} (Persona).png`; if (overwriteName in power_user.personas) { const confirmation = await callPopup("This character exists as a persona already. Are you sure want to overwrite it?", "confirm", "", { okButton: 'Yes' }); if (confirmation === false) { console.log("User cancelled the overwrite of the persona"); return; } } if (description.includes('{{char}}') || description.includes('{{user}}')) { await delay(500); const confirmation = await callPopup("This character has a description that uses {{char}} or {{user}} macros. Do you want to swap them in the persona description?", "confirm", "", { okButton: 'Yes' }); if (confirmation) { 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, }; // 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(`You can now select ${name} as a persona in the Persona Management menu.`, 'Persona Created'); // Refresh the persona selector await getUserAvatars(); // Reload the persona description setPersonaDescription(); } /** * Counts the number of tokens in a persona description. */ const countPersonaDescriptionTokens = debounce(() => { const description = String($("#persona_description").val()); const count = getTokenCount(description); $("#persona_description_token_count").text(String(count)); }, 1000); export function setPersonaDescription() { if (power_user.persona_description_position === persona_description_positions.AFTER_CHAR) { power_user.persona_description_position = persona_description_positions.IN_PROMPT; } $("#persona_description").val(power_user.persona_description); $("#persona_description_position") .val(power_user.persona_description_position) .find(`option[value='${power_user.persona_description_position}']`) .attr("selected", String(true)); countPersonaDescriptionTokens(); } 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}`); $(`.avatar[imgfile="${key}"]`).trigger('click'); return; } } } async function bindUserNameToPersona() { const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); if (!avatarId) { console.warn('No avatar id found'); return; } const existingPersona = power_user.personas[avatarId]; const personaName = await callPopup('

Enter a name for this persona:

(If empty name is provided, this will unbind the name from this avatar)', 'input', existingPersona || ''); // If the user clicked cancel, don't do anything if (personaName === false) { return; } if (personaName.length > 0) { // If the user clicked ok and entered a name, bind the name to the persona console.log(`Binding persona ${avatarId} to name ${personaName}`); power_user.personas[avatarId] = personaName; const descriptor = power_user.persona_descriptions[avatarId]; const isCurrentPersona = avatarId === user_avatar; // Create a description object if it doesn't exist if (!descriptor) { // If the user is currently using this persona, set the description to the current description power_user.persona_descriptions[avatarId] = { description: isCurrentPersona ? power_user.persona_description : '', position: isCurrentPersona ? power_user.persona_description_position : persona_description_positions.IN_PROMPT, }; } // If the user is currently using this persona, update the name if (isCurrentPersona) { console.log(`Auto-updating user name to ${personaName}`); setUserName(personaName); } } else { // If the user clicked ok, but didn't enter a name, delete the persona console.log(`Unbinding persona ${avatarId}`); delete power_user.personas[avatarId]; delete power_user.persona_descriptions[avatarId]; } saveSettingsDebounced(); await getUserAvatars(); setPersonaDescription(); } export function selectCurrentPersona() { const personaName = power_user.personas[user_avatar]; if (personaName) { const lockedPersona = chat_metadata['persona']; if (lockedPersona && lockedPersona !== user_avatar && power_user.persona_show_notifications) { toastr.info( `To permanently set "${personaName}" as the selected persona, unlock and relock it using the "Lock" button. Otherwise, the selection resets upon reloading the chat.`, `This chat is locked to a different persona (${power_user.personas[lockedPersona]}).`, { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true } ); } if (personaName !== name1) { console.log(`Auto-updating user name to ${personaName}`); setUserName(personaName); } const descriptor = power_user.persona_descriptions[user_avatar]; if (descriptor) { power_user.persona_description = descriptor.description; power_user.persona_description_position = descriptor.position; } else { power_user.persona_description = ''; power_user.persona_description_position = persona_description_positions.IN_PROMPT; power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.IN_PROMPT }; } setPersonaDescription(); } } async function lockUserNameToChat() { if (chat_metadata['persona']) { console.log(`Unlocking persona for this chat ${chat_metadata['persona']}`); delete chat_metadata['persona']; await saveMetadata(); if (power_user.persona_show_notifications) { toastr.info('User persona is now unlocked for this chat. Click the "Lock" again to revert.', 'Persona unlocked'); } updateUserLockIcon(); return; } if (!(user_avatar in power_user.personas)) { console.log(`Creating a new persona ${user_avatar}`); if (power_user.persona_show_notifications) { toastr.info( 'Creating a new persona for currently selected user name and avatar...', 'Persona not set for this avatar', { timeOut: 10000, extendedTimeOut: 20000, }, ); } power_user.personas[user_avatar] = name1; power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.IN_PROMPT }; } chat_metadata['persona'] = user_avatar; await saveMetadata(); saveSettingsDebounced(); console.log(`Locking persona for this chat ${user_avatar}`); if (power_user.persona_show_notifications) { toastr.success(`User persona is locked to ${name1} in this chat`); } updateUserLockIcon(); } async function deleteUserAvatar() { const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); if (!avatarId) { console.warn('No avatar id found'); return; } if (avatarId == user_avatar) { console.warn(`User tried to delete their current avatar ${avatarId}`); toastr.warning('You cannot delete the avatar you are currently using', 'Warning'); return; } const confirm = await callPopup('

Are you sure you want to delete this avatar?

All information associated with its linked persona will be lost.', 'confirm'); if (!confirm) { console.debug('User cancelled deleting avatar'); return; } const request = await fetch("/deleteuseravatar", { 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('The default persona was deleted. You will need to set a new default persona.', 'Default persona deleted'); power_user.default_persona = null; } if (avatarId === chat_metadata['persona']) { toastr.warning('The locked persona was deleted. You will need to set a new persona for this chat.', 'Persona deleted'); delete chat_metadata['persona']; await saveMetadata(); } saveSettingsDebounced(); await getUserAvatars(); updateUserLockIcon(); } } 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()), }; power_user.persona_descriptions[user_avatar] = object; } object.description = power_user.persona_description; } saveSettingsDebounced(); } function onPersonaDescriptionPositionInput() { power_user.persona_description_position = Number( $("#persona_description_position").find(":selected").val() ); if (power_user.personas[user_avatar]) { let object = power_user.persona_descriptions[user_avatar]; if (!object) { object = { description: power_user.persona_description, position: power_user.persona_description_position, }; power_user.persona_descriptions[user_avatar] = object; } object.position = power_user.persona_description_position; } saveSettingsDebounced(); } async function setDefaultPersona() { const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); 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('You must bind a name to this persona before you can set it as the default.', 'Persona name not set'); return; } const personaName = power_user.personas[avatarId]; if (avatarId === currentDefault) { const confirm = await callPopup('Are you sure you want to remove the default persona?', 'confirm'); if (!confirm) { console.debug('User cancelled removing default persona'); return; } console.log(`Removing default persona ${avatarId}`); if (power_user.persona_show_notifications) { toastr.info('This persona will no longer be used by default when you open a new chat.', `Default persona removed`); } delete power_user.default_persona; } else { const confirm = await callPopup(`

Are you sure you want to set "${personaName}" as the default persona?

This name and avatar will be used for all new chats, as well as existing chats where the user persona is not locked.`, 'confirm'); if (!confirm) { console.debug('User cancelled setting default persona'); return; } power_user.default_persona = avatarId; if (power_user.persona_show_notifications) { toastr.success('This persona will be used by default when you open a new chat.', `Default persona set to ${personaName}`); } } saveSettingsDebounced(); await getUserAvatars(); } function updateUserLockIcon() { const hasLock = !!chat_metadata['persona']; $('#lock_user_name').toggleClass('fa-unlock', !hasLock); $('#lock_user_name').toggleClass('fa-lock', hasLock); } function setChatLockedPersona() { // Define a persona for this chat let chatPersona = ''; if (chat_metadata['persona']) { // If persona is locked in chat metadata, select it console.log(`Using locked persona ${chat_metadata['persona']}`); chatPersona = chat_metadata['persona']; } else if (power_user.default_persona) { // If default persona is set, select it console.log(`Using default persona ${power_user.default_persona}`); chatPersona = power_user.default_persona; } // No persona set: user current settings if (!chatPersona) { console.debug('No default or locked persona set for this chat'); return; } // Find the avatar file const personaAvatar = $(`.avatar[imgfile="${chatPersona}"]`).trigger('click'); // Avatar missing (persona deleted) if (chat_metadata['persona'] && personaAvatar.length == 0) { console.warn('Persona avatar not found, unlocking persona'); delete chat_metadata['persona']; updateUserLockIcon(); return; } // Default persona missing if (power_user.default_persona && personaAvatar.length == 0) { console.warn('Default persona avatar not found, clearing default persona'); power_user.default_persona = null; saveSettingsDebounced(); return; } // Persona avatar found, select it personaAvatar.trigger('click'); updateUserLockIcon(); } export function initPersonas() { $(document).on('click', '.bind_user_name', bindUserNameToPersona); $(document).on('click', '.set_default_persona', setDefaultPersona); $(document).on('click', '.delete_avatar', deleteUserAvatar); $('#lock_user_name').on('click', lockUserNameToChat); $("#create_dummy_persona").on('click', createDummyPersona); $('#persona_description').on('input', onPersonaDescriptionInput); $('#persona_description_position').on('input', onPersonaDescriptionPositionInput); eventSource.on("charManagementDropdown", (target) => { if (target === 'convert_to_persona') { convertCharacterToPersona(); } }); eventSource.on(event_types.CHAT_CHANGED, setChatLockedPersona); }