2023-08-29 00:54:11 +02:00
/ * *
* 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 ( '<h3>Enter a name for this persona:</h3>(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 ] ;
2023-10-23 22:20:49 +02:00
if ( personaName ) {
2023-08-29 00:54:11 +02:00
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 }
) ;
}
2023-10-23 22:20:49 +02:00
if ( personaName !== name1 ) {
console . log ( ` Auto-updating user name to ${ personaName } ` ) ;
setUserName ( personaName ) ;
}
2023-08-29 00:54:11 +02:00
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 ( '<h3>Are you sure you want to delete this avatar?</h3>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 ( ` <h3>Are you sure you want to set " ${ personaName } " as the default persona?</h3>
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 ) ;
}