Merge pull request #2560 from SillyTavern/dupe-persona Dupe persona
Signed| @@ -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> |
| @@ -1,5 +1,4 @@ | |||
| 1 | import { | 1 | import { |
| 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 | |||
| 22 | import { debounce_timeout } from './constants.js'; | 21 | import { debounce_timeout } from './constants.js'; |
| 23 | import { FILTER_TYPES, FilterHelper } from './filters.js'; | 22 | import { FILTER_TYPES, FilterHelper } from './filters.js'; |
| 24 | import { selected_group } from './group-chats.js'; | 23 | import { selected_group } from './group-chats.js'; |
| 25 | import { POPUP_TYPE, Popup } from './popup.js'; | 24 | import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js'; |
| 26 | 25 | ||
| 27 | let savePersonasPage = 0; | 26 | let savePersonasPage = 0; |
| 28 | const GRID_STORAGE_KEY = 'Personas_GridView'; | 27 | const GRID_STORAGE_KEY = 'Personas_GridView'; |
| @@ -332,15 +331,14 @@ async function changeUserAvatar(e) { | |||
| 332 | * @returns {Promise} Promise that resolves when the persona is set | 331 | * @returns {Promise} Promise that resolves when the persona is set |
| 333 | */ | 332 | */ |
| 334 | export async function createPersona(avatarId) { | 333 | export 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.', ''); |
| 336 | 335 | ||
| 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 | } |
| 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 | 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 | } |
| 350 | 348 | ||
| 351 | async function createDummyPersona() { | 349 | async 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); |
| 353 | 351 | ||
| 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 | } |
| 510 | 508 | ||
| 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; } }); | ||
| 513 | 516 | ||
| 514 | // If the user clicked cancel, don't do anything | 517 | // If the user clicked cancel, don't do anything |
| 515 | if (personaName === false) { | 518 | if (personaName === null && !personaUnbind) { |
| 516 | return; | 519 | return; |
| 517 | } | 520 | } |
| 518 | 521 | ||
| 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 persona | 523 | // 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 | } |
| 648 | 656 | ||
| 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 | } |
| 674 | 682 | ||
| 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.'); |
| 676 | 684 | ||
| 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]; |
| 807 | 815 | ||
| 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); |
| 810 | 818 | ||
| 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'); | ||
| 824 | 831 | ||
| 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 | } |
| 979 | 986 | ||
| 980 | async function syncUserNameToPersona() { | 987 | async 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}.`); |
| 982 | 989 | ||
| 983 | if (!confirmation) { | 990 | if (!confirmation) { |
| 984 | return; | 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 | export function initPersonas() { | 1047 | export 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 | }); |
| 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 | $(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'); |
| @@ -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 below | 592 | * Builds popup content with header and text below |
| 593 | * | 593 | * |
| 594 | * @param {string} header - The header to be added to the text | 594 | * @param {string?} header - The header to be added to the text |
| 595 | * @param {string} text - The main text content | 595 | * @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 | } |
| 605 | 605 | ||
| @@ -3031,6 +3031,10 @@ grammarly-extension { | |||
| 3031 | opacity: 1; | 3031 | opacity: 1; |
| 3032 | } | 3032 | } |
| 3033 | 3033 | ||
| 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; |