diff --git a/public/index.html b/public/index.html index 2035ba52a..90e2f4a57 100644 --- a/public/index.html +++ b/public/index.html @@ -82,6 +82,7 @@ + SillyTavern @@ -3197,6 +3198,9 @@ + diff --git a/public/script.js b/public/script.js index d33da5ee3..ca01c3383 100644 --- a/public/script.js +++ b/public/script.js @@ -175,6 +175,7 @@ import { } from "./scripts/instruct-mode.js"; import { applyLocale } from "./scripts/i18n.js"; import { getTokenCount, getTokenizerModel, saveTokenCache } from "./scripts/tokenizers.js"; +import { initPersonas, selectCurrentPersona, setPersonaDescription } from "./scripts/personas.js"; //exporting functions and vars for mods export { @@ -290,7 +291,6 @@ export const eventSource = new EventEmitter(); setInterval(displayOverrideWarnings, 5000); // ...or when the chat changes eventSource.on(event_types.CHAT_CHANGED, displayOverrideWarnings); -eventSource.on(event_types.CHAT_CHANGED, setChatLockedPersona); eventSource.on(event_types.MESSAGE_RECEIVED, processExtensionHelpers); eventSource.on(event_types.MESSAGE_SENT, processExtensionHelpers); @@ -653,7 +653,7 @@ var settings; export let koboldai_settings; export let koboldai_setting_names; var preset_settings = "gui"; -var user_avatar = "you.png"; +export let user_avatar = "you.png"; export var amount_gen = 80; //default max length of AI generated responses var max_context = 2048; @@ -4574,7 +4574,7 @@ function changeMainAPI() { //////////////////////////////////////////////////// -async function getUserAvatars() { +export async function getUserAvatars() { const response = await fetch("/getuseravatars", { method: "POST", headers: getRequestHeaders(), @@ -4598,70 +4598,8 @@ async function getUserAvatars() { } } -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", true); - countPersonaDescriptionTokens(); -} -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(); -} - -/** - * 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)); -}, durationSaveEdit); - -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 highlightSelectedAvatar() { $("#user_avatar_block").find(".avatar").removeClass("selected"); @@ -4710,124 +4648,13 @@ export function setUserName(value) { saveSettings("change_name"); } -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(); -} - -async function createDummyPersona() { - const fetchResult = await fetch(default_avatar); - const blob = await fetchResult.blob(); - const file = new File([blob], "avatar.png", { type: "image/png" }); - const formData = new FormData(); - formData.append("avatar", file); - - jQuery.ajax({ - type: "POST", - url: "/uploaduseravatar", - data: formData, - beforeSend: () => { }, - cache: false, - contentType: false, - processData: false, - success: async function (data) { - await getUserAvatars(); - }, - }); -} - -function updateUserLockIcon() { - const hasLock = !!chat_metadata['persona']; - $('#lock_user_name').toggleClass('fa-unlock', !hasLock); - $('#lock_user_name').toggleClass('fa-lock', hasLock); -} function setUserAvatar() { user_avatar = $(this).attr("imgfile"); reloadUserAvatar(); saveSettingsDebounced(); highlightSelectedAvatar(); - - const personaName = power_user.personas[user_avatar]; - if (personaName && name1 !== 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 }, - ); - } - - 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(); - } + selectCurrentPersona(); } async function uploadUserAvatar(e) { @@ -4885,185 +4712,7 @@ async function uploadUserAvatar(e) { $("#form_upload_avatar").trigger("reset"); } -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(); -} - -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(); - } -} - -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(); -} - -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(); -} async function doOnboarding(avatarId) { const template = $('#onboarding_template .onboarding'); @@ -6142,7 +5791,7 @@ function hideSwipeButtons() { $("#chat").children().filter(`[mesid="${count_view_mes - 1}"]`).children('.swipe_left').css('display', 'none'); } -async function saveMetadata() { +export async function saveMetadata() { if (selected_group) { await editGroup(selected_group, true, false); } @@ -8476,7 +8125,6 @@ $(document).ready(function () { setUserName($('#your_name').val()); }); - $("#create_dummy_persona").on('click', createDummyPersona); $('#sync_name_button').on('click', async function () { const confirmation = await callPopup(`

Are you sure?

All user-sent messages in this chat will be attributed to ${name1}.`, 'confirm'); @@ -8518,13 +8166,6 @@ $(document).ready(function () { setTimeout(getStatusNovel, 10); }); - $(document).on('click', '.bind_user_name', bindUserNameToPersona); - $(document).on('click', '.delete_avatar', deleteUserAvatar); - $(document).on('click', '.set_default_persona', setDefaultPersona); - $('#lock_user_name').on('click', lockUserNameToChat); - $('#persona_description').on('input', onPersonaDescriptionInput); - $('#persona_description_position').on('input', onPersonaDescriptionPositionInput); - //**************************CHARACTER IMPORT EXPORT*************************// $("#character_import_button").click(function () { $("#character_import_file").click(); @@ -8902,6 +8543,8 @@ $(document).ready(function () { OF THE CHARACTER'S CHAT FILES.

` ); break;*/ + default: + eventSource.emit('charManagementDropdown', target); } $("#char-management-dropdown").prop('selectedIndex', 0); }); @@ -9111,4 +8754,5 @@ $(document).ready(function () { // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts initAuthorsNote(); initRossMods(); + initPersonas(); }); diff --git a/public/scripts/personas.js b/public/scripts/personas.js new file mode 100644 index 000000000..1252545f4 --- /dev/null +++ b/public/scripts/personas.js @@ -0,0 +1,455 @@ +/** + * This is a placeholder file for all the Persona Management code. Will be refactored into a separate file soon. + */ + +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(); + }, + }); +} + +async function createDummyPersona() { + await uploadUserAvatar(default_avatar); +} + +async function convertCharacterToPersona() { + const avatarUrl = characters[this_chid]?.avatar; + + if (!avatarUrl) { + console.log("No avatar found for this character"); + return; + } + + const name = characters[this_chid]?.name; + let description = characters[this_chid]?.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 && name1 !== 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 } + ); + } + + 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); +} diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index aa69c9f8f..5d189f56d 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -1,6 +1,5 @@ import { addOneMessage, - autoSelectPersona, characters, chat, chat_metadata, @@ -26,6 +25,7 @@ import { getMessageTimeStamp } from "./RossAscends-mods.js"; import { resetSelectedGroup } from "./group-chats.js"; import { getRegexedString, regex_placement } from "./extensions/regex/engine.js"; import { chat_styles, power_user } from "./power-user.js"; +import { autoSelectPersona } from "./personas.js"; export { executeSlashCommands, registerSlashCommand,