Reprint non-tainted group chat on switching personas (#4603) * fix: reprint non-tainted group chat on switching personas * feat: mark chat as tainted when sending user message * feat: mark chat as tainted when adding custom messages or captions * feat: export createOrEditCharacter function and integrate it into retriggerFirstMessageOnEmptyChat * fix: optimize loadPersonaForCurrentChat by storing current chat ID in a variable * Improve conditions of first message retrigger --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -393,7 +393,7 @@ let isExportPopupOpen = false; | ||
| 393 | 393 | |
| 394 | 394 | // Saved here for performance reasons |
| 395 | 395 | const messageTemplate = $('#message_template .mes'); |
| 396 | 396 | export const chatElement = $('#chat'); |
| 397 | 397 | |
| 398 | 398 | let dialogueResolve = null; |
| 399 | 399 | let dialogueCloseStop = false; |
| @@ -5042,6 +5042,8 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul | ||
| 5042 | 5042 | await populateFileAttachment(message); |
| 5043 | 5043 | statMesProcess(message, 'user', characters, this_chid, ''); |
| 5044 | 5044 | |
| 5045 | + chat_metadata['tainted'] = true; | |
| 5046 | + | |
| 5045 | 5047 | if (typeof insertAt === 'number' && insertAt >= 0 && insertAt <= chat.length) { |
| 5046 | 5048 | chat.splice(insertAt, 0, message); |
| 5047 | 5049 | await saveChatConditional(); |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | import { ensureImageFormatSupported, getBase64Async, getFileExtension, isTrueBoolean, saveBase64AsFile } from '../../utils.js'; |
| 2 | 2 | import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 3 | 3 | import { appendMediaToMessage, chat_metadata, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js'; |
| 4 | 4 | import { getMessageTimeStamp } from '../../RossAscends-mods.js'; |
| 5 | 5 | import { SECRET_KEYS, secret_state } from '../../secrets.js'; |
| 6 | 6 | import { getMultimodalCaption } from '../shared.js'; |
| @@ -174,6 +174,7 @@ async function sendCaptionedMessage(caption, image) { | ||
| 174 | 174 | inline_image: !!extension_settings.caption.show_in_chat, |
| 175 | 175 | }, |
| 176 | 176 | }; |
| 177 | + chat_metadata['tainted'] = true; | |
| 177 | 178 | context.chat.push(message); |
| 178 | 179 | const messageId = context.chat.length - 1; |
| 179 | 180 | await eventSource.emit(event_types.MESSAGE_SENT, messageId); |
| @@ -76,6 +76,7 @@ import { | ||
| 76 | 76 | depth_prompt_role_default, |
| 77 | 77 | shouldAutoContinue, |
| 78 | 78 | unshallowCharacter, |
| 79 | + chatElement, | |
| 79 | 80 | } from '../script.js'; |
| 80 | 81 | import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js'; |
| 81 | 82 | import { FILTER_TYPES, FilterHelper } from './filters.js'; |
| @@ -236,37 +237,37 @@ export async function getGroupChat(groupId, reload = false) { | ||
| 236 | 237 | const chat_id = group.chat_id; |
| 237 | 238 | const data = await loadGroupChat(chat_id); |
| 238 | 239 | const metadata = group.chat_metadata ?? {}; |
| 239 | 240 | letconst freshChat = false!metadata.tainted; |
| 240 | 241 | |
| 241 | 242 | await loadItemizedPrompts(getCurrentChatId()); |
| 242 | 243 | |
| 243 | 244 | if (group && Array.isArray(datagroup.members) && data.lengthfreshChat) { |
| 244 | - data[0].is_group = true; | |
| 245 | + chat.splice(0, chat.length); | |
| 245 | - chat.splice(0, chat.length, ...data); | |
| 246 | + chatElement.find('.mes').remove(); | |
| 246 | - await printMessages(); | |
| 247 | + for (let member of group.members) { | |
| 247 | - } else { | |
| 248 | + const character = characters.find(x => x.avatar === member || x.name === member); | |
| 248 | - freshChat = !metadata.tainted; | |
| 249 | + if (!character) { | |
| 249 | - if (group && Array.isArray(group.members) && freshChat) { | |
| 250 | + continue; | |
| 250 | - for (let member of group.members) { | |
| 251 | + } | |
| 251 | - const character = characters.find(x => x.avatar === member || x.name === member); | |
| 252 | - if (!character) { | |
| 253 | - continue; | |
| 254 | - } | |
| 255 | - | |
| 256 | - const mes = await getFirstCharacterMessage(character); | |
| 257 | 252 | |
| 258 | - // No first message | |
| 253 | + const mes = await getFirstCharacterMessage(character); | |
| 259 | - if (!(mes?.mes)) { | |
| 260 | - continue; | |
| 261 | - } | |
| 262 | 254 | |
| 263 | - chat.push(mes); | |
| 255 | + // No first message | |
| 264 | - await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1), 'first_message'); | |
| 256 | + if (!(mes?.mes)) { | |
| 265 | - addOneMessage(mes); | |
| 257 | + continue; | |
| 266 | - await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1), 'first_message'); | |
| 267 | 258 | } |
| 268 | - await saveGroupChat(groupId, false); | |
| 259 | + | |
| 260 | + chat.push(mes); | |
| 261 | + await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1), 'first_message'); | |
| 262 | + addOneMessage(mes); | |
| 263 | + await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1), 'first_message'); | |
| 269 | 264 | } |
| 265 | + await saveGroupChat(groupId, false); | |
| 266 | + } else if (Array.isArray(data) && data.length) { | |
| 267 | + data[0].is_group = true; | |
| 268 | + chat.splice(0, chat.length, ...data); | |
| 269 | + chatElement.find('.mes').remove(); | |
| 270 | + await printMessages(); | |
| 270 | 271 | } |
| 271 | 272 | |
| 272 | 273 | updateChatMetadata(metadata, true); |
| @@ -4,9 +4,11 @@ import { | ||
| 4 | 4 | characters, |
| 5 | 5 | chat, |
| 6 | 6 | chat_metadata, |
| 7 | + createOrEditCharacter, | |
| 7 | 8 | default_user_avatar, |
| 8 | 9 | eventSource, |
| 9 | 10 | event_types, |
| 11 | + getCurrentChatId, | |
| 10 | 12 | getRequestHeaders, |
| 11 | 13 | getThumbnailUrl, |
| 12 | 14 | groupToEntity, |
| @@ -69,6 +71,9 @@ export let user_avatar = ''; | ||
| 69 | 71 | /** @type {FilterHelper} Filter helper for the persona list */ |
| 70 | 72 | export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick)); |
| 71 | 73 | |
| 74 | +/** @type {string} The last loaded chat id to remember for persona loading */ | |
| 75 | +let personaLastLoadedChatId = null; | |
| 76 | + | |
| 72 | 77 | /** @type {function(string): void} */ |
| 73 | 78 | let navigateToAvatar = () => { }; |
| 74 | 79 | |
| @@ -107,12 +112,16 @@ export function initUserAvatar(avatar) { | ||
| 107 | 112 | * @param {boolean} [options.toastPersonaNameChange=true] Whether to show a toast when the persona name is changed |
| 108 | 113 | * @param {boolean} [options.navigateToCurrent=false] Whether to navigate to the current persona after setting the avatar |
| 109 | 114 | */ |
| 110 | 115 | export async function setUserAvatar(imgfile, { toastPersonaNameChange = true, navigateToCurrent = false } = {}) { |
| 116 | + const currentUserAvatar = user_avatar; | |
| 111 | 117 | user_avatar = imgfile && typeof imgfile === 'string' ? imgfile : $(this).attr('data-avatar-id'); |
| 118 | + if (currentUserAvatar === user_avatar) { | |
| 119 | + return; | |
| 120 | + } | |
| 112 | 121 | reloadUserAvatar(); |
| 113 | 122 | updatePersonaUIStates({ navigateToCurrent: navigateToCurrent }); |
| 114 | 123 | selectCurrentPersona({ toastPersonaNameChange: toastPersonaNameChange }); |
| 115 | 124 | await retriggerFirstMessageOnEmptyChat(); |
| 116 | 125 | saveSettingsDebounced(); |
| 117 | 126 | $('.zoomed_avatar[forchar]').remove(); |
| 118 | 127 | } |
| @@ -732,13 +741,13 @@ export async function askForPersonaSelection(title, text, personas, { okButton = | ||
| 732 | 741 | /** |
| 733 | 742 | * Automatically selects a persona based on the given name if a matching persona exists. |
| 734 | 743 | * @param {string} name - The name to search for |
| 735 | 744 | * @returns {Promise<boolean>} True if a matching persona was found and selected, false otherwise |
| 736 | 745 | */ |
| 737 | 746 | export async function autoSelectPersona(name) { |
| 738 | 747 | for (const [key, value] of Object.entries(power_user.personas)) { |
| 739 | 748 | if (value === name) { |
| 740 | 749 | console.log(`Auto-selecting persona ${key} for name ${name}`); |
| 741 | 750 | await setUserAvatar(key); |
| 742 | 751 | return true; |
| 743 | 752 | } |
| 744 | 753 | } |
| @@ -1429,13 +1438,17 @@ function getPersonaTemporaryLockInfo() { | ||
| 1429 | 1438 | * @returns {Promise<boolean>} - A promise that resolves to a boolean indicating whether a persona was selected |
| 1430 | 1439 | */ |
| 1431 | 1440 | async function loadPersonaForCurrentChat({ doRender = false } = {}) { |
| 1441 | + const currentChatId = getCurrentChatId(); | |
| 1442 | + if (currentChatId === personaLastLoadedChatId) return; | |
| 1443 | + personaLastLoadedChatId = currentChatId; | |
| 1444 | + | |
| 1432 | 1445 | // Cache persona list to check if they exist |
| 1433 | 1446 | const userAvatars = await getUserAvatars(doRender); |
| 1434 | 1447 | |
| 1435 | 1448 | // Check if the user avatar is set and exists in the list of user avatars |
| 1436 | 1449 | if (userAvatars.length && !userAvatars.includes(user_avatar)) { |
| 1437 | 1450 | console.log(`User avatar ${user_avatar} not found in user avatars list, pick the first available one`); |
| 1438 | 1451 | await setUserAvatar(userAvatars[0], { toastPersonaNameChange: false, navigateToCurrent: true }); |
| 1439 | 1452 | } |
| 1440 | 1453 | |
| 1441 | 1454 | // Define a persona for this chat |
| @@ -1530,7 +1543,7 @@ async function loadPersonaForCurrentChat({ doRender = false } = {}) { | ||
| 1530 | 1543 | // Persona avatar found, select it |
| 1531 | 1544 | if (chatPersona && user_avatar !== chatPersona) { |
| 1532 | 1545 | const willAutoLock = power_user.persona_auto_lock && user_avatar !== chat_metadata['persona']; |
| 1533 | 1546 | await setUserAvatar(chatPersona, { toastPersonaNameChange: false, navigateToCurrent: true }); |
| 1534 | 1547 | |
| 1535 | 1548 | if (power_user.persona_show_notifications) { |
| 1536 | 1549 | let message = t`Auto-selected persona based on ${connectType} connection.<br />Your messages will now be sent as ${power_user.personas[chatPersona]}.`; |
| @@ -1542,7 +1555,7 @@ async function loadPersonaForCurrentChat({ doRender = false } = {}) { | ||
| 1542 | 1555 | } |
| 1543 | 1556 | // Even if it's the same persona, we still might need to auto-lock to chat if that's enabled |
| 1544 | 1557 | else if (chatPersona && power_user.persona_auto_lock && !chat_metadata['persona']) { |
| 1545 | 1558 | await lockPersona('chat'); |
| 1546 | 1559 | } |
| 1547 | 1560 | |
| 1548 | 1561 | updatePersonaUIStates(); |
| @@ -1606,7 +1619,7 @@ export async function showCharConnections() { | ||
| 1606 | 1619 | |
| 1607 | 1620 | // One of the persona was selected. So load it. |
| 1608 | 1621 | if (!isRemoving && selectedPersona) { |
| 1609 | 1622 | await setUserAvatar(selectedPersona, { toastPersonaNameChange: false }); |
| 1610 | 1623 | if (power_user.persona_show_notifications) { |
| 1611 | 1624 | toastr.success(t`Selected persona ${power_user.personas[selectedPersona]} for current chat.`, t`Connected Persona Selected`); |
| 1612 | 1625 | } |
| @@ -1736,12 +1749,16 @@ async function syncUserNameToPersona() { | ||
| 1736 | 1749 | |
| 1737 | 1750 | /** |
| 1738 | 1751 | * Retriggers the first message to reload it from the char definition. |
| 1739 | - * | |
| 1740 | - * Only works if only the first message is present, and not in group mode. | |
| 1741 | 1752 | */ |
| 1742 | 1753 | export async function retriggerFirstMessageOnEmptyChat() { |
| 1743 | - if (Number(this_chid) >= 0 && !selected_group && chat.length === 1) { | |
| 1754 | + if (chat_metadata.tainted) { | |
| 1744 | - $('#firstmessage_textarea').trigger('input'); | |
| 1755 | + return; | |
| 1756 | + } | |
| 1757 | + if (selected_group) { | |
| 1758 | + await reloadCurrentChat(); | |
| 1759 | + } | |
| 1760 | + if (!selected_group && Number(this_chid) >= 0 && chat.length === 1) { | |
| 1761 | + await createOrEditCharacter(); | |
| 1745 | 1762 | } |
| 1746 | 1763 | } |
| 1747 | 1764 | |
| @@ -1838,9 +1855,9 @@ async function lockPersonaCallback(_args, value) { | ||
| 1838 | 1855 | * Sets a persona name and optionally an avatar. |
| 1839 | 1856 | * @param {{mode: 'lookup' | 'temp' | 'all'}} namedArgs Named arguments |
| 1840 | 1857 | * @param {string} name Name to set |
| 1841 | 1858 | * @returns {Promise<string>} |
| 1842 | 1859 | */ |
| 1843 | 1860 | async function setNameCallback({ mode = 'all' }, name) { |
| 1844 | 1861 | if (!name) { |
| 1845 | 1862 | toastr.warning('You must specify a name to change to'); |
| 1846 | 1863 | return ''; |
| @@ -1858,7 +1875,7 @@ function setNameCallback({ mode = 'all' }, name) { | ||
| 1858 | 1875 | let persona = Object.entries(power_user.personas).find(([avatar, _]) => avatar === name)?.[1]; |
| 1859 | 1876 | if (!persona) persona = Object.entries(power_user.personas).find(([_, personaName]) => personaName.toLowerCase() === name.toLowerCase())?.[1]; |
| 1860 | 1877 | if (persona) { |
| 1861 | 1878 | await autoSelectPersona(persona); |
| 1862 | 1879 | return ''; |
| 1863 | 1880 | } else if (mode === 'lookup') { |
| 1864 | 1881 | toastr.warning(`Persona ${name} not found`); |
| @@ -2016,9 +2033,9 @@ export async function initPersonas() { | ||
| 2016 | 2033 | $('#sync_name_button').on('click', syncUserNameToPersona); |
| 2017 | 2034 | $('#avatar_upload_file').on('change', changeUserAvatar); |
| 2018 | 2035 | |
| 2019 | 2036 | $(document).on('click', '#user_avatar_block .avatar-container', async function () { |
| 2020 | 2037 | const imgfile = $(this).attr('data-avatar-id'); |
| 2021 | 2038 | await setUserAvatar(imgfile); |
| 2022 | 2039 | }); |
| 2023 | 2040 | |
| 2024 | 2041 | $('#persona_rename_button').on('click', () => renamePersona(user_avatar)); |
| @@ -4585,6 +4585,8 @@ export async function sendMessageAs(args, text) { | ||
| 4585 | 4585 | insertAt = chat.length + insertAt; |
| 4586 | 4586 | } |
| 4587 | 4587 | |
| 4588 | + chat_metadata['tainted'] = true; | |
| 4589 | + | |
| 4588 | 4590 | if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { |
| 4589 | 4591 | chat.splice(insertAt, 0, message); |
| 4590 | 4592 | await saveChatConditional(); |
| @@ -4635,6 +4637,8 @@ export async function sendNarratorMessage(args, text) { | ||
| 4635 | 4637 | insertAt = chat.length + insertAt; |
| 4636 | 4638 | } |
| 4637 | 4639 | |
| 4640 | + chat_metadata['tainted'] = true; | |
| 4641 | + | |
| 4638 | 4642 | if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { |
| 4639 | 4643 | chat.splice(insertAt, 0, message); |
| 4640 | 4644 | await saveChatConditional(); |
| @@ -4685,6 +4689,8 @@ export async function promptQuietForLoudResponse(who, text) { | ||
| 4685 | 4689 | }, |
| 4686 | 4690 | }; |
| 4687 | 4691 | |
| 4692 | + chat_metadata['tainted'] = true; | |
| 4693 | + | |
| 4688 | 4694 | chat.push(message); |
| 4689 | 4695 | await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); |
| 4690 | 4696 | addOneMessage(message); |
| @@ -4719,6 +4725,8 @@ async function sendCommentMessage(args, text) { | ||
| 4719 | 4725 | insertAt = chat.length + insertAt; |
| 4720 | 4726 | } |
| 4721 | 4727 | |
| 4728 | + chat_metadata['tainted'] = true; | |
| 4729 | + | |
| 4722 | 4730 | if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { |
| 4723 | 4731 | chat.splice(insertAt, 0, message); |
| 4724 | 4732 | await saveChatConditional(); |