Merge pull request #2560 from SillyTavern/dupe-persona Dupe persona
Signed| @@ -6440,6 +6440,9 @@ | ||
| 6440 | 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 | 6441 | <i class="fa-fw fa-solid fa-crown"></i> |
| 6442 | 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 | 6446 | <button class="menu_button delete_avatar" title="Delete persona" data-i18n="[title]Delete persona"> |
| 6444 | 6447 | <i class="fa-fw fa-solid fa-trash-alt"></i> |
| 6445 | 6448 | </button> |
| @@ -1,5 +1,4 @@ | ||
| 1 | 1 | import { |
| 2 | - callPopup, | |
| 3 | 2 | characters, |
| 4 | 3 | chat, |
| 5 | 4 | chat_metadata, |
| @@ -22,7 +21,7 @@ import { PAGINATION_TEMPLATE, debounce, delay, download, ensureImageFormatSuppor | ||
| 22 | 21 | import { debounce_timeout } from './constants.js'; |
| 23 | 22 | import { FILTER_TYPES, FilterHelper } from './filters.js'; |
| 24 | 23 | import { selected_group } from './group-chats.js'; |
| 25 | 24 | import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js'; |
| 26 | 25 | |
| 27 | 26 | let savePersonasPage = 0; |
| 28 | 27 | const GRID_STORAGE_KEY = 'Personas_GridView'; |
| @@ -332,15 +331,14 @@ async function changeUserAvatar(e) { | ||
| 332 | 331 | * @returns {Promise} Promise that resolves when the persona is set |
| 333 | 332 | */ |
| 334 | 333 | export async function createPersona(avatarId) { |
| 335 | 334 | const personaName = await callPopupPopup.show.input('<h3>Enter a name for this persona:</h3>', 'Cancel if you\'re just uploading an avatar.', 'input', ''); |
| 336 | 335 | |
| 337 | 336 | if (!personaName) { |
| 338 | 337 | console.debug('User cancelled creating a persona'); |
| 339 | 338 | return; |
| 340 | 339 | } |
| 341 | 340 | |
| 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 }); | |
| 344 | 342 | |
| 345 | 343 | initPersona(avatarId, personaName, personaDescription); |
| 346 | 344 | if (power_user.persona_show_notifications) { |
| @@ -349,7 +347,7 @@ export async function createPersona(avatarId) { | ||
| 349 | 347 | } |
| 350 | 348 | |
| 351 | 349 | async function createDummyPersona() { |
| 352 | 350 | const personaName = await callPopupPopup.show.input('<h3>Enter a name for this persona:</h3>', 'input', ''null); |
| 353 | 351 | |
| 354 | 352 | if (!personaName) { |
| 355 | 353 | console.debug('User cancelled creating dummy persona'); |
| @@ -508,15 +506,20 @@ async function bindUserNameToPersona(e) { | ||
| 508 | 506 | return; |
| 509 | 507 | } |
| 510 | 508 | |
| 509 | + let personaUnbind = false; | |
| 511 | 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; } }); | |
| 513 | 516 | |
| 514 | 517 | // If the user clicked cancel, don't do anything |
| 515 | 518 | if (personaName === falsenull && !personaUnbind) { |
| 516 | 519 | return; |
| 517 | 520 | } |
| 518 | 521 | |
| 519 | 522 | if (personaName && personaName.length > 0) { |
| 520 | 523 | // If the user clicked ok and entered a name, bind the name to the persona |
| 521 | 524 | console.log(`Binding persona ${avatarId} to name ${personaName}`); |
| 522 | 525 | power_user.personas[avatarId] = personaName; |
| @@ -643,7 +646,12 @@ async function lockPersona() { | ||
| 643 | 646 | ); |
| 644 | 647 | } |
| 645 | 648 | power_user.personas[user_avatar] = name1; |
| 646 | 649 | power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.IN_PROMPT }; |
| 650 | + description: '', | |
| 651 | + position: persona_description_positions.IN_PROMPT, | |
| 652 | + depth: DEFAULT_DEPTH, | |
| 653 | + role: DEFAULT_ROLE, | |
| 654 | + }; | |
| 647 | 655 | } |
| 648 | 656 | |
| 649 | 657 | chat_metadata['persona'] = user_avatar; |
| @@ -672,7 +680,7 @@ async function deleteUserAvatar(e) { | ||
| 672 | 680 | return; |
| 673 | 681 | } |
| 674 | 682 | |
| 675 | 683 | const confirm = await callPopupPopup.show.confirm('<h3>Are you sure you want to delete this avatar?</h3>', 'All information associated with its linked persona will be lost.', 'confirm'); |
| 676 | 684 | |
| 677 | 685 | if (!confirm) { |
| 678 | 686 | console.debug('User cancelled deleting avatar'); |
| @@ -806,7 +814,7 @@ async function setDefaultPersona(e) { | ||
| 806 | 814 | const personaName = power_user.personas[avatarId]; |
| 807 | 815 | |
| 808 | 816 | if (avatarId === currentDefault) { |
| 809 | 817 | const confirm = await callPopupPopup.show.confirm('Are you sure you want to remove the default persona?', 'confirm'personaName); |
| 810 | 818 | |
| 811 | 819 | if (!confirm) { |
| 812 | 820 | console.debug('User cancelled removing default persona'); |
| @@ -819,8 +827,7 @@ async function setDefaultPersona(e) { | ||
| 819 | 827 | } |
| 820 | 828 | delete power_user.default_persona; |
| 821 | 829 | } else { |
| 822 | 830 | const confirm = await callPopupPopup.show.confirm(`<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.'); |
| 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'); | |
| 824 | 831 | |
| 825 | 832 | if (!confirm) { |
| 826 | 833 | console.debug('User cancelled setting default persona'); |
| @@ -978,7 +985,7 @@ async function onPersonasRestoreInput(e) { | ||
| 978 | 985 | } |
| 979 | 986 | |
| 980 | 987 | async function syncUserNameToPersona() { |
| 981 | 988 | const confirmation = await callPopupPopup.show.confirm(`<h3>'Are you sure?</h3>', `All user-sent messages in this chat will be attributed to ${name1}.`, 'confirm'); |
| 982 | 989 | |
| 983 | 990 | if (!confirmation) { |
| 984 | 991 | return; |
| @@ -1001,6 +1008,42 @@ export function retriggerFirstMessageOnEmptyChat() { | ||
| 1001 | 1008 | } |
| 1002 | 1009 | } |
| 1003 | 1010 | |
| 1011 | +/** | |
| 1012 | + * Duplicates a persona. | |
| 1013 | + * @param {string} avatarId | |
| 1014 | + * @returns {Promise<void>} | |
| 1015 | + */ | |
| 1016 | +async 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 | + | |
| 1004 | 1047 | export function initPersonas() { |
| 1005 | 1048 | $(document).on('click', '.bind_user_name', bindUserNameToPersona); |
| 1006 | 1049 | $(document).on('click', '.set_default_persona', setDefaultPersona); |
| @@ -1059,6 +1102,18 @@ export function initPersonas() { | ||
| 1059 | 1102 | $('#avatar_upload_file').trigger('click'); |
| 1060 | 1103 | }); |
| 1061 | 1104 | |
| 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 | 1117 | $(document).on('click', '#user_avatar_block .set_persona_image', function (e) { |
| 1063 | 1118 | e.stopPropagation(); |
| 1064 | 1119 | const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); |
| @@ -73,8 +73,8 @@ const showPopupHelper = { | ||
| 73 | 73 | /** |
| 74 | 74 | * Asynchronously displays an input popup with the given header and text, and returns the user's input. |
| 75 | 75 | * |
| 76 | 76 | * @param {string?} header - The header text for the popup. |
| 77 | 77 | * @param {string?} text - The main text for the popup. |
| 78 | 78 | * @param {string} [defaultValue=''] - The default value for the input field. |
| 79 | 79 | * @param {PopupOptions} [popupOptions={}] - Options for the popup. |
| 80 | 80 | * @return {Promise<string?>} A Promise that resolves with the user's input. |
| @@ -591,15 +591,15 @@ class PopupUtils { | ||
| 591 | 591 | /** |
| 592 | 592 | * Builds popup content with header and text below |
| 593 | 593 | * |
| 594 | 594 | * @param {string?} header - The header to be added to the text |
| 595 | 595 | * @param {string?} text - The main text content |
| 596 | 596 | */ |
| 597 | 597 | static BuildTextWithHeader(header, text) { |
| 598 | 598 | if (!header) { |
| 599 | 599 | return text; |
| 600 | 600 | } |
| 601 | 601 | return `<h3>${header}</h3> |
| 602 | - ${text}`; | |
| 602 | + ${text ?? ''}`; // Convert no text to empty string | |
| 603 | 603 | } |
| 604 | 604 | } |
| 605 | 605 | |
| @@ -3031,6 +3031,10 @@ grammarly-extension { | ||
| 3031 | 3031 | opacity: 1; |
| 3032 | 3032 | } |
| 3033 | 3033 | |
| 3034 | +.avatar-container .avatar-buttons .menu_button { | |
| 3035 | + padding: 3px; | |
| 3036 | +} | |
| 3037 | + | |
| 3034 | 3038 | /* Ross should be able to handle this later */ |
| 3035 | 3039 | /*.big-avatars .avatar-buttons{ |
| 3036 | 3040 | justify-content: center; |