Add persona titles (#4224) * Add persona titles * Refactor persona title editing
Signed| @@ -93,7 +93,8 @@ body.charListGrid #rm_print_characters_block .group_select .ch_name, | ||
| 93 | 93 | body.charListGrid #rm_print_characters_block .group_select .group_select_counter, |
| 94 | 94 | #user_avatar_block.gridView .avatar-container .ch_name, |
| 95 | 95 | #user_avatar_block.gridView .avatar-container .bogus_folder_counter, |
| 96 | 96 | #user_avatar_block.gridView .avatar-container .group_select_counter {, |
| 97 | +#user_avatar_block.gridView .avatar-container .ch_additional_info { | |
| 97 | 98 | width: 100%; |
| 98 | 99 | max-width: 100px; |
| 99 | 100 | text-align: center; |
| @@ -5934,6 +5934,7 @@ | ||
| 5934 | 5934 | <div class="flex-container wide100pLess70px character_select_container"> |
| 5935 | 5935 | <div class="wide100p character_name_block"> |
| 5936 | 5936 | <span class="ch_name flex1"></span> |
| 5937 | + <small class="ch_additional_info"></small> | |
| 5937 | 5938 | </div> |
| 5938 | 5939 | <div class="ch_description"></div> |
| 5939 | 5940 | <div class="avatar_container_states buttons_block"> |
| @@ -26,7 +26,7 @@ import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureI | ||
| 26 | 26 | import { debounce_timeout } from './constants.js'; |
| 27 | 27 | import { FILTER_TYPES, FilterHelper } from './filters.js'; |
| 28 | 28 | import { groups, selected_group } from './group-chats.js'; |
| 29 | 29 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| 30 | 30 | import { t } from './i18n.js'; |
| 31 | 31 | import { openWorldInfoEditor, world_names } from './world-info.js'; |
| 32 | 32 | import { renderTemplateAsync } from './templates.js'; |
| @@ -183,9 +183,11 @@ function getUserAvatarBlock(avatarId) { | ||
| 183 | 183 | const template = $('#user_avatar_template .avatar-container').clone(); |
| 184 | 184 | const personaName = power_user.personas[avatarId]; |
| 185 | 185 | const personaDescription = power_user.persona_descriptions[avatarId]?.description; |
| 186 | + const personaTitle = power_user.persona_descriptions[avatarId]?.title; | |
| 186 | 187 | |
| 187 | 188 | template.find('.ch_name').text(personaName || '[Unnamed Persona]'); |
| 188 | 189 | template.find('.ch_description').text(personaDescription || $('#user_avatar_block').attr('no_desc_text')).toggleClass('text_muted', !personaDescription); |
| 190 | + template.find('.ch_additional_info').text(personaTitle || ''); | |
| 189 | 191 | template.attr('data-avatar-id', avatarId); |
| 190 | 192 | template.find('.avatar').attr('data-avatar-id', avatarId).attr('title', avatarId); |
| 191 | 193 | template.toggleClass('default_persona', avatarId === power_user.default_persona); |
| @@ -209,7 +211,7 @@ function getUserAvatarBlock(avatarId) { | ||
| 209 | 211 | function addMissingPersonas(avatarsList) { |
| 210 | 212 | for (const persona of avatarsList) { |
| 211 | 213 | if (!power_user.personas[persona]) { |
| 212 | 214 | initPersona(persona, '[Unnamed Persona]', '', ''); |
| 213 | 215 | } |
| 214 | 216 | } |
| 215 | 217 | } |
| @@ -415,23 +417,32 @@ export async function createPersona(avatarId) { | ||
| 415 | 417 | |
| 416 | 418 | const personaDescription = await Popup.show.input(t`Enter a description for this persona:`, t`You can always add or change it later.`, '', { rows: 4 }); |
| 417 | 419 | |
| 418 | 420 | initPersona(avatarId, personaName, personaDescription, ''); |
| 419 | 421 | if (power_user.persona_show_notifications) { |
| 420 | 422 | toastr.success(t`You can now pick ${personaName} as a persona in the Persona Management menu.`, t`Persona Created`); |
| 421 | 423 | } |
| 422 | 424 | } |
| 423 | 425 | |
| 424 | 426 | async function createDummyPersona() { |
| 425 | 427 | const personaNamepopup = awaitnew Popup.show.input(t`Enter a name for this persona:`, null);POPUP_TYPE.INPUT, '', { |
| 428 | + customInputs: [{ | |
| 429 | + id: 'persona_title', | |
| 430 | + type: 'text', | |
| 431 | + label: t`Persona Title (optional, display only)`, | |
| 432 | + }], | |
| 433 | + }); | |
| 426 | 434 | |
| 427 | - if (!personaName) { | |
| 435 | + const personaName = await popup.show(); | |
| 436 | + const personaTitle = String(popup.inputResults.get('persona_title') || '').trim(); | |
| 437 | + | |
| 438 | + if (!personaName || typeof personaName !== 'string') { | |
| 428 | 439 | console.debug('User cancelled creating dummy persona'); |
| 429 | 440 | return; |
| 430 | 441 | } |
| 431 | 442 | |
| 432 | 443 | // Date + name (only ASCII) to make it unique |
| 433 | 444 | const avatarId = `${Date.now()}-${personaName.replace(/[^a-zA-Z0-9]/g, '')}.png`; |
| 434 | 445 | initPersona(avatarId, personaName, '', personaTitle); |
| 435 | 446 | await uploadUserAvatar(default_user_avatar, avatarId); |
| 436 | 447 | } |
| 437 | 448 | |
| @@ -440,9 +451,10 @@ async function createDummyPersona() { | ||
| 440 | 451 | * @param {string} avatarId User avatar id |
| 441 | 452 | * @param {string} personaName Name for the persona |
| 442 | 453 | * @param {string} personaDescription Optional description for the persona |
| 454 | + * @param {string} personaTitle Optional title for the persona | |
| 443 | 455 | * @returns {void} |
| 444 | 456 | */ |
| 445 | 457 | export function initPersona(avatarId, personaName, personaDescription, personaTitle) { |
| 446 | 458 | power_user.personas[avatarId] = personaName; |
| 447 | 459 | power_user.persona_descriptions[avatarId] = { |
| 448 | 460 | description: personaDescription || '', |
| @@ -450,6 +462,7 @@ export function initPersona(avatarId, personaName, personaDescription) { | ||
| 450 | 462 | depth: DEFAULT_DEPTH, |
| 451 | 463 | role: DEFAULT_ROLE, |
| 452 | 464 | lorebook: '', |
| 465 | + title: personaTitle || '', | |
| 453 | 466 | }; |
| 454 | 467 | |
| 455 | 468 | saveSettingsDebounced(); |
| @@ -506,6 +519,7 @@ export async function convertCharacterToPersona(characterId = null) { | ||
| 506 | 519 | depth: DEFAULT_DEPTH, |
| 507 | 520 | role: DEFAULT_ROLE, |
| 508 | 521 | lorebook: '', |
| 522 | + title: '', | |
| 509 | 523 | }; |
| 510 | 524 | |
| 511 | 525 | // If the user is currently using this persona, update the description |
| @@ -730,13 +744,58 @@ export function autoSelectPersona(name) { | ||
| 730 | 744 | } |
| 731 | 745 | |
| 732 | 746 | /** |
| 747 | + * Edits the title of a persona based on the input from a popup. | |
| 748 | + * @param {Popup} popup Popup instance | |
| 749 | + * @param {string} avatarId Avatar ID of the persona to edit | |
| 750 | + * @param {string} currentTitle Current title of the persona | |
| 751 | + */ | |
| 752 | +async function editPersonaTitle(popup, avatarId, currentTitle) { | |
| 753 | + if (popup.result !== POPUP_RESULT.AFFIRMATIVE) { | |
| 754 | + return; | |
| 755 | + } | |
| 756 | + | |
| 757 | + if (!power_user.persona_descriptions[avatarId]) { | |
| 758 | + console.warn('Uninitialized persona descriptor for avatar:', avatarId); | |
| 759 | + return; | |
| 760 | + } | |
| 761 | + | |
| 762 | + const newTitle = String(popup.inputResults.get('persona_title') || '').trim(); | |
| 763 | + | |
| 764 | + if (!newTitle && currentTitle) { | |
| 765 | + console.log(`Removed persona title for ${avatarId}`); | |
| 766 | + delete power_user.persona_descriptions[avatarId].title; | |
| 767 | + await getUserAvatars(true, avatarId); | |
| 768 | + saveSettingsDebounced(); | |
| 769 | + return; | |
| 770 | + } | |
| 771 | + | |
| 772 | + if (newTitle !== currentTitle) { | |
| 773 | + power_user.persona_descriptions[avatarId].title = newTitle; | |
| 774 | + console.log(`Updated persona title for ${avatarId} to ${newTitle}`); | |
| 775 | + await getUserAvatars(true, avatarId); | |
| 776 | + saveSettingsDebounced(); | |
| 777 | + return; | |
| 778 | + } | |
| 779 | +} | |
| 780 | + | |
| 781 | +/** | |
| 733 | 782 | * Renames the persona with the given avatar ID by showing a popup to enter a new name. |
| 734 | 783 | * @param {string} avatarId - ID of the avatar to rename |
| 735 | 784 | * @returns {Promise<boolean>} A promise that resolves to true if the persona was renamed, false otherwise |
| 736 | 785 | */ |
| 737 | 786 | async function renamePersona(avatarId) { |
| 738 | 787 | const currentName = power_user.personas[avatarId]; |
| 739 | - const newName = await Popup.show.input(t`Rename Persona`, t`Enter a new name for this persona:`, currentName); | |
| 788 | + const currentTitle = power_user.persona_descriptions[avatarId]?.title || ''; | |
| 789 | + const newName = await Popup.show.input(t`Rename Persona`, t`Enter a new name for this persona:`, currentName, { | |
| 790 | + customInputs: [{ | |
| 791 | + id: 'persona_title', | |
| 792 | + type: 'text', | |
| 793 | + label: t`Persona Title (optional, display only)`, | |
| 794 | + defaultState: currentTitle, | |
| 795 | + }], | |
| 796 | + onClose: (popup) => editPersonaTitle(popup, avatarId, currentTitle), | |
| 797 | + }); | |
| 798 | + | |
| 740 | 799 | if (!newName || newName === currentName) { |
| 741 | 800 | console.debug('User cancelled renaming persona or name is unchanged'); |
| 742 | 801 | return false; |
| @@ -793,6 +852,7 @@ async function selectCurrentPersona({ toastPersonaNameChange = true } = {}) { | ||
| 793 | 852 | role: DEFAULT_ROLE, |
| 794 | 853 | lorebook: '', |
| 795 | 854 | connections: [], |
| 855 | + title: '', | |
| 796 | 856 | }; |
| 797 | 857 | } |
| 798 | 858 | |
| @@ -939,6 +999,7 @@ async function lockPersona(type = 'chat') { | ||
| 939 | 999 | role: DEFAULT_ROLE, |
| 940 | 1000 | lorebook: '', |
| 941 | 1001 | connections: [], |
| 1002 | + title: '', | |
| 942 | 1003 | }; |
| 943 | 1004 | } |
| 944 | 1005 | |
| @@ -1060,6 +1121,7 @@ function onPersonaDescriptionInput() { | ||
| 1060 | 1121 | depth: Number($('#persona_depth_value').val()), |
| 1061 | 1122 | role: Number($('#persona_depth_role').find(':selected').val()), |
| 1062 | 1123 | lorebook: '', |
| 1124 | + title: '', | |
| 1063 | 1125 | }; |
| 1064 | 1126 | power_user.persona_descriptions[user_avatar] = object; |
| 1065 | 1127 | } |
| @@ -1166,6 +1228,7 @@ function getOrCreatePersonaDescriptor() { | ||
| 1166 | 1228 | role: power_user.persona_description_role, |
| 1167 | 1229 | lorebook: power_user.persona_description_lorebook, |
| 1168 | 1230 | connections: [], |
| 1231 | + title: '', | |
| 1169 | 1232 | }; |
| 1170 | 1233 | power_user.persona_descriptions[user_avatar] = object; |
| 1171 | 1234 | } |
| @@ -1704,6 +1767,7 @@ async function duplicatePersona(avatarId) { | ||
| 1704 | 1767 | depth: descriptor?.depth ?? DEFAULT_DEPTH, |
| 1705 | 1768 | role: descriptor?.role ?? DEFAULT_ROLE, |
| 1706 | 1769 | lorebook: descriptor?.lorebook ?? '', |
| 1770 | + title: descriptor?.title ?? '', | |
| 1707 | 1771 | }; |
| 1708 | 1772 | |
| 1709 | 1773 | await uploadUserAvatar(getUserAvatar(avatarId), newAvatarId); |
| @@ -1719,7 +1783,7 @@ async function migrateNonPersonaUser() { | ||
| 1719 | 1783 | return; |
| 1720 | 1784 | } |
| 1721 | 1785 | |
| 1722 | 1786 | initPersona(user_avatar, name1, '', ''); |
| 1723 | 1787 | setPersonaDescription(); |
| 1724 | 1788 | await getUserAvatars(true, user_avatar); |
| 1725 | 1789 | } |
| @@ -293,7 +293,7 @@ export class Popup { | ||
| 293 | 293 | label.setAttribute('for', input.id); |
| 294 | 294 | |
| 295 | 295 | const inputElement = document.createElement('input'); |
| 296 | 296 | inputElement.classList.add('text_pole', 'result-control'); |
| 297 | 297 | inputElement.type = 'text'; |
| 298 | 298 | inputElement.id = input.id; |
| 299 | 299 | inputElement.value = String(input.defaultState ?? ''); |
| @@ -3216,6 +3216,7 @@ input[type=search]:focus::-webkit-search-cancel-button { | ||
| 3216 | 3216 | display: block; |
| 3217 | 3217 | } |
| 3218 | 3218 | |
| 3219 | +.avatar-container .ch_additional_info, | |
| 3219 | 3220 | .character_name_block .character_version { |
| 3220 | 3221 | text-overflow: ellipsis; |
| 3221 | 3222 | overflow: hidden; |