Add persona titles (#4224) * Add persona titles * Refactor persona title editing

51904c2f1058f19b3698035abd7a607df0245134

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

Signed
5 files changed, +78 -11Showing whitespace changes
public/css/toggle-dependent.css+2 -1
@@ -93,7 +93,8 @@ body.charListGrid #rm_print_characters_block .group_select .ch_name,
9393body.charListGrid #rm_print_characters_block .group_select .group_select_counter,
9494#user_avatar_block.gridView .avatar-container .ch_name,
9595#user_avatar_block.gridView .avatar-container .bogus_folder_counter,
9696#user_avatar_block.gridView .avatar-container .group_select_counter {,
97+#user_avatar_block.gridView .avatar-container .ch_additional_info {
9798 width: 100%;
9899 max-width: 100px;
99100 text-align: center;
public/index.html+1 -0
@@ -5934,6 +5934,7 @@
59345934 <div class="flex-container wide100pLess70px character_select_container">
59355935 <div class="wide100p character_name_block">
59365936 <span class="ch_name flex1"></span>
5937+ <small class="ch_additional_info"></small>
59375938 </div>
59385939 <div class="ch_description"></div>
59395940 <div class="avatar_container_states buttons_block">
public/scripts/personas.js+73 -9
@@ -26,7 +26,7 @@ import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureI
2626import { debounce_timeout } from './constants.js';
2727import { FILTER_TYPES, FilterHelper } from './filters.js';
2828import { groups, selected_group } from './group-chats.js';
2929import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
3030import { t } from './i18n.js';
3131import { openWorldInfoEditor, world_names } from './world-info.js';
3232import { renderTemplateAsync } from './templates.js';
@@ -183,9 +183,11 @@ function getUserAvatarBlock(avatarId) {
183183 const template = $('#user_avatar_template .avatar-container').clone();
184184 const personaName = power_user.personas[avatarId];
185185 const personaDescription = power_user.persona_descriptions[avatarId]?.description;
186+ const personaTitle = power_user.persona_descriptions[avatarId]?.title;
186187
187188 template.find('.ch_name').text(personaName || '[Unnamed Persona]');
188189 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 || '');
189191 template.attr('data-avatar-id', avatarId);
190192 template.find('.avatar').attr('data-avatar-id', avatarId).attr('title', avatarId);
191193 template.toggleClass('default_persona', avatarId === power_user.default_persona);
@@ -209,7 +211,7 @@ function getUserAvatarBlock(avatarId) {
209211function addMissingPersonas(avatarsList) {
210212 for (const persona of avatarsList) {
211213 if (!power_user.personas[persona]) {
212214 initPersona(persona, '[Unnamed Persona]', '', '');
213215 }
214216 }
215217}
@@ -415,23 +417,32 @@ export async function createPersona(avatarId) {
415417
416418 const personaDescription = await Popup.show.input(t`Enter a description for this persona:`, t`You can always add or change it later.`, '', { rows: 4 });
417419
418420 initPersona(avatarId, personaName, personaDescription, '');
419421 if (power_user.persona_show_notifications) {
420422 toastr.success(t`You can now pick ${personaName} as a persona in the Persona Management menu.`, t`Persona Created`);
421423 }
422424}
423425
424426async function createDummyPersona() {
425427 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+ });
426434
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') {
428439 console.debug('User cancelled creating dummy persona');
429440 return;
430441 }
431442
432443 // Date + name (only ASCII) to make it unique
433444 const avatarId = `${Date.now()}-${personaName.replace(/[^a-zA-Z0-9]/g, '')}.png`;
434445 initPersona(avatarId, personaName, '', personaTitle);
435446 await uploadUserAvatar(default_user_avatar, avatarId);
436447}
437448
@@ -440,9 +451,10 @@ async function createDummyPersona() {
440451 * @param {string} avatarId User avatar id
441452 * @param {string} personaName Name for the persona
442453 * @param {string} personaDescription Optional description for the persona
454+ * @param {string} personaTitle Optional title for the persona
443455 * @returns {void}
444456 */
445457export function initPersona(avatarId, personaName, personaDescription, personaTitle) {
446458 power_user.personas[avatarId] = personaName;
447459 power_user.persona_descriptions[avatarId] = {
448460 description: personaDescription || '',
@@ -450,6 +462,7 @@ export function initPersona(avatarId, personaName, personaDescription) {
450462 depth: DEFAULT_DEPTH,
451463 role: DEFAULT_ROLE,
452464 lorebook: '',
465+ title: personaTitle || '',
453466 };
454467
455468 saveSettingsDebounced();
@@ -506,6 +519,7 @@ export async function convertCharacterToPersona(characterId = null) {
506519 depth: DEFAULT_DEPTH,
507520 role: DEFAULT_ROLE,
508521 lorebook: '',
522+ title: '',
509523 };
510524
511525 // If the user is currently using this persona, update the description
@@ -730,13 +744,58 @@ export function autoSelectPersona(name) {
730744}
731745
732746/**
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+/**
733782 * Renames the persona with the given avatar ID by showing a popup to enter a new name.
734783 * @param {string} avatarId - ID of the avatar to rename
735784 * @returns {Promise<boolean>} A promise that resolves to true if the persona was renamed, false otherwise
736785 */
737786async function renamePersona(avatarId) {
738787 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+
740799 if (!newName || newName === currentName) {
741800 console.debug('User cancelled renaming persona or name is unchanged');
742801 return false;
@@ -793,6 +852,7 @@ async function selectCurrentPersona({ toastPersonaNameChange = true } = {}) {
793852 role: DEFAULT_ROLE,
794853 lorebook: '',
795854 connections: [],
855+ title: '',
796856 };
797857 }
798858
@@ -939,6 +999,7 @@ async function lockPersona(type = 'chat') {
939999 role: DEFAULT_ROLE,
9401000 lorebook: '',
9411001 connections: [],
1002+ title: '',
9421003 };
9431004 }
9441005
@@ -1060,6 +1121,7 @@ function onPersonaDescriptionInput() {
10601121 depth: Number($('#persona_depth_value').val()),
10611122 role: Number($('#persona_depth_role').find(':selected').val()),
10621123 lorebook: '',
1124+ title: '',
10631125 };
10641126 power_user.persona_descriptions[user_avatar] = object;
10651127 }
@@ -1166,6 +1228,7 @@ function getOrCreatePersonaDescriptor() {
11661228 role: power_user.persona_description_role,
11671229 lorebook: power_user.persona_description_lorebook,
11681230 connections: [],
1231+ title: '',
11691232 };
11701233 power_user.persona_descriptions[user_avatar] = object;
11711234 }
@@ -1704,6 +1767,7 @@ async function duplicatePersona(avatarId) {
17041767 depth: descriptor?.depth ?? DEFAULT_DEPTH,
17051768 role: descriptor?.role ?? DEFAULT_ROLE,
17061769 lorebook: descriptor?.lorebook ?? '',
1770+ title: descriptor?.title ?? '',
17071771 };
17081772
17091773 await uploadUserAvatar(getUserAvatar(avatarId), newAvatarId);
@@ -1719,7 +1783,7 @@ async function migrateNonPersonaUser() {
17191783 return;
17201784 }
17211785
17221786 initPersona(user_avatar, name1, '', '');
17231787 setPersonaDescription();
17241788 await getUserAvatars(true, user_avatar);
17251789}
public/scripts/popup.js+1 -1
@@ -293,7 +293,7 @@ export class Popup {
293293 label.setAttribute('for', input.id);
294294
295295 const inputElement = document.createElement('input');
296296 inputElement.classList.add('text_pole', 'result-control');
297297 inputElement.type = 'text';
298298 inputElement.id = input.id;
299299 inputElement.value = String(input.defaultState ?? '');
public/style.css+1 -0
@@ -3216,6 +3216,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
32163216 display: block;
32173217}
32183218
3219+.avatar-container .ch_additional_info,
32193220.character_name_block .character_version {
32203221 text-overflow: ellipsis;
32213222 overflow: hidden;