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>

9463d1a1529255b0fd390eeb60f9a4d6fd9ef656

Wolfsblvt <wolfsblvt@gmail.com>

Signed
5 files changed, +58 -29Showing whitespace changes
public/script.js+3 -1
@@ -393,7 +393,7 @@ let isExportPopupOpen = false;
393393
394// Saved here for performance reasons394// Saved here for performance reasons
395const messageTemplate = $('#message_template .mes');395const messageTemplate = $('#message_template .mes');
396const chatElement = $('#chat');396export const chatElement = $('#chat');
397397
398let dialogueResolve = null;398let dialogueResolve = null;
399let dialogueCloseStop = false;399let 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, '');
50445044
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();
public/scripts/extensions/caption/index.js+2 -1
@@ -1,6 +1,6 @@
1import { ensureImageFormatSupported, getBase64Async, getFileExtension, isTrueBoolean, saveBase64AsFile } from '../../utils.js';1import { ensureImageFormatSupported, getBase64Async, getFileExtension, isTrueBoolean, saveBase64AsFile } from '../../utils.js';
2import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';2import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';
3import { appendMediaToMessage, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';3import { appendMediaToMessage, chat_metadata, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';
4import { getMessageTimeStamp } from '../../RossAscends-mods.js';4import { getMessageTimeStamp } from '../../RossAscends-mods.js';
5import { SECRET_KEYS, secret_state } from '../../secrets.js';5import { SECRET_KEYS, secret_state } from '../../secrets.js';
6import { getMultimodalCaption } from '../shared.js';6import { 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);
public/scripts/group-chats.js+9 -8
@@ -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';
80import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js';81import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js';
81import { FILTER_TYPES, FilterHelper } from './filters.js';82import { 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;
240241
241 await loadItemizedPrompts(getCurrentChatId());242 await loadItemizedPrompts(getCurrentChatId());
242243
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 }
271272
272 updateChatMetadata(metadata, true);273 updateChatMetadata(metadata, true);
public/scripts/personas.js+36 -19
@@ -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 */
70export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick));72export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick));
7173
74/** @type {string} The last loaded chat id to remember for persona loading */
75let personaLastLoadedChatId = null;
76
72/** @type {function(string): void} */77/** @type {function(string): void} */
73let navigateToAvatar = () => { };78let navigateToAvatar = () => { };
7479
@@ -107,12 +112,16 @@ export function initUserAvatar(avatar) {
107 * @param {boolean} [options.toastPersonaNameChange=true] Whether to show a toast when the persona name is changed112 * @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 avatar113 * @param {boolean} [options.navigateToCurrent=false] Whether to navigate to the current persona after setting the avatar
109 */114 */
110export function setUserAvatar(imgfile, { toastPersonaNameChange = true, navigateToCurrent = false } = {}) {115export 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 for743 * @param {string} name - The name to search for
735 * @returns {boolean} True if a matching persona was found and selected, false otherwise744 * @returns {Promise<boolean>} True if a matching persona was found and selected, false otherwise
736 */745 */
737export function autoSelectPersona(name) {746export 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 selected1438 * @returns {Promise<boolean>} - A promise that resolves to a boolean indicating whether a persona was selected
1430 */1439 */
1431async function loadPersonaForCurrentChat({ doRender = false } = {}) {1440async 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 exist1445 // Cache persona list to check if they exist
1433 const userAvatars = await getUserAvatars(doRender);1446 const userAvatars = await getUserAvatars(doRender);
14341447
1435 // Check if the user avatar is set and exists in the list of user avatars1448 // 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 }
14401453
1441 // Define a persona for this chat1454 // Define a persona for this chat
@@ -1530,7 +1543,7 @@ async function loadPersonaForCurrentChat({ doRender = false } = {}) {
1530 // Persona avatar found, select it1543 // 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 });
15341547
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 enabled1556 // 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 }
15471560
1548 updatePersonaUIStates();1561 updatePersonaUIStates();
@@ -1606,7 +1619,7 @@ export async function showCharConnections() {
16061619
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() {
17361749
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 */
1742export function retriggerFirstMessageOnEmptyChat() {1753export 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}
17471764
@@ -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 arguments1856 * @param {{mode: 'lookup' | 'temp' | 'all'}} namedArgs Named arguments
1840 * @param {string} name Name to set1857 * @param {string} name Name to set
1841 * @returns {string}1858 * @returns {Promise<string>}
1842 */1859 */
1843function setNameCallback({ mode = 'all' }, name) {1860async 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);
20182035
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 });
20232040
2024 $('#persona_rename_button').on('click', () => renamePersona(user_avatar));2041 $('#persona_rename_button').on('click', () => renamePersona(user_avatar));
public/scripts/slash-commands.js+8 -0
@@ -4585,6 +4585,8 @@ export async function sendMessageAs(args, text) {
4585 insertAt = chat.length + insertAt;4585 insertAt = chat.length + insertAt;
4586 }4586 }
45874587
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 }
46374639
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 };
46874691
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 }
47214727
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();