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