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