Chore: Add persona lifecycle events (`PERSONA_CREATED`, `PERSONA_UPDATED`, `PERSONA_RENAMED`, `PERSONA_DELETED`) (#5443) * Add persona lifecycle events (created, updated, renamed, deleted) and emit them throughout persona management functions * fix: await event emissions * fix: await event in convertCharacterToPersona * fix: await PERSONA_UPDATED in callbacks * fix: delete multiple personas without reloading the chat * fix: add CHAT_RENAMED event and adjust welcome screen pin update logic --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -10610,6 +10610,9 @@ export async function renameGroupOrCharacterChat({ characterId, groupId, oldFile | ||
| 10610 | 10610 | if (currentChatId) { |
| 10611 | 10611 | await reloadCurrentChat(); |
| 10612 | 10612 | } |
| 10613 | + | |
| 10614 | + const eventData = { avatarId: body.avatar_url, groupId, oldFileName: body.original_file, newFileName: body.renamed_file }; | |
| 10615 | + await eventSource.emit(event_types.CHAT_RENAMED, eventData); | |
| 10613 | 10616 | } catch { |
| 10614 | 10617 | await delay(500); |
| 10615 | 10618 | await callGenericPopup('An error has occurred. Chat was not renamed.', POPUP_TYPE.TEXT); |
| @@ -50,6 +50,7 @@ export const event_types = { | ||
| 50 | 50 | FORCE_SET_BACKGROUND: 'force_set_background', |
| 51 | 51 | CHAT_DELETED: 'chat_deleted', |
| 52 | 52 | CHAT_CREATED: 'chat_created', |
| 53 | + CHAT_RENAMED: 'chat_renamed', | |
| 53 | 54 | GROUP_CHAT_DELETED: 'group_chat_deleted', |
| 54 | 55 | GROUP_CHAT_CREATED: 'group_chat_created', |
| 55 | 56 | GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts', |
| @@ -97,6 +98,10 @@ export const event_types = { | ||
| 97 | 98 | WORLDINFO_SCAN_DONE: 'worldinfo_scan_done', |
| 98 | 99 | MEDIA_ATTACHMENT_DELETED: 'media_attachment_deleted', |
| 99 | 100 | PERSONA_CHANGED: 'persona_changed', |
| 101 | + PERSONA_CREATED: 'persona_created', | |
| 102 | + PERSONA_UPDATED: 'persona_updated', | |
| 103 | + PERSONA_RENAMED: 'persona_renamed', | |
| 104 | + PERSONA_DELETED: 'persona_deleted', | |
| 100 | 105 | TTS_JOB_STARTED: 'tts_job_started', |
| 101 | 106 | TTS_AUDIO_READY: 'tts_audio_ready', |
| 102 | 107 | TTS_JOB_COMPLETE: 'tts_job_complete', |
| @@ -24,7 +24,7 @@ import { | ||
| 24 | 24 | } from '../script.js'; |
| 25 | 25 | import { persona_description_positions, power_user } from './power-user.js'; |
| 26 | 26 | import { getTokenCountAsync } from './tokenizers.js'; |
| 27 | 27 | import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, parseJsonFile, setInfoBlock, localizePagination, renderPaginationDropdown, paginationDropdownChangeHandler, addLongPressEvent, uuidv4 } from './utils.js'; |
| 28 | 28 | import { debounce_timeout } from './constants.js'; |
| 29 | 29 | import { FILTER_TYPES, FilterHelper } from './filters.js'; |
| 30 | 30 | import { groups, selected_group } from './group-chats.js'; |
| @@ -217,11 +217,12 @@ function getUserAvatarBlock(avatarId) { | ||
| 217 | 217 | /** |
| 218 | 218 | * Initialize missing personas in the power user settings. |
| 219 | 219 | * @param {string[]} avatarsList List of avatar file names |
| 220 | + * @returns {Promise<void>} | |
| 220 | 221 | */ |
| 221 | 222 | async function addMissingPersonas(avatarsList) { |
| 222 | 223 | for (const persona of avatarsList) { |
| 223 | 224 | if (!power_user.personas[persona]) { |
| 224 | 225 | await initPersona(persona, '[Unnamed Persona]', '', '', { silent: true }); |
| 225 | 226 | } |
| 226 | 227 | } |
| 227 | 228 | } |
| @@ -249,7 +250,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') { | ||
| 249 | 250 | } |
| 250 | 251 | |
| 251 | 252 | // If any persona is missing from the power user settings, we add it |
| 252 | 253 | await addMissingPersonas(allEntities); |
| 253 | 254 | // Before printing the personas, we check if we should enable/disable search sorting |
| 254 | 255 | verifyPersonaSearchSortRule(); |
| 255 | 256 | |
| @@ -429,7 +430,7 @@ export async function createPersona(avatarId) { | ||
| 429 | 430 | |
| 430 | 431 | const personaDescription = await Popup.show.input(t`Enter a description for this persona:`, t`You can always add or change it later.`, '', { rows: 4 }); |
| 431 | 432 | |
| 432 | 433 | await initPersona(avatarId, personaName, personaDescription, ''); |
| 433 | 434 | if (power_user.persona_show_notifications) { |
| 434 | 435 | toastr.success(t`You can now pick ${personaName} as a persona in the Persona Management menu.`, t`Persona Created`); |
| 435 | 436 | } |
| @@ -454,7 +455,7 @@ async function createDummyPersona() { | ||
| 454 | 455 | |
| 455 | 456 | // Date + name (only ASCII) to make it unique |
| 456 | 457 | const avatarId = `${Date.now()}-${personaName.replace(/[^a-zA-Z0-9]/g, '')}.png`; |
| 457 | 458 | await initPersona(avatarId, personaName, '', personaTitle); |
| 458 | 459 | await uploadUserAvatar(default_user_avatar, avatarId); |
| 459 | 460 | } |
| 460 | 461 | |
| @@ -464,9 +465,11 @@ async function createDummyPersona() { | ||
| 464 | 465 | * @param {string} personaName Name for the persona |
| 465 | 466 | * @param {string} personaDescription Optional description for the persona |
| 466 | 467 | * @param {string} personaTitle Optional title for the persona |
| 467 | 468 | * @returnsparam {voidobject} [options] Optional settings |
| 469 | + * @param {boolean} [options.silent=false] If true, no PERSONA_CREATED event is emitted (used for background migrations) | |
| 470 | + * @returns {Promise<void>} | |
| 468 | 471 | */ |
| 469 | 472 | export async function initPersona(avatarId, personaName, personaDescription, personaTitle, { silent = false } = {}) { |
| 470 | 473 | power_user.personas[avatarId] = personaName; |
| 471 | 474 | power_user.persona_descriptions[avatarId] = { |
| 472 | 475 | description: personaDescription || '', |
| @@ -478,6 +481,10 @@ export function initPersona(avatarId, personaName, personaDescription, personaTi | ||
| 478 | 481 | }; |
| 479 | 482 | |
| 480 | 483 | saveSettingsDebounced(); |
| 484 | + | |
| 485 | + if (!silent) { | |
| 486 | + await eventSource.emit(event_types.PERSONA_CREATED, { avatarId, name: personaName, description: personaDescription || '', title: personaTitle || '' }); | |
| 487 | + } | |
| 481 | 488 | } |
| 482 | 489 | |
| 483 | 490 | /** |
| @@ -540,6 +547,7 @@ export async function convertCharacterToPersona(characterId = null) { | ||
| 540 | 547 | } |
| 541 | 548 | |
| 542 | 549 | saveSettingsDebounced(); |
| 550 | + await eventSource.emit(event_types.PERSONA_CREATED, { avatarId: overwriteName, name, description, title: '' }); | |
| 543 | 551 | |
| 544 | 552 | console.log('Persona for character created'); |
| 545 | 553 | toastr.success(t`You can now pick ${name} as a persona in the Persona Management menu.`, t`Persona Created`); |
| @@ -778,6 +786,7 @@ async function editPersonaTitle(popup, avatarId, currentTitle) { | ||
| 778 | 786 | delete power_user.persona_descriptions[avatarId].title; |
| 779 | 787 | await getUserAvatars(true, avatarId); |
| 780 | 788 | saveSettingsDebounced(); |
| 789 | + await eventSource.emit(event_types.PERSONA_UPDATED, avatarId); | |
| 781 | 790 | return; |
| 782 | 791 | } |
| 783 | 792 | |
| @@ -786,6 +795,7 @@ async function editPersonaTitle(popup, avatarId, currentTitle) { | ||
| 786 | 795 | console.log(`Updated persona title for ${avatarId} to ${newTitle}`); |
| 787 | 796 | await getUserAvatars(true, avatarId); |
| 788 | 797 | saveSettingsDebounced(); |
| 798 | + await eventSource.emit(event_types.PERSONA_UPDATED, avatarId); | |
| 789 | 799 | return; |
| 790 | 800 | } |
| 791 | 801 | } |
| @@ -821,6 +831,7 @@ async function renamePersona(avatarId) { | ||
| 821 | 831 | } |
| 822 | 832 | |
| 823 | 833 | saveSettingsDebounced(); |
| 834 | + await eventSource.emit(event_types.PERSONA_RENAMED, { avatarId, oldName: currentName, newName }); | |
| 824 | 835 | await getUserAvatars(true, avatarId); |
| 825 | 836 | updatePersonaUIStates(); |
| 826 | 837 | setPersonaDescription(); |
| @@ -1013,6 +1024,7 @@ async function lockPersona(type = 'chat') { | ||
| 1013 | 1024 | connections: [], |
| 1014 | 1025 | title: '', |
| 1015 | 1026 | }; |
| 1027 | + await eventSource.emit(event_types.PERSONA_CREATED, { avatarId: user_avatar, name: name1, description: '', title: '' }); | |
| 1016 | 1028 | } |
| 1017 | 1029 | |
| 1018 | 1030 | switch (type) { |
| @@ -1113,13 +1125,15 @@ async function deleteUserAvatar() { | ||
| 1113 | 1125 | } |
| 1114 | 1126 | |
| 1115 | 1127 | saveSettingsDebounced(); |
| 1128 | + await eventSource.emit(event_types.PERSONA_DELETED, { avatarId, name }); | |
| 1116 | 1129 | |
| 1117 | 1130 | // Use the existing mechanism to re-render the persona list and choose the next persona here |
| 1131 | + personaLastLoadedChatId = uuidv4(); // Force reload by making a dummy chat id | |
| 1118 | 1132 | await loadPersonaForCurrentChat({ doRender: true }); |
| 1119 | 1133 | } |
| 1120 | 1134 | } |
| 1121 | 1135 | |
| 1122 | 1136 | async function onPersonaDescriptionInput() { |
| 1123 | 1137 | power_user.persona_description = String($('#persona_description').val()); |
| 1124 | 1138 | countPersonaDescriptionTokens(); |
| 1125 | 1139 | |
| @@ -1145,25 +1159,35 @@ function onPersonaDescriptionInput() { | ||
| 1145 | 1159 | .text(power_user.persona_description || $('#user_avatar_block').attr('no_desc_text')) |
| 1146 | 1160 | .toggleClass('text_muted', !power_user.persona_description); |
| 1147 | 1161 | saveSettingsDebounced(); |
| 1162 | + | |
| 1163 | + if (power_user.personas[user_avatar]) { | |
| 1164 | + await eventSource.emit(event_types.PERSONA_UPDATED, user_avatar); | |
| 1165 | + } | |
| 1148 | 1166 | } |
| 1149 | 1167 | |
| 1150 | 1168 | async function onPersonaDescriptionDepthValueInput() { |
| 1151 | 1169 | power_user.persona_description_depth = Number($('#persona_depth_value').val()); |
| 1152 | 1170 | |
| 1153 | 1171 | if (power_user.personas[user_avatar]) { |
| 1154 | 1172 | const object = getOrCreatePersonaDescriptor(); |
| 1155 | 1173 | object.depth = power_user.persona_description_depth; |
| 1174 | + saveSettingsDebounced(); | |
| 1175 | + await eventSource.emit(event_types.PERSONA_UPDATED, user_avatar); | |
| 1176 | + return; | |
| 1156 | 1177 | } |
| 1157 | 1178 | |
| 1158 | 1179 | saveSettingsDebounced(); |
| 1159 | 1180 | } |
| 1160 | 1181 | |
| 1161 | 1182 | async function onPersonaDescriptionDepthRoleInput() { |
| 1162 | 1183 | power_user.persona_description_role = Number($('#persona_depth_role').find(':selected').val()); |
| 1163 | 1184 | |
| 1164 | 1185 | if (power_user.personas[user_avatar]) { |
| 1165 | 1186 | const object = getOrCreatePersonaDescriptor(); |
| 1166 | 1187 | object.role = power_user.persona_description_role; |
| 1188 | + saveSettingsDebounced(); | |
| 1189 | + await eventSource.emit(event_types.PERSONA_UPDATED, user_avatar); | |
| 1190 | + return; | |
| 1167 | 1191 | } |
| 1168 | 1192 | |
| 1169 | 1193 | saveSettingsDebounced(); |
| @@ -1200,7 +1224,7 @@ async function onPersonaLoreButtonClick({ shiftKey, altKey }) { | ||
| 1200 | 1224 | worldSelect.append(option); |
| 1201 | 1225 | } |
| 1202 | 1226 | |
| 1203 | 1227 | worldSelect.on('change', async function () { |
| 1204 | 1228 | power_user.persona_description_lorebook = String($(this).val()); |
| 1205 | 1229 | |
| 1206 | 1230 | if (power_user.personas[user_avatar]) { |
| @@ -1210,12 +1234,16 @@ async function onPersonaLoreButtonClick({ shiftKey, altKey }) { | ||
| 1210 | 1234 | |
| 1211 | 1235 | $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook); |
| 1212 | 1236 | saveSettingsDebounced(); |
| 1237 | + | |
| 1238 | + if (power_user.personas[user_avatar]) { | |
| 1239 | + await eventSource.emit(event_types.PERSONA_UPDATED, user_avatar); | |
| 1240 | + } | |
| 1213 | 1241 | }); |
| 1214 | 1242 | |
| 1215 | 1243 | await callGenericPopup(template, POPUP_TYPE.TEXT); |
| 1216 | 1244 | } |
| 1217 | 1245 | |
| 1218 | 1246 | async function onPersonaDescriptionPositionInput() { |
| 1219 | 1247 | power_user.persona_description_position = Number( |
| 1220 | 1248 | $('#persona_description_position').find(':selected').val(), |
| 1221 | 1249 | ); |
| @@ -1223,6 +1251,10 @@ function onPersonaDescriptionPositionInput() { | ||
| 1223 | 1251 | if (power_user.personas[user_avatar]) { |
| 1224 | 1252 | const object = getOrCreatePersonaDescriptor(); |
| 1225 | 1253 | object.position = power_user.persona_description_position; |
| 1254 | + saveSettingsDebounced(); | |
| 1255 | + await eventSource.emit(event_types.PERSONA_UPDATED, user_avatar); | |
| 1256 | + $('#persona_depth_position_settings').toggle(power_user.persona_description_position === persona_description_positions.AT_DEPTH); | |
| 1257 | + return; | |
| 1226 | 1258 | } |
| 1227 | 1259 | |
| 1228 | 1260 | saveSettingsDebounced(); |
| @@ -1807,7 +1839,7 @@ async function migrateNonPersonaUser() { | ||
| 1807 | 1839 | return; |
| 1808 | 1840 | } |
| 1809 | 1841 | |
| 1810 | 1842 | await initPersona(user_avatar, name1, '', '', { silent: true }); |
| 1811 | 1843 | setPersonaDescription(); |
| 1812 | 1844 | await getUserAvatars(true, user_avatar); |
| 1813 | 1845 | } |
| @@ -550,7 +550,6 @@ async function renameRecentCharacterChat(avatarId, fileName) { | ||
| 550 | 550 | newFileName: newName, |
| 551 | 551 | loader: false, |
| 552 | 552 | }); |
| 553 | - PinnedChatsManager.rename({ avatar: avatarId, group: '', file_name: fileName }, newName); | |
| 554 | 553 | await updateRemoteChatName(characterId, newName); |
| 555 | 554 | await refreshWelcomeScreen(); |
| 556 | 555 | toastr.success(t`Chat renamed.`); |
| @@ -584,7 +583,6 @@ async function renameRecentGroupChat(groupId, fileName) { | ||
| 584 | 583 | newFileName: String(newName), |
| 585 | 584 | loader: false, |
| 586 | 585 | }); |
| 587 | - PinnedChatsManager.rename({ avatar: '', group: groupId, file_name: fileName }, String(newName)); | |
| 588 | 586 | await refreshWelcomeScreen(); |
| 589 | 587 | toastr.success(t`Group chat renamed.`); |
| 590 | 588 | } catch (error) { |
| @@ -944,4 +942,8 @@ export function initWelcomeScreen() { | ||
| 944 | 942 | accountStorage.setItem(assistantAvatarKey, newAvatar); |
| 945 | 943 | } |
| 946 | 944 | }); |
| 945 | + | |
| 946 | + eventSource.on(event_types.CHAT_RENAMED, async ({ avatarId, groupId, oldFileName, newFileName }) => { | |
| 947 | + PinnedChatsManager.rename({ avatar: avatarId, group: groupId, file_name: oldFileName }, newFileName); | |
| 948 | + }); | |
| 947 | 949 | } |