Merge pull request #2560 from SillyTavern/dupe-persona Dupe persona

a2661b2c486f2b509f096a3ce8e59fedaddbbb92

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
4 files changed, +82 -20Ignore whitespace
public/index.html+3 -0
@@ -6440,6 +6440,9 @@
6440 <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.">6440 <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.">
6441 <i class="fa-fw fa-solid fa-crown"></i>6441 <i class="fa-fw fa-solid fa-crown"></i>
6442 </button>6442 </button>
6443 <button class="menu_button duplicate_persona" title="Duplicate persona" data-i18n="[title]Duplicate persona">
6444 <i class="fa-fw fa-solid fa-clone"></i>
6445 </button>
6443 <button class="menu_button delete_avatar" title="Delete persona" data-i18n="[title]Delete persona">6446 <button class="menu_button delete_avatar" title="Delete persona" data-i18n="[title]Delete persona">
6444 <i class="fa-fw fa-solid fa-trash-alt"></i>6447 <i class="fa-fw fa-solid fa-trash-alt"></i>
6445 </button>6448 </button>
public/scripts/personas.js+70 -15
@@ -1,5 +1,4 @@
1import {1import {
2 callPopup,
3 characters,2 characters,
4 chat,3 chat,
5 chat_metadata,4 chat_metadata,
@@ -22,7 +21,7 @@ import { PAGINATION_TEMPLATE, debounce, delay, download, ensureImageFormatSuppor
22import { debounce_timeout } from './constants.js';21import { debounce_timeout } from './constants.js';
23import { FILTER_TYPES, FilterHelper } from './filters.js';22import { FILTER_TYPES, FilterHelper } from './filters.js';
24import { selected_group } from './group-chats.js';23import { selected_group } from './group-chats.js';
25import { POPUP_TYPE, Popup } from './popup.js';24import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js';
2625
27let savePersonasPage = 0;26let savePersonasPage = 0;
28const GRID_STORAGE_KEY = 'Personas_GridView';27const GRID_STORAGE_KEY = 'Personas_GridView';
@@ -332,15 +331,14 @@ async function changeUserAvatar(e) {
332 * @returns {Promise} Promise that resolves when the persona is set331 * @returns {Promise} Promise that resolves when the persona is set
333 */332 */
334export async function createPersona(avatarId) {333export async function createPersona(avatarId) {
335 const personaName = await callPopup('<h3>Enter a name for this persona:</h3>Cancel if you\'re just uploading an avatar.', 'input', '');334 const personaName = await Popup.show.input('Enter a name for this persona:', 'Cancel if you\'re just uploading an avatar.', '');
336335
337 if (!personaName) {336 if (!personaName) {
338 console.debug('User cancelled creating a persona');337 console.debug('User cancelled creating a persona');
339 return;338 return;
340 }339 }
341340
342 await delay(500);341 const personaDescription = await Popup.show.input('Enter a description for this persona:', 'You can always add or change it later.', '', { rows: 4 });
343 const personaDescription = await callPopup('<h3>Enter a description for this persona:</h3>You can always add or change it later.', 'input', '', { rows: 4 });
344342
345 initPersona(avatarId, personaName, personaDescription);343 initPersona(avatarId, personaName, personaDescription);
346 if (power_user.persona_show_notifications) {344 if (power_user.persona_show_notifications) {
@@ -349,7 +347,7 @@ export async function createPersona(avatarId) {
349}347}
350348
351async function createDummyPersona() {349async function createDummyPersona() {
352 const personaName = await callPopup('<h3>Enter a name for this persona:</h3>', 'input', '');350 const personaName = await Popup.show.input('Enter a name for this persona:', null);
353351
354 if (!personaName) {352 if (!personaName) {
355 console.debug('User cancelled creating dummy persona');353 console.debug('User cancelled creating dummy persona');
@@ -508,15 +506,20 @@ async function bindUserNameToPersona(e) {
508 return;506 return;
509 }507 }
510508
509 let personaUnbind = false;
511 const existingPersona = power_user.personas[avatarId];510 const existingPersona = power_user.personas[avatarId];
512 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 || '');511 const personaName = await Popup.show.input(
512 'Enter a name for this persona:',
513 '(If empty name is provided, this will unbind the name from this avatar)',
514 existingPersona || '',
515 { onClose: (p) => { personaUnbind = p.value === '' && p.result === POPUP_RESULT.AFFIRMATIVE; } });
513516
514 // If the user clicked cancel, don't do anything517 // If the user clicked cancel, don't do anything
515 if (personaName === false) {518 if (personaName === null && !personaUnbind) {
516 return;519 return;
517 }520 }
518521
519 if (personaName.length > 0) {522 if (personaName && personaName.length > 0) {
520 // If the user clicked ok and entered a name, bind the name to the persona523 // If the user clicked ok and entered a name, bind the name to the persona
521 console.log(`Binding persona ${avatarId} to name ${personaName}`);524 console.log(`Binding persona ${avatarId} to name ${personaName}`);
522 power_user.personas[avatarId] = personaName;525 power_user.personas[avatarId] = personaName;
@@ -643,7 +646,12 @@ async function lockPersona() {
643 );646 );
644 }647 }
645 power_user.personas[user_avatar] = name1;648 power_user.personas[user_avatar] = name1;
646 power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.IN_PROMPT };649 power_user.persona_descriptions[user_avatar] = {
650 description: '',
651 position: persona_description_positions.IN_PROMPT,
652 depth: DEFAULT_DEPTH,
653 role: DEFAULT_ROLE,
654 };
647 }655 }
648656
649 chat_metadata['persona'] = user_avatar;657 chat_metadata['persona'] = user_avatar;
@@ -672,7 +680,7 @@ async function deleteUserAvatar(e) {
672 return;680 return;
673 }681 }
674682
675 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');683 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.');
676684
677 if (!confirm) {685 if (!confirm) {
678 console.debug('User cancelled deleting avatar');686 console.debug('User cancelled deleting avatar');
@@ -806,7 +814,7 @@ async function setDefaultPersona(e) {
806 const personaName = power_user.personas[avatarId];814 const personaName = power_user.personas[avatarId];
807815
808 if (avatarId === currentDefault) {816 if (avatarId === currentDefault) {
809 const confirm = await callPopup('Are you sure you want to remove the default persona?', 'confirm');817 const confirm = await Popup.show.confirm('Are you sure you want to remove the default persona?', personaName);
810818
811 if (!confirm) {819 if (!confirm) {
812 console.debug('User cancelled removing default persona');820 console.debug('User cancelled removing default persona');
@@ -819,8 +827,7 @@ async function setDefaultPersona(e) {
819 }827 }
820 delete power_user.default_persona;828 delete power_user.default_persona;
821 } else {829 } else {
822 const confirm = await callPopup(`<h3>Are you sure you want to set "${personaName}" as the default persona?</h3>830 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.');
823 This name and avatar will be used for all new chats, as well as existing chats where the user persona is not locked.`, 'confirm');
824831
825 if (!confirm) {832 if (!confirm) {
826 console.debug('User cancelled setting default persona');833 console.debug('User cancelled setting default persona');
@@ -978,7 +985,7 @@ async function onPersonasRestoreInput(e) {
978}985}
979986
980async function syncUserNameToPersona() {987async function syncUserNameToPersona() {
981 const confirmation = await callPopup(`<h3>Are you sure?</h3>All user-sent messages in this chat will be attributed to ${name1}.`, 'confirm');988 const confirmation = await Popup.show.confirm('Are you sure?', `All user-sent messages in this chat will be attributed to ${name1}.`);
982989
983 if (!confirmation) {990 if (!confirmation) {
984 return;991 return;
@@ -1001,6 +1008,42 @@ export function retriggerFirstMessageOnEmptyChat() {
1001 }1008 }
1002}1009}
10031010
1011/**
1012 * Duplicates a persona.
1013 * @param {string} avatarId
1014 * @returns {Promise<void>}
1015 */
1016async function duplicatePersona(avatarId) {
1017 const personaName = power_user.personas[avatarId];
1018
1019 if (!personaName) {
1020 toastr.warning('Chosen avatar is not a persona');
1021 return;
1022 }
1023
1024 const confirm = await Popup.show.confirm('Are you sure you want to duplicate this persona?', personaName);
1025
1026 if (!confirm) {
1027 console.debug('User cancelled duplicating persona');
1028 return;
1029 }
1030
1031 const newAvatarId = `${Date.now()}-${personaName.replace(/[^a-zA-Z0-9]/g, '')}.png`;
1032 const descriptor = power_user.persona_descriptions[avatarId];
1033
1034 power_user.personas[newAvatarId] = personaName;
1035 power_user.persona_descriptions[newAvatarId] = {
1036 description: descriptor?.description ?? '',
1037 position: descriptor?.position ?? persona_description_positions.IN_PROMPT,
1038 depth: descriptor?.depth ?? DEFAULT_DEPTH,
1039 role: descriptor?.role ?? DEFAULT_ROLE,
1040 };
1041
1042 await uploadUserAvatar(getUserAvatar(avatarId), newAvatarId);
1043 await getUserAvatars(true, newAvatarId);
1044 saveSettingsDebounced();
1045}
1046
1004export function initPersonas() {1047export function initPersonas() {
1005 $(document).on('click', '.bind_user_name', bindUserNameToPersona);1048 $(document).on('click', '.bind_user_name', bindUserNameToPersona);
1006 $(document).on('click', '.set_default_persona', setDefaultPersona);1049 $(document).on('click', '.set_default_persona', setDefaultPersona);
@@ -1059,6 +1102,18 @@ export function initPersonas() {
1059 $('#avatar_upload_file').trigger('click');1102 $('#avatar_upload_file').trigger('click');
1060 });1103 });
10611104
1105 $(document).on('click', '#user_avatar_block .duplicate_persona', function (e) {
1106 e.stopPropagation();
1107 const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');
1108
1109 if (!avatarId) {
1110 console.log('no imgfile');
1111 return;
1112 }
1113
1114 duplicatePersona(avatarId);
1115 });
1116
1062 $(document).on('click', '#user_avatar_block .set_persona_image', function (e) {1117 $(document).on('click', '#user_avatar_block .set_persona_image', function (e) {
1063 e.stopPropagation();1118 e.stopPropagation();
1064 const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');1119 const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');
public/scripts/popup.js+5 -5
@@ -73,8 +73,8 @@ const showPopupHelper = {
73 /**73 /**
74 * Asynchronously displays an input popup with the given header and text, and returns the user's input.74 * Asynchronously displays an input popup with the given header and text, and returns the user's input.
75 *75 *
76 * @param {string} header - The header text for the popup.76 * @param {string?} header - The header text for the popup.
77 * @param {string} text - The main text for the popup.77 * @param {string?} text - The main text for the popup.
78 * @param {string} [defaultValue=''] - The default value for the input field.78 * @param {string} [defaultValue=''] - The default value for the input field.
79 * @param {PopupOptions} [popupOptions={}] - Options for the popup.79 * @param {PopupOptions} [popupOptions={}] - Options for the popup.
80 * @return {Promise<string?>} A Promise that resolves with the user's input.80 * @return {Promise<string?>} A Promise that resolves with the user's input.
@@ -591,15 +591,15 @@ class PopupUtils {
591 /**591 /**
592 * Builds popup content with header and text below592 * Builds popup content with header and text below
593 *593 *
594 * @param {string} header - The header to be added to the text594 * @param {string?} header - The header to be added to the text
595 * @param {string} text - The main text content595 * @param {string?} text - The main text content
596 */596 */
597 static BuildTextWithHeader(header, text) {597 static BuildTextWithHeader(header, text) {
598 if (!header) {598 if (!header) {
599 return text;599 return text;
600 }600 }
601 return `<h3>${header}</h3>601 return `<h3>${header}</h3>
602 ${text}`;602 ${text ?? ''}`; // Convert no text to empty string
603 }603 }
604}604}
605605
public/style.css+4 -0
@@ -3031,6 +3031,10 @@ grammarly-extension {
3031 opacity: 1;3031 opacity: 1;
3032}3032}
30333033
3034.avatar-container .avatar-buttons .menu_button {
3035 padding: 3px;
3036}
3037
3034/* Ross should be able to handle this later */3038/* Ross should be able to handle this later */
3035/*.big-avatars .avatar-buttons{3039/*.big-avatars .avatar-buttons{
3036 justify-content: center;3040 justify-content: center;