Merge pull request #2560 from SillyTavern/dupe-persona

Dupe persona
This commit is contained in:
Cohee
2024-07-26 19:47:51 +03:00
committed by GitHub
4 changed files with 82 additions and 20 deletions

View File

@@ -6440,6 +6440,9 @@
<button class="menu_button set_default_persona" title="Select this as default persona for the new chats." data-i18n="[title]Select this as default persona for the new chats."> <button class="menu_button set_default_persona" title="Select this as default persona for the new chats." data-i18n="[title]Select this as default persona for the new chats.">
<i class="fa-fw fa-solid fa-crown"></i> <i class="fa-fw fa-solid fa-crown"></i>
</button> </button>
<button class="menu_button duplicate_persona" title="Duplicate persona" data-i18n="[title]Duplicate persona">
<i class="fa-fw fa-solid fa-clone"></i>
</button>
<button class="menu_button delete_avatar" title="Delete persona" data-i18n="[title]Delete persona"> <button class="menu_button delete_avatar" title="Delete persona" data-i18n="[title]Delete persona">
<i class="fa-fw fa-solid fa-trash-alt"></i> <i class="fa-fw fa-solid fa-trash-alt"></i>
</button> </button>

View File

@@ -1,5 +1,4 @@
import { import {
callPopup,
characters, characters,
chat, chat,
chat_metadata, chat_metadata,
@@ -22,7 +21,7 @@ import { PAGINATION_TEMPLATE, debounce, delay, download, ensureImageFormatSuppor
import { debounce_timeout } from './constants.js'; import { debounce_timeout } from './constants.js';
import { FILTER_TYPES, FilterHelper } from './filters.js'; import { FILTER_TYPES, FilterHelper } from './filters.js';
import { selected_group } from './group-chats.js'; import { selected_group } from './group-chats.js';
import { POPUP_TYPE, Popup } from './popup.js'; import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js';
let savePersonasPage = 0; let savePersonasPage = 0;
const GRID_STORAGE_KEY = 'Personas_GridView'; const GRID_STORAGE_KEY = 'Personas_GridView';
@@ -332,15 +331,14 @@ async function changeUserAvatar(e) {
* @returns {Promise} Promise that resolves when the persona is set * @returns {Promise} Promise that resolves when the persona is set
*/ */
export async function createPersona(avatarId) { export async function createPersona(avatarId) {
const personaName = await callPopup('<h3>Enter a name for this persona:</h3>Cancel if you\'re just uploading an avatar.', 'input', ''); const personaName = await Popup.show.input('Enter a name for this persona:', 'Cancel if you\'re just uploading an avatar.', '');
if (!personaName) { if (!personaName) {
console.debug('User cancelled creating a persona'); console.debug('User cancelled creating a persona');
return; return;
} }
await delay(500); const personaDescription = await Popup.show.input('Enter a description for this persona:', 'You can always add or change it later.', '', { rows: 4 });
const personaDescription = await callPopup('<h3>Enter a description for this persona:</h3>You can always add or change it later.', 'input', '', { rows: 4 });
initPersona(avatarId, personaName, personaDescription); initPersona(avatarId, personaName, personaDescription);
if (power_user.persona_show_notifications) { if (power_user.persona_show_notifications) {
@@ -349,7 +347,7 @@ export async function createPersona(avatarId) {
} }
async function createDummyPersona() { async function createDummyPersona() {
const personaName = await callPopup('<h3>Enter a name for this persona:</h3>', 'input', ''); const personaName = await Popup.show.input('Enter a name for this persona:', null);
if (!personaName) { if (!personaName) {
console.debug('User cancelled creating dummy persona'); console.debug('User cancelled creating dummy persona');
@@ -508,15 +506,20 @@ async function bindUserNameToPersona(e) {
return; return;
} }
let personaUnbind = false;
const existingPersona = power_user.personas[avatarId]; 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 || ''); const personaName = await Popup.show.input(
'Enter a name for this persona:',
'(If empty name is provided, this will unbind the name from this avatar)',
existingPersona || '',
{ onClose: (p) => { personaUnbind = p.value === '' && p.result === POPUP_RESULT.AFFIRMATIVE; } });
// If the user clicked cancel, don't do anything // If the user clicked cancel, don't do anything
if (personaName === false) { if (personaName === null && !personaUnbind) {
return; return;
} }
if (personaName.length > 0) { if (personaName && personaName.length > 0) {
// If the user clicked ok and entered a name, bind the name to the persona // If the user clicked ok and entered a name, bind the name to the persona
console.log(`Binding persona ${avatarId} to name ${personaName}`); console.log(`Binding persona ${avatarId} to name ${personaName}`);
power_user.personas[avatarId] = personaName; power_user.personas[avatarId] = personaName;
@@ -643,7 +646,12 @@ async function lockPersona() {
); );
} }
power_user.personas[user_avatar] = name1; power_user.personas[user_avatar] = name1;
power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.IN_PROMPT }; power_user.persona_descriptions[user_avatar] = {
description: '',
position: persona_description_positions.IN_PROMPT,
depth: DEFAULT_DEPTH,
role: DEFAULT_ROLE,
};
} }
chat_metadata['persona'] = user_avatar; chat_metadata['persona'] = user_avatar;
@@ -672,7 +680,7 @@ async function deleteUserAvatar(e) {
return; 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'); const confirm = await Popup.show.confirm('Are you sure you want to delete this avatar?', 'All information associated with its linked persona will be lost.');
if (!confirm) { if (!confirm) {
console.debug('User cancelled deleting avatar'); console.debug('User cancelled deleting avatar');
@@ -806,7 +814,7 @@ async function setDefaultPersona(e) {
const personaName = power_user.personas[avatarId]; const personaName = power_user.personas[avatarId];
if (avatarId === currentDefault) { if (avatarId === currentDefault) {
const confirm = await callPopup('Are you sure you want to remove the default persona?', 'confirm'); const confirm = await Popup.show.confirm('Are you sure you want to remove the default persona?', personaName);
if (!confirm) { if (!confirm) {
console.debug('User cancelled removing default persona'); console.debug('User cancelled removing default persona');
@@ -819,8 +827,7 @@ async function setDefaultPersona(e) {
} }
delete power_user.default_persona; delete power_user.default_persona;
} else { } else {
const confirm = await callPopup(`<h3>Are you sure you want to set "${personaName}" as the default persona?</h3> const confirm = await Popup.show.confirm(`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.');
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) { if (!confirm) {
console.debug('User cancelled setting default persona'); console.debug('User cancelled setting default persona');
@@ -978,7 +985,7 @@ async function onPersonasRestoreInput(e) {
} }
async function syncUserNameToPersona() { async function syncUserNameToPersona() {
const confirmation = await callPopup(`<h3>Are you sure?</h3>All user-sent messages in this chat will be attributed to ${name1}.`, 'confirm'); const confirmation = await Popup.show.confirm('Are you sure?', `All user-sent messages in this chat will be attributed to ${name1}.`);
if (!confirmation) { if (!confirmation) {
return; return;
@@ -1001,6 +1008,42 @@ export function retriggerFirstMessageOnEmptyChat() {
} }
} }
/**
* Duplicates a persona.
* @param {string} avatarId
* @returns {Promise<void>}
*/
async function duplicatePersona(avatarId) {
const personaName = power_user.personas[avatarId];
if (!personaName) {
toastr.warning('Chosen avatar is not a persona');
return;
}
const confirm = await Popup.show.confirm('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,
};
await uploadUserAvatar(getUserAvatar(avatarId), newAvatarId);
await getUserAvatars(true, newAvatarId);
saveSettingsDebounced();
}
export function initPersonas() { export function initPersonas() {
$(document).on('click', '.bind_user_name', bindUserNameToPersona); $(document).on('click', '.bind_user_name', bindUserNameToPersona);
$(document).on('click', '.set_default_persona', setDefaultPersona); $(document).on('click', '.set_default_persona', setDefaultPersona);
@@ -1059,6 +1102,18 @@ export function initPersonas() {
$('#avatar_upload_file').trigger('click'); $('#avatar_upload_file').trigger('click');
}); });
$(document).on('click', '#user_avatar_block .duplicate_persona', function (e) {
e.stopPropagation();
const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');
if (!avatarId) {
console.log('no imgfile');
return;
}
duplicatePersona(avatarId);
});
$(document).on('click', '#user_avatar_block .set_persona_image', function (e) { $(document).on('click', '#user_avatar_block .set_persona_image', function (e) {
e.stopPropagation(); e.stopPropagation();
const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');

View File

@@ -73,8 +73,8 @@ const showPopupHelper = {
/** /**
* Asynchronously displays an input popup with the given header and text, and returns the user's input. * Asynchronously displays an input popup with the given header and text, and returns the user's input.
* *
* @param {string} header - The header text for the popup. * @param {string?} header - The header text for the popup.
* @param {string} text - The main text for the popup. * @param {string?} text - The main text for the popup.
* @param {string} [defaultValue=''] - The default value for the input field. * @param {string} [defaultValue=''] - The default value for the input field.
* @param {PopupOptions} [popupOptions={}] - Options for the popup. * @param {PopupOptions} [popupOptions={}] - Options for the popup.
* @return {Promise<string?>} A Promise that resolves with the user's input. * @return {Promise<string?>} A Promise that resolves with the user's input.
@@ -591,15 +591,15 @@ class PopupUtils {
/** /**
* Builds popup content with header and text below * Builds popup content with header and text below
* *
* @param {string} header - The header to be added to the text * @param {string?} header - The header to be added to the text
* @param {string} text - The main text content * @param {string?} text - The main text content
*/ */
static BuildTextWithHeader(header, text) { static BuildTextWithHeader(header, text) {
if (!header) { if (!header) {
return text; return text;
} }
return `<h3>${header}</h3> return `<h3>${header}</h3>
${text}`; ${text ?? ''}`; // Convert no text to empty string
} }
} }

View File

@@ -3031,6 +3031,10 @@ grammarly-extension {
opacity: 1; opacity: 1;
} }
.avatar-container .avatar-buttons .menu_button {
padding: 3px;
}
/* Ross should be able to handle this later */ /* Ross should be able to handle this later */
/*.big-avatars .avatar-buttons{ /*.big-avatars .avatar-buttons{
justify-content: center; justify-content: center;