|
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, localizePagination, renderPaginationDropdown, paginationDropdownChangeHandler } 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'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const USER_AVATAR_PATH = 'User Avatars/'; |
|
|
|
let savePersonasPage = 0; |
|
const GRID_STORAGE_KEY = 'Personas_GridView'; |
|
const DEFAULT_DEPTH = 2; |
|
const DEFAULT_ROLE = 0; |
|
|
|
|
|
export let user_avatar = ''; |
|
|
|
|
|
export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick)); |
|
|
|
|
|
let navigateToAvatar = () => { }; |
|
|
|
|
|
|
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getUserAvatar(avatarImg) { |
|
return `${USER_AVATAR_PATH}${avatarImg}`; |
|
} |
|
|
|
export function initUserAvatar(avatar) { |
|
user_avatar = avatar; |
|
reloadUserAvatar(); |
|
updatePersonaUIStates(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 }); |
|
retriggerFirstMessageOnEmptyChat(); |
|
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)); |
|
} |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
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 (searchTerm && isHidden) { |
|
searchOption.removeAttr('hidden'); |
|
selector.val(searchOption.attr('value')); |
|
flashHighlight(selector); |
|
} |
|
|
|
if (!searchTerm) { |
|
searchOption.attr('hidden', ''); |
|
selector.val(power_user.persona_sort_order); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function addMissingPersonas(avatarsList) { |
|
for (const persona of avatarsList) { |
|
if (!power_user.personas[persona]) { |
|
initPersona(persona, '[Unnamed Persona]', ''); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
addMissingPersonas(allEntities); |
|
|
|
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; |
|
const sizeChangerOptions = [5, 10, 25, 50, 100, 250, 500, 1000]; |
|
|
|
$('#persona_pagination_container').pagination({ |
|
dataSource: entities, |
|
pageSize: perPage, |
|
sizeChangerOptions, |
|
pageRange: 1, |
|
pageNumber: savePersonasPage || 1, |
|
position: 'top', |
|
showPageNumbers: false, |
|
showSizeChanger: true, |
|
formatSizeChanger: renderPaginationDropdown(perPage, sizeChangerOptions), |
|
prevText: '<', |
|
nextText: '>', |
|
formatNavigator: PAGINATION_TEMPLATE, |
|
showNavigator: true, |
|
callback: function (data) { |
|
$(listId).empty(); |
|
for (const item of data) { |
|
$(listId).append(getUserAvatarBlock(item)); |
|
} |
|
updatePersonaUIStates(); |
|
localizePagination($('#persona_pagination_container')); |
|
}, |
|
afterSizeSelectorChange: function (e, size) { |
|
accountStorage.setItem(storageKey, e.target.value); |
|
paginationDropdownChangeHandler(e, size); |
|
}, |
|
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; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
form.reset(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
const avatarId = `${Date.now()}-${personaName.replace(/[^a-zA-Z0-9]/g, '')}.png`; |
|
initPersona(avatarId, personaName, ''); |
|
await uploadUserAvatar(default_user_avatar, avatarId); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function convertCharacterToPersona(characterId = null) { |
|
if (null === characterId) characterId = Number(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 (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`); |
|
|
|
|
|
await getUserAvatars(true, overwriteName); |
|
|
|
setPersonaDescription(); |
|
return true; |
|
} |
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
|
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(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 }); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function updatePersonaConnectionsAvatarList() { |
|
|
|
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.]`); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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() : []); |
|
|
|
|
|
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.`; |
|
} |
|
}); |
|
|
|
|
|
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)) { |
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
if (shouldAutoLock) { |
|
chat_metadata['persona'] = user_avatar; |
|
console.log(`Auto locked persona to ${user_avatar}`); |
|
if (toastPersonaNameChange && power_user.persona_show_notifications) { |
|
toastr.success(t`Persona ${personaName} selected and auto-locked to current chat`, t`Persona Selected`); |
|
} |
|
saveMetadataDebounced(); |
|
updatePersonaUIStates(); |
|
} |
|
|
|
|
|
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 }); |
|
}, |
|
}); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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}`); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function setPersonaLockState(state, type = 'chat') { |
|
return state ? await lockPersona(type) : await unlockPersona(type); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function togglePersonaLock(type = 'chat') { |
|
if (isPersonaLocked(type)) { |
|
await unlockPersona(type); |
|
return false; |
|
} else { |
|
await lockPersona(type); |
|
return true; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
async function unlockPersona(type = 'chat') { |
|
switch (type) { |
|
case 'default': { |
|
|
|
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': { |
|
|
|
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(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function lockPersona(type = 'chat') { |
|
|
|
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(); |
|
|
|
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(); |
|
|
|
|
|
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(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getPersonaStates(avatarId) { |
|
const isDefaultPersona = power_user.default_persona === avatarId; |
|
const hasChatLock = chat_metadata['persona'] == avatarId; |
|
|
|
|
|
const connections = power_user.persona_descriptions[avatarId]?.connections; |
|
const hasCharLock = !!connections?.some(c => |
|
(!selected_group && c.type === 'character' && c.id === characters[Number(this_chid)]?.avatar) |
|
|| (selected_group && c.type === 'group' && c.id === selected_group)); |
|
|
|
return { |
|
avatarId: avatarId, |
|
default: isDefaultPersona, |
|
locked: { |
|
chat: hasChatLock, |
|
character: hasCharLock, |
|
}, |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function updatePersonaUIStates({ navigateToCurrent = false } = {}) { |
|
if (navigateToCurrent) { |
|
navigateToAvatar(user_avatar); |
|
} |
|
|
|
|
|
$('#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); |
|
}); |
|
|
|
|
|
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); |
|
|
|
|
|
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); |
|
|
|
|
|
setInfoBlock('#persona_connections_info_block', messageContainer, 'hint'); |
|
} else { |
|
|
|
clearInfoBlock('#persona_connections_info_block'); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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, |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function loadPersonaForCurrentChat({ doRender = false } = {}) { |
|
|
|
const userAvatars = await getUserAvatars(doRender); |
|
|
|
|
|
let chatPersona = ''; |
|
|
|
|
|
let connectType = null; |
|
|
|
|
|
if (chat_metadata['persona']) { |
|
console.log(`Using locked persona ${chat_metadata['persona']}`); |
|
chatPersona = chat_metadata['persona']; |
|
|
|
|
|
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 (!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'; |
|
} |
|
|
|
|
|
if (!chatPersona && power_user.default_persona) { |
|
console.log(`Using default persona ${power_user.default_persona}`); |
|
chatPersona = power_user.default_persona; |
|
|
|
if (chatPersona) connectType = 'default'; |
|
} |
|
|
|
|
|
if (chat_metadata['persona'] && !userAvatars.includes(chat_metadata['persona'])) { |
|
console.warn('Persona avatar not found, unlocking persona'); |
|
delete chat_metadata['persona']; |
|
} |
|
|
|
|
|
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(); |
|
} |
|
|
|
|
|
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 }); |
|
} |
|
} |
|
|
|
else if (chatPersona && power_user.persona_auto_lock && !chat_metadata['persona']) { |
|
lockPersona('chat'); |
|
} |
|
|
|
updatePersonaUIStates(); |
|
|
|
return !!chatPersona; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getConnectedPersonas(characterKey = undefined) { |
|
characterKey ??= selected_group || characters[Number(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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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'); |
|
|
|
|
|
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[Number(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'); |
|
} |
|
}, |
|
}); |
|
|
|
|
|
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`); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getCurrentConnectionObj() { |
|
if (selected_group) |
|
return { type: 'group', id: selected_group }; |
|
if (characters[Number(this_chid)]?.avatar) |
|
return { type: 'character', id: characters[Number(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 = []; |
|
|
|
|
|
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 (!avatarsList.includes(key)) { |
|
warnings.push(`Persona image "${key}" (${value}) is missing, uploading default avatar`); |
|
await uploadUserAvatar(default_user_avatar, key); |
|
} |
|
} |
|
|
|
|
|
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(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function retriggerFirstMessageOnEmptyChat() { |
|
if (Number(this_chid) >= 0 && !selected_group && chat.length === 1) { |
|
$('#firstmessage_textarea').trigger('input'); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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(); |
|
} |
|
|
|
|
|
|
|
|
|
async function migrateNonPersonaUser() { |
|
if (user_avatar in power_user.personas) { |
|
return; |
|
} |
|
|
|
initPersona(user_avatar, name1, ''); |
|
setPersonaDescription(); |
|
await getUserAvatars(true, user_avatar); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function lockPersonaCallback(_args, value) { |
|
const type = (_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 ''; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 (['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); |
|
return ''; |
|
} else if (mode === 'lookup') { |
|
toastr.warning(`Persona ${name} not found`); |
|
return ''; |
|
} |
|
} |
|
|
|
if (['temp', 'all'].includes(mode)) { |
|
|
|
setUserName(name); |
|
} |
|
|
|
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'), |
|
}), |
|
], |
|
})); |
|
|
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
|
name: 'lock', |
|
|
|
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.', |
|
})); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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()); |
|
|
|
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); |
|
}); |
|
|
|
$('#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(event_types.CHARACTER_MANAGEMENT_DROPDOWN, (target) => { |
|
if (target === 'convert_to_persona') { |
|
convertCharacterToPersona(); |
|
} |
|
}); |
|
eventSource.on(event_types.CHAT_CHANGED, updatePersonaUIStates); |
|
eventSource.on(event_types.CHAT_CHANGED, loadPersonaForCurrentChat); |
|
switchPersonaGridView(); |
|
} |
|
|