import { shuffle, onlyUnique, debounce, delay, isDataURL, createThumbnail, extractAllWords, saveBase64AsFile, PAGINATION_TEMPLATE, getBase64Async, resetScrollHeight, initScrollHeight, } from './utils.js'; import { RA_CountCharTokens, humanizedDateTime, dragElement, favsToHotswap, getMessageTimeStamp } from './RossAscends-mods.js'; import { power_user, loadMovingUIState, sortEntitiesList } from './power-user.js'; import { debounce_timeout } from './constants.js'; import { chat, sendSystemMessage, printMessages, substituteParams, characters, default_avatar, addOneMessage, clearChat, Generate, select_rm_info, setCharacterId, setCharacterName, setEditedMessageId, is_send_press, name1, resetChatState, setSendButtonState, getCharacters, system_message_types, online_status, talkativeness_default, selectRightMenuWithAnimation, deleteLastMessage, showSwipeButtons, hideSwipeButtons, chat_metadata, updateChatMetadata, isStreamingEnabled, getThumbnailUrl, getRequestHeaders, setMenuType, menu_type, select_selected_character, cancelTtsPlay, displayPastChats, sendMessageAsUser, getBiasStrings, saveChatConditional, deactivateSendButtons, activateSendButtons, eventSource, event_types, getCurrentChatId, setScenarioOverride, system_avatar, isChatSaving, setExternalAbortController, baseChatReplace, depth_prompt_depth_default, loadItemizedPrompts, animation_duration, depth_prompt_role_default, shouldAutoContinue, this_chid, } from '../script.js'; import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js'; import { FILTER_TYPES, FilterHelper } from './filters.js'; import { isExternalMediaAllowed } from './chats.js'; import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; export { selected_group, is_group_automode_enabled, hideMutedSprites, is_group_generating, group_generation_id, groups, saveGroupChat, generateGroupWrapper, deleteGroup, getGroupAvatar, getGroups, regenerateGroup, resetSelectedGroup, select_group_chats, getGroupChatNames, }; let is_group_generating = false; // Group generation flag let is_group_automode_enabled = false; let hideMutedSprites = true; let groups = []; let selected_group = null; let group_generation_id = null; let fav_grp_checked = false; let openGroupId = null; let newGroupMembers = []; export const group_activation_strategy = { NATURAL: 0, LIST: 1, }; export const group_generation_mode = { SWAP: 0, APPEND: 1, APPEND_DISABLED: 2, }; const DEFAULT_AUTO_MODE_DELAY = 5; export const groupCandidatesFilter = new FilterHelper(debounce(printGroupCandidates, debounce_timeout.quick)); let autoModeWorker = null; const saveGroupDebounced = debounce(async (group, reload) => await _save(group, reload), debounce_timeout.relaxed); /** @type {Map} */ let groupChatQueueOrder = new Map(); function setAutoModeWorker() { clearInterval(autoModeWorker); const autoModeDelay = groups.find(x => x.id === selected_group)?.auto_mode_delay ?? DEFAULT_AUTO_MODE_DELAY; autoModeWorker = setInterval(groupChatAutoModeWorker, autoModeDelay * 1000); } async function _save(group, reload = true) { await fetch('/api/groups/edit', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify(group), }); if (reload) { await getCharacters(); } } // Group chats async function regenerateGroup() { let generationId = getLastMessageGenerationId(); while (chat.length > 0) { const lastMes = chat[chat.length - 1]; const this_generationId = lastMes.extra?.gen_id; // for new generations after the update if ((generationId && this_generationId) && generationId !== this_generationId) { break; } // legacy for generations before the update else if (lastMes.is_user || lastMes.is_system) { break; } await deleteLastMessage(); } const abortController = new AbortController(); setExternalAbortController(abortController); generateGroupWrapper(false, 'normal', { signal: abortController.signal }); } async function loadGroupChat(chatId) { const response = await fetch('/api/chats/group/get', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ id: chatId }), }); if (response.ok) { const data = await response.json(); return data; } return []; } async function validateGroup(group) { if (!group) return; // Validate that all members exist as characters let dirty = false; group.members = group.members.filter(member => { const character = characters.find(x => x.avatar === member || x.name === member); if (!character) { const msg = `Warning: Listed member ${member} does not exist as a character. It will be removed from the group.`; toastr.warning(msg, 'Group Validation'); console.warn(msg); dirty = true; } return character; }); if (dirty) { await editGroup(group.id, true, false); } } export async function getGroupChat(groupId, reload = false) { const group = groups.find((x) => x.id === groupId); if (!group) { console.warn('Group not found', groupId); return; } // Run validation before any loading validateGroup(group); const chat_id = group.chat_id; const data = await loadGroupChat(chat_id); let freshChat = false; await loadItemizedPrompts(getCurrentChatId()); if (Array.isArray(data) && data.length) { data[0].is_group = true; chat.splice(0, chat.length, ...data); await printMessages(); } else { sendSystemMessage(system_message_types.GROUP, '', { isSmallSys: true }); await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1)); await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1)); if (group && Array.isArray(group.members)) { for (let member of group.members) { const character = characters.find(x => x.avatar === member || x.name === member); if (!character) { continue; } const mes = await getFirstCharacterMessage(character); // No first message if (!(mes?.mes)) { continue; } chat.push(mes); await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1)); addOneMessage(mes); await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1)); } } await saveGroupChat(groupId, false); freshChat = true; } let metadata = group.chat_metadata ?? {}; updateChatMetadata(metadata, true); if (reload) { select_group_chats(groupId, true); } await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); if (freshChat) await eventSource.emit(event_types.GROUP_CHAT_CREATED); } /** * Retrieves the members of a group * * @param {string} [groupId=selected_group] - The ID of the group to retrieve members from. Defaults to the currently selected group. * @returns {import('../script.js').Character[]} An array of character objects representing the members of the group. If the group is not found, an empty array is returned. */ export function getGroupMembers(groupId = selected_group) { const group = groups.find((x) => x.id === groupId); return group?.members.map(member => characters.find(x => x.avatar === member)) ?? []; } /** * Finds the character ID for a group member. * @param {string} arg 0-based member index or character name * @returns {number} 0-based character ID */ export function findGroupMemberId(arg) { arg = arg?.trim(); if (!arg) { console.warn('WARN: No argument provided for findGroupMemberId'); return; } const group = groups.find(x => x.id == selected_group); if (!group || !Array.isArray(group.members)) { console.warn('WARN: No group found for selected group ID'); return; } const index = parseInt(arg); const searchByName = isNaN(index); if (searchByName) { const memberNames = group.members.map(x => ({ name: characters.find(y => y.avatar === x)?.name, index: characters.findIndex(y => y.avatar === x) })); const fuse = new Fuse(memberNames, { keys: ['name'] }); const result = fuse.search(arg); if (!result.length) { console.warn(`WARN: No group member found with name ${arg}`); return; } const chid = result[0].item.index; if (chid === -1) { console.warn(`WARN: No character found for group member ${arg}`); return; } console.log(`Triggering group member ${chid} (${arg}) from search result`, result[0]); return chid; } else { const memberAvatar = group.members[index]; if (memberAvatar === undefined) { console.warn(`WARN: No group member found at index ${index}`); return; } const chid = characters.findIndex(x => x.avatar === memberAvatar); if (chid === -1) { console.warn(`WARN: No character found for group member ${memberAvatar} at index ${index}`); return; } console.log(`Triggering group member ${memberAvatar} at index ${index}`); return chid; } } /** * Gets depth prompts for group members. * @param {string} groupId Group ID * @param {number} characterId Current Character ID * @returns {{depth: number, text: string, role: string}[]} Array of depth prompts */ export function getGroupDepthPrompts(groupId, characterId) { if (!groupId) { return []; } console.debug('getGroupDepthPrompts entered for group: ', groupId); const group = groups.find(x => x.id === groupId); if (!group || !Array.isArray(group.members) || !group.members.length) { return []; } if (group.generation_mode === group_generation_mode.SWAP) { return []; } const depthPrompts = []; for (const member of group.members) { const index = characters.findIndex(x => x.avatar === member); const character = characters[index]; if (index === -1 || !character) { console.debug(`Skipping missing member: ${member}`); continue; } if (group.disabled_members.includes(member) && characterId !== index) { console.debug(`Skipping disabled group member: ${member}`); continue; } const depthPromptText = baseChatReplace(character.data?.extensions?.depth_prompt?.prompt?.trim(), name1, character.name) || ''; const depthPromptDepth = character.data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default; const depthPromptRole = character.data?.extensions?.depth_prompt?.role ?? depth_prompt_role_default; if (depthPromptText) { depthPrompts.push({ text: depthPromptText, depth: depthPromptDepth, role: depthPromptRole }); } } return depthPrompts; } /** * Combines group members cards into a single string. Only for groups with generation mode set to APPEND or APPEND_DISABLED. * @param {string} groupId Group ID * @param {number} characterId Current Character ID * @returns {{description: string, personality: string, scenario: string, mesExamples: string}} Group character cards combined */ export function getGroupCharacterCards(groupId, characterId) { console.debug('getGroupCharacterCards entered for group: ', groupId); const group = groups.find(x => x.id === groupId); if (!group || !group?.generation_mode || !Array.isArray(group.members) || !group.members.length) { return null; } /** * Runs the macro engine on a text, with custom replace * @param {string} value Value to replace * @param {string} fieldName Name of the field * @param {string} characterName Name of the character * @returns {string} Replaced text * */ function customBaseChatReplace(value, fieldName, characterName) { if (!value) { return ''; } // We should do the custom field name replacement first, and then run it through the normal macro engine with provided names value = value.replace(//gi, fieldName); return baseChatReplace(value.trim(), name1, characterName); } /** * Prepares text with prefix/suffix for a character field * @param {string} value Value to replace * @param {string} characterName Name of the character * @param {string} fieldName Name of the field * @returns {string} Prepared text * */ function replaceAndPrepareForJoin(value, characterName, fieldName) { value = value.trim(); if (!value) { return ''; } // Prepare and replace prefixes const prefix = customBaseChatReplace(group.generation_mode_join_prefix, fieldName, characterName); const suffix = customBaseChatReplace(group.generation_mode_join_suffix, fieldName, characterName); const separator = power_user.instruct.wrap ? '\n' : ''; // Also run the macro replacement on the actual content value = customBaseChatReplace(value, fieldName, characterName); return `${prefix ? prefix + separator : ''}${value}${suffix ? separator + suffix : ''}`; } const scenarioOverride = chat_metadata['scenario']; let descriptions = []; let personalities = []; let scenarios = []; let mesExamplesArray = []; for (const member of group.members) { const index = characters.findIndex(x => x.avatar === member); const character = characters[index]; if (index === -1 || !character) { console.debug(`Skipping missing member: ${member}`); continue; } if (group.disabled_members.includes(member) && characterId !== index && group.generation_mode !== group_generation_mode.APPEND_DISABLED) { console.debug(`Skipping disabled group member: ${member}`); continue; } descriptions.push(replaceAndPrepareForJoin(character.description, character.name, 'Description')); personalities.push(replaceAndPrepareForJoin(character.personality, character.name, 'Personality')); scenarios.push(replaceAndPrepareForJoin(character.scenario, character.name, 'Scenario')); mesExamplesArray.push(replaceAndPrepareForJoin(character.mes_example, character.name, 'Example Messages')); } const description = descriptions.filter(x => x.length).join('\n'); const personality = personalities.filter(x => x.length).join('\n'); const scenario = scenarioOverride?.trim() || scenarios.filter(x => x.length).join('\n'); const mesExamples = mesExamplesArray.filter(x => x.length).join('\n'); return { description, personality, scenario, mesExamples }; } async function getFirstCharacterMessage(character) { let messageText = character.first_mes; // if there are alternate greetings, pick one at random if (Array.isArray(character.data?.alternate_greetings)) { const messageTexts = [character.first_mes, ...character.data.alternate_greetings].filter(x => x); messageText = messageTexts[Math.floor(Math.random() * messageTexts.length)]; } // Allow extensions to change the first message const eventArgs = { input: messageText, output: '', character: character }; await eventSource.emit(event_types.CHARACTER_FIRST_MESSAGE_SELECTED, eventArgs); if (eventArgs.output) { messageText = eventArgs.output; } const mes = {}; mes['is_user'] = false; mes['is_system'] = false; mes['name'] = character.name; mes['send_date'] = getMessageTimeStamp(); mes['original_avatar'] = character.avatar; mes['extra'] = { 'gen_id': Date.now() * Math.random() * 1000000 }; mes['mes'] = messageText ? substituteParams(messageText.trim(), name1, character.name) : ''; mes['force_avatar'] = character.avatar != 'none' ? getThumbnailUrl('avatar', character.avatar) : default_avatar; return mes; } function resetSelectedGroup() { selected_group = null; is_group_generating = false; } async function saveGroupChat(groupId, shouldSaveGroup) { const group = groups.find(x => x.id == groupId); const chat_id = group.chat_id; group['date_last_chat'] = Date.now(); const response = await fetch('/api/chats/group/save', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ id: chat_id, chat: [...chat] }), }); if (!response.ok) { toastr.error('Check the server connection and reload the page to prevent data loss.', 'Group Chat could not be saved'); console.error('Group chat could not be saved', response); return; } if (shouldSaveGroup) { await editGroup(groupId, false, false); } } export async function renameGroupMember(oldAvatar, newAvatar, newName) { // Scan every group for our renamed character for (const group of groups) { try { // Try finding the member by old avatar link const memberIndex = group.members.findIndex(x => x == oldAvatar); // Character was not present in the group... if (memberIndex == -1) { continue; } // Replace group member avatar id and save the changes group.members[memberIndex] = newAvatar; await editGroup(group.id, true, false); console.log(`Renamed character ${newName} in group: ${group.name}`); // Load all chats from this group for (const chatId of group.chats) { const messages = await loadGroupChat(chatId); // Only save the chat if there were any changes to the chat content let hadChanges = false; // Chat shouldn't be empty if (Array.isArray(messages) && messages.length) { // Iterate over every chat message for (const message of messages) { // Only look at character messages if (message.is_user || message.is_system) { continue; } // Message belonged to the old-named character: // Update name, avatar thumbnail URL and original avatar link if (message.force_avatar && message.force_avatar.indexOf(encodeURIComponent(oldAvatar)) !== -1) { message.name = newName; message.force_avatar = message.force_avatar.replace(encodeURIComponent(oldAvatar), encodeURIComponent(newAvatar)); message.original_avatar = newAvatar; hadChanges = true; } } if (hadChanges) { const saveChatResponse = await fetch('/api/chats/group/save', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ id: chatId, chat: [...messages] }), }); if (!saveChatResponse.ok) { throw new Error('Group member could not be renamed'); } console.log(`Renamed character ${newName} in group chat: ${chatId}`); } } } } catch (error) { console.log(`An error during renaming the character ${newName} in group: ${group.name}`); console.error(error); } } } async function getGroups() { const response = await fetch('/api/groups/all', { method: 'POST', headers: getRequestHeaders(), }); if (response.ok) { const data = await response.json(); groups = data.sort((a, b) => a.id - b.id); // Convert groups to new format for (const group of groups) { if (typeof group.id === 'number') { group.id = String(group.id); } if (group.disabled_members == undefined) { group.disabled_members = []; } if (group.chat_id == undefined) { group.chat_id = group.id; group.chats = [group.id]; group.members = group.members .map(x => characters.find(y => y.name == x)?.avatar) .filter(x => x) .filter(onlyUnique); } if (group.past_metadata == undefined) { group.past_metadata = {}; } if (typeof group.chat_id === 'number') { group.chat_id = String(group.chat_id); } if (Array.isArray(group.chats) && group.chats.some(x => typeof x === 'number')) { group.chats = group.chats.map(x => String(x)); } } } } export function getGroupBlock(group) { let count = 0; let namesList = []; // Build inline name list if (Array.isArray(group.members) && group.members.length) { for (const member of group.members) { const character = characters.find(x => x.avatar === member || x.name === member); if (character) { namesList.push(character.name); count++; } } } const template = $('#group_list_template .group_select').clone(); template.data('id', group.id); template.attr('grid', group.id); template.find('.ch_name').text(group.name).attr('title', `[Group] ${group.name}`); template.find('.group_fav_icon').css('display', 'none'); template.addClass(group.fav ? 'is_fav' : ''); template.find('.ch_fav').val(group.fav); template.find('.group_select_counter').text(`${count} ${count != 1 ? 'characters' : 'character'}`); template.find('.group_select_block_list').text(namesList.join(', ')); // Display inline tags const tagsElement = template.find('.tags'); printTagList(tagsElement, { forEntityOrKey: group.id }); const avatar = getGroupAvatar(group); if (avatar) { $(template).find('.avatar').replaceWith(avatar); } return template; } function updateGroupAvatar(group) { $('#group_avatar_preview').empty().append(getGroupAvatar(group)); $('.group_select').each(function () { if ($(this).data('id') == group.id) { $(this).find('.avatar').replaceWith(getGroupAvatar(group)); } }); favsToHotswap(); } // check if isDataURLor if it's a valid local file url function isValidImageUrl(url) { // check if empty dict if (Object.keys(url).length === 0) { return false; } return isDataURL(url) || (url && (url.startsWith('user') || url.startsWith('/user'))); } function getGroupAvatar(group) { if (!group) { return $(`
`); } // if isDataURL or if it's a valid local file url if (isValidImageUrl(group.avatar_url)) { return $(`
`); } const memberAvatars = []; if (group && Array.isArray(group.members) && group.members.length) { for (const member of group.members) { const charIndex = characters.findIndex(x => x.avatar === member); if (charIndex !== -1 && characters[charIndex].avatar !== 'none') { const avatar = getThumbnailUrl('avatar', characters[charIndex].avatar); memberAvatars.push(avatar); } if (memberAvatars.length === 4) { break; } } } const avatarCount = memberAvatars.length; if (avatarCount >= 1 && avatarCount <= 4) { const groupAvatar = $(`#group_avatars_template .collage_${avatarCount}`).clone(); for (let i = 0; i < avatarCount; i++) { groupAvatar.find(`.img_${i + 1}`).attr('src', memberAvatars[i]); } groupAvatar.attr('title', `[Group] ${group.name}`); return groupAvatar; } // catch edge case where group had one member and that member is deleted if (avatarCount === 0) { return $('
'); } // default avatar const groupAvatar = $('#group_avatars_template .collage_1').clone(); groupAvatar.find('.img_1').attr('src', group.avatar_url || system_avatar); groupAvatar.attr('title', `[Group] ${group.name}`); return groupAvatar; } function getGroupChatNames(groupId) { const group = groups.find(x => x.id === groupId); if (!group) { return []; } const names = []; for (const chatId of group.chats) { names.push(chatId); } return names; } async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { function throwIfAborted() { if (params.signal instanceof AbortSignal && params.signal.aborted) { throw new Error('AbortSignal was fired. Group generation stopped'); } } if (online_status === 'no_connection') { is_group_generating = false; setSendButtonState(false); return Promise.resolve(); } if (is_group_generating) { return Promise.resolve(); } // Auto-navigate back to group menu if (menu_type !== 'group_edit') { select_group_chats(selected_group); await delay(1); } /** @type {any} Caution: JS war crimes ahead */ let textResult = ''; let typingIndicator = $('#chat .typing_indicator'); const group = groups.find((x) => x.id === selected_group); if (!group || !Array.isArray(group.members) || !group.members.length) { sendSystemMessage(system_message_types.EMPTY, '', { isSmallSys: true }); return Promise.resolve(); } try { throwIfAborted(); hideSwipeButtons(); is_group_generating = true; setCharacterName(''); setCharacterId(undefined); const userInput = String($('#send_textarea').val()); if (typingIndicator.length === 0 && !isStreamingEnabled()) { typingIndicator = $( '#typing_indicator_template .typing_indicator', ).clone(); typingIndicator.hide(); $('#chat').append(typingIndicator); } // id of this specific batch for regeneration purposes group_generation_id = Date.now(); const lastMessage = chat[chat.length - 1]; let activationText = ''; let isUserInput = false; if (userInput?.length && !by_auto_mode) { isUserInput = true; activationText = userInput; } else { if (lastMessage && !lastMessage.is_system) { activationText = lastMessage.mes; } } const activationStrategy = Number(group.activation_strategy ?? group_activation_strategy.NATURAL); const enabledMembers = group.members.filter(x => !group.disabled_members.includes(x)); let activatedMembers = []; if (params && typeof params.force_chid == 'number') { activatedMembers = [params.force_chid]; } else if (type === 'quiet') { activatedMembers = activateSwipe(group.members); if (activatedMembers.length === 0) { activatedMembers = activateListOrder(group.members.slice(0, 1)); } } else if (type === 'swipe' || type === 'continue') { activatedMembers = activateSwipe(group.members); if (activatedMembers.length === 0) { toastr.warning('Deleted group member swiped. To get a reply, add them back to the group.'); throw new Error('Deleted group member swiped'); } } else if (type === 'impersonate') { activatedMembers = activateImpersonate(group.members); } else if (activationStrategy === group_activation_strategy.NATURAL) { activatedMembers = activateNaturalOrder(enabledMembers, activationText, lastMessage, group.allow_self_responses, isUserInput); } else if (activationStrategy === group_activation_strategy.LIST) { activatedMembers = activateListOrder(enabledMembers); } if (activatedMembers.length === 0) { //toastr.warning('All group members are disabled. Enable at least one to get a reply.'); // Send user message as is const bias = getBiasStrings(userInput, type); await sendMessageAsUser(userInput, bias.messageBias); await saveChatConditional(); $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true })); } groupChatQueueOrder = new Map(); if (power_user.show_group_chat_queue) { for (let i = 0; i < activatedMembers.length; ++i) { groupChatQueueOrder.set(characters[activatedMembers[i]].avatar, i + 1); } } // now the real generation begins: cycle through every activated character for (const chId of activatedMembers) { throwIfAborted(); deactivateSendButtons(); const generateType = type == 'swipe' || type == 'impersonate' || type == 'quiet' || type == 'continue' ? type : 'group_chat'; setCharacterId(chId); setCharacterName(characters[chId].name); if (power_user.show_group_chat_queue) { printGroupMembers(); } await eventSource.emit(event_types.GROUP_MEMBER_DRAFTED, chId); if (type !== 'swipe' && type !== 'impersonate' && !isStreamingEnabled()) { // update indicator and scroll down typingIndicator .find('.typing_indicator_name') .text(characters[chId].name); typingIndicator.show(); } // Wait for generation to finish textResult = await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) }); let messageChunk = textResult?.messageChunk; if (messageChunk) { while (shouldAutoContinue(messageChunk, type === 'impersonate')) { textResult = await Generate('continue', { automatic_trigger: by_auto_mode, ...(params || {}) }); messageChunk = textResult?.messageChunk; } } if (power_user.show_group_chat_queue) { groupChatQueueOrder.delete(characters[chId].avatar); groupChatQueueOrder.forEach((value, key, map) => map.set(key, value - 1)); } } } finally { typingIndicator.hide(); is_group_generating = false; setSendButtonState(false); setCharacterId(undefined); if (power_user.show_group_chat_queue) { groupChatQueueOrder = new Map(); printGroupMembers(); } setCharacterName(''); activateSendButtons(); showSwipeButtons(); } return Promise.resolve(textResult); } function getLastMessageGenerationId() { let generationId = null; if (chat.length > 0) { const lastMes = chat[chat.length - 1]; if (!lastMes.is_user && !lastMes.is_system && lastMes.extra) { generationId = lastMes.extra.gen_id; } } return generationId; } function activateImpersonate(members) { const randomIndex = Math.floor(Math.random() * members.length); const activatedMembers = [members[randomIndex]]; const memberIds = activatedMembers .map((x) => characters.findIndex((y) => y.avatar === x)) .filter((x) => x !== -1); return memberIds; } /** * Activates a group member based on the last message. * @param {string[]} members Array of group member avatar ids * @returns {number[]} Array of character ids */ function activateSwipe(members) { let activatedNames = []; const lastMessage = chat[chat.length - 1]; if (lastMessage.is_user || lastMessage.is_system || lastMessage.extra?.type === system_message_types.NARRATOR) { for (const message of chat.slice().reverse()) { if (message.is_user || message.is_system || message.extra?.type === system_message_types.NARRATOR) { continue; } if (message.original_avatar) { activatedNames.push(message.original_avatar); break; } } if (activatedNames.length === 0) { activatedNames.push(shuffle(members.slice())[0]); } } // pre-update group chat swipe if (!lastMessage.original_avatar) { const matches = characters.filter(x => x.name == lastMessage.name); for (const match of matches) { if (members.includes(match.avatar)) { activatedNames.push(match.avatar); break; } } } else { activatedNames.push(lastMessage.original_avatar); } const memberIds = activatedNames .map((x) => characters.findIndex((y) => y.avatar === x)) .filter((x) => x !== -1); return memberIds; } function activateListOrder(members) { let activatedMembers = members.filter(onlyUnique); // map to character ids const memberIds = activatedMembers .map((x) => characters.findIndex((y) => y.avatar === x)) .filter((x) => x !== -1); return memberIds; } function activateNaturalOrder(members, input, lastMessage, allowSelfResponses, isUserInput) { let activatedMembers = []; // prevents the same character from speaking twice let bannedUser = !isUserInput && lastMessage && !lastMessage.is_user && lastMessage.name; // ...unless allowed to do so if (allowSelfResponses) { bannedUser = undefined; } // find mentions (excluding self) if (input && input.length) { for (let inputWord of extractAllWords(input)) { for (let member of members) { const character = characters.find(x => x.avatar === member); if (!character || character.name === bannedUser) { continue; } if (extractAllWords(character.name).includes(inputWord)) { activatedMembers.push(member); break; } } } } const chattyMembers = []; // activation by talkativeness (in shuffled order, except banned) const shuffledMembers = shuffle([...members]); for (let member of shuffledMembers) { const character = characters.find((x) => x.avatar === member); if (!character || character.name === bannedUser) { continue; } const rollValue = Math.random(); const talkativeness = isNaN(character.talkativeness) ? talkativeness_default : Number(character.talkativeness); if (talkativeness >= rollValue) { activatedMembers.push(member); } if (talkativeness > 0) { chattyMembers.push(member); } } // pick 1 at random if no one was activated let retries = 0; // try to limit the selected random character to those with talkativeness > 0 const randomPool = chattyMembers.length > 0 ? chattyMembers : members; while (activatedMembers.length === 0 && ++retries <= randomPool.length) { const randomIndex = Math.floor(Math.random() * randomPool.length); const character = characters.find((x) => x.avatar === randomPool[randomIndex]); if (!character) { continue; } activatedMembers.push(randomPool[randomIndex]); } // de-duplicate array of character avatars activatedMembers = activatedMembers.filter(onlyUnique); // map to character ids const memberIds = activatedMembers .map((x) => characters.findIndex((y) => y.avatar === x)) .filter((x) => x !== -1); return memberIds; } async function deleteGroup(id) { const group = groups.find((x) => x.id === id); const response = await fetch('/api/groups/delete', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ id: id }), }); if (group && Array.isArray(group.chats)) { for (const chatId of group.chats) { await eventSource.emit(event_types.GROUP_CHAT_DELETED, chatId); } } if (response.ok) { await clearChat(); selected_group = null; delete tag_map[id]; resetChatState(); await printMessages(); await getCharacters(); select_rm_info('group_delete', id); $('#rm_button_selected_ch').children('h2').text(''); } } export async function editGroup(id, immediately, reload = true) { let group = groups.find((x) => x.id === id); if (!group) { return; } group['chat_metadata'] = chat_metadata; if (immediately) { return await _save(group, reload); } saveGroupDebounced(group, reload); } let groupAutoModeAbortController = null; async function groupChatAutoModeWorker() { if (!is_group_automode_enabled || online_status === 'no_connection') { return; } if (!selected_group || is_send_press || is_group_generating) { return; } const group = groups.find((x) => x.id === selected_group); if (!group || !Array.isArray(group.members) || !group.members.length) { return; } groupAutoModeAbortController = new AbortController(); await generateGroupWrapper(true, 'auto', { signal: groupAutoModeAbortController.signal }); } async function modifyGroupMember(chat_id, groupMember, isDelete) { const id = groupMember.data('id'); const thisGroup = groups.find((x) => x.id == chat_id); const membersArray = thisGroup?.members ?? newGroupMembers; if (isDelete) { const index = membersArray.findIndex((x) => x === id); if (index !== -1) { membersArray.splice(membersArray.indexOf(id), 1); } } else { membersArray.unshift(id); } if (openGroupId) { await editGroup(openGroupId, false, false); updateGroupAvatar(thisGroup); } printGroupCandidates(); printGroupMembers(); const groupHasMembers = getGroupCharacters({ doFilter: false, onlyMembers: true }).length > 0; $('#rm_group_submit').prop('disabled', !groupHasMembers); } async function reorderGroupMember(chat_id, groupMember, direction) { const id = groupMember.data('id'); const thisGroup = groups.find((x) => x.id == chat_id); const memberArray = thisGroup?.members ?? newGroupMembers; const indexOf = memberArray.indexOf(id); if (direction == 'down') { const next = memberArray[indexOf + 1]; if (next) { memberArray[indexOf + 1] = memberArray[indexOf]; memberArray[indexOf] = next; } } if (direction == 'up') { const prev = memberArray[indexOf - 1]; if (prev) { memberArray[indexOf - 1] = memberArray[indexOf]; memberArray[indexOf] = prev; } } printGroupMembers(); // Existing groups need to modify members list if (openGroupId) { await editGroup(chat_id, false, false); updateGroupAvatar(thisGroup); } } async function onGroupActivationStrategyInput(e) { if (openGroupId) { let _thisGroup = groups.find((x) => x.id == openGroupId); _thisGroup.activation_strategy = Number(e.target.value); await editGroup(openGroupId, false, false); } } async function onGroupGenerationModeInput(e) { if (openGroupId) { let _thisGroup = groups.find((x) => x.id == openGroupId); _thisGroup.generation_mode = Number(e.target.value); await editGroup(openGroupId, false, false); toggleHiddenControls(_thisGroup); } } async function onGroupAutoModeDelayInput(e) { if (openGroupId) { let _thisGroup = groups.find((x) => x.id == openGroupId); _thisGroup.auto_mode_delay = Number(e.target.value); await editGroup(openGroupId, false, false); setAutoModeWorker(); } } async function onGroupGenerationModeTemplateInput(e) { if (openGroupId) { let _thisGroup = groups.find((x) => x.id == openGroupId); const prop = $(e.target).attr('setting'); _thisGroup[prop] = String(e.target.value); await editGroup(openGroupId, false, false); } } async function onGroupNameInput() { if (openGroupId) { let _thisGroup = groups.find((x) => x.id == openGroupId); _thisGroup.name = $(this).val(); $('#rm_button_selected_ch').children('h2').text(_thisGroup.name); await editGroup(openGroupId); } } function isGroupMember(group, avatarId) { if (group && Array.isArray(group.members)) { return group.members.includes(avatarId); } else { return newGroupMembers.includes(avatarId); } } function getGroupCharacters({ doFilter, onlyMembers } = {}) { function sortMembersFn(a, b) { const membersArray = thisGroup?.members ?? newGroupMembers; const aIndex = membersArray.indexOf(a.item.avatar); const bIndex = membersArray.indexOf(b.item.avatar); return aIndex - bIndex; } const thisGroup = openGroupId && groups.find((x) => x.id == openGroupId); let candidates = characters .filter((x) => isGroupMember(thisGroup, x.avatar) == onlyMembers) .map((x, index) => ({ item: x, id: index, type: 'character' })); if (onlyMembers) { candidates.sort(sortMembersFn); } else { sortEntitiesList(candidates); } if (doFilter) { candidates = groupCandidatesFilter.applyFilters(candidates); } return candidates; } function printGroupCandidates() { const storageKey = 'GroupCandidates_PerPage'; $('#rm_group_add_members_pagination').pagination({ dataSource: getGroupCharacters({ doFilter: true, onlyMembers: false }), pageRange: 1, position: 'top', showPageNumbers: false, prevText: '<', nextText: '>', formatNavigator: PAGINATION_TEMPLATE, showNavigator: true, showSizeChanger: true, pageSize: Number(localStorage.getItem(storageKey)) || 5, sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000], afterSizeSelectorChange: function (e) { localStorage.setItem(storageKey, e.target.value); }, callback: function (data) { $('#rm_group_add_members').empty(); for (const i of data) { $('#rm_group_add_members').append(getGroupCharacterBlock(i.item)); } }, }); } function printGroupMembers() { const storageKey = 'GroupMembers_PerPage'; $('.rm_group_members_pagination').each(function () { $(this).pagination({ dataSource: getGroupCharacters({ doFilter: false, onlyMembers: true }), pageRange: 1, position: 'top', showPageNumbers: false, prevText: '<', nextText: '>', formatNavigator: PAGINATION_TEMPLATE, showNavigator: true, showSizeChanger: true, pageSize: Number(localStorage.getItem(storageKey)) || 5, sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000], afterSizeSelectorChange: function (e) { localStorage.setItem(storageKey, e.target.value); }, callback: function (data) { $('.rm_group_members').empty(); for (const i of data) { $('.rm_group_members').append(getGroupCharacterBlock(i.item)); } }, }); }); } function getGroupCharacterBlock(character) { const avatar = getThumbnailUrl('avatar', character.avatar); const template = $('#group_member_template .group_member').clone(); const isFav = character.fav || character.fav == 'true'; template.data('id', character.avatar); template.find('.avatar img').attr({ 'src': avatar, 'title': character.avatar }); template.find('.ch_name').text(character.name); template.attr('chid', characters.indexOf(character)); template.find('.ch_fav').val(isFav); template.toggleClass('is_fav', isFav); let queuePosition = groupChatQueueOrder.get(character.avatar); if (queuePosition) { template.find('.queue_position').text(queuePosition); template.toggleClass('is_queued', queuePosition > 1); template.toggleClass('is_active', queuePosition === 1); } template.toggleClass('disabled', isGroupMemberDisabled(character.avatar)); // Display inline tags const tagsElement = template.find('.tags'); printTagList(tagsElement, { forEntityOrKey: characters.indexOf(character) }); if (!openGroupId) { template.find('[data-action="speak"]').hide(); template.find('[data-action="enable"]').hide(); template.find('[data-action="disable"]').hide(); } return template; } function isGroupMemberDisabled(avatarId) { const thisGroup = openGroupId && groups.find((x) => x.id == openGroupId); return Boolean(thisGroup && thisGroup.disabled_members.includes(avatarId)); } async function onDeleteGroupClick() { if (!openGroupId) { toastr.warning('Currently no group selected.'); return; } if (is_group_generating) { toastr.warning('Not so fast! Wait for the characters to stop typing before deleting the group.'); return; } const confirm = await Popup.show.confirm('Delete the group?', '

This will also delete all your chats with that group. If you want to delete a single conversation, select a "View past chats" option in the lower left menu.

'); if (confirm) { deleteGroup(openGroupId); } } async function onFavoriteGroupClick() { updateFavButtonState(!fav_grp_checked); if (openGroupId) { let _thisGroup = groups.find((x) => x.id == openGroupId); _thisGroup.fav = fav_grp_checked; await editGroup(openGroupId, false, false); favsToHotswap(); } } async function onGroupSelfResponsesClick() { if (openGroupId) { let _thisGroup = groups.find((x) => x.id == openGroupId); const value = $(this).prop('checked'); _thisGroup.allow_self_responses = value; await editGroup(openGroupId, false, false); } } async function onHideMutedSpritesClick(value) { if (openGroupId) { let _thisGroup = groups.find((x) => x.id == openGroupId); _thisGroup.hideMutedSprites = value; console.log(`_thisGroup.hideMutedSprites = ${_thisGroup.hideMutedSprites}`); await editGroup(openGroupId, false, false); } } function toggleHiddenControls(group, generationMode = null) { const isJoin = [group_generation_mode.APPEND, group_generation_mode.APPEND_DISABLED].includes(generationMode ?? group?.generation_mode); $('#rm_group_generation_mode_join_prefix').parent().toggle(isJoin); $('#rm_group_generation_mode_join_suffix').parent().toggle(isJoin); initScrollHeight($('#rm_group_generation_mode_join_prefix')); initScrollHeight($('#rm_group_generation_mode_join_suffix')); } function select_group_chats(groupId, skipAnimation) { openGroupId = groupId; newGroupMembers = []; const group = openGroupId && groups.find((x) => x.id == openGroupId); const groupName = group?.name ?? ''; const replyStrategy = Number(group?.activation_strategy ?? group_activation_strategy.NATURAL); const generationMode = Number(group?.generation_mode ?? group_generation_mode.SWAP); setMenuType(group ? 'group_edit' : 'group_create'); $('#group_avatar_preview').empty().append(getGroupAvatar(group)); $('#rm_group_restore_avatar').toggle(!!group && isValidImageUrl(group.avatar_url)); $('#rm_group_filter').val('').trigger('input'); $('#rm_group_activation_strategy').val(replyStrategy); $(`#rm_group_activation_strategy option[value="${replyStrategy}"]`).prop('selected', true); $('#rm_group_generation_mode').val(generationMode); $(`#rm_group_generation_mode option[value="${generationMode}"]`).prop('selected', true); $('#rm_group_chat_name').val(groupName); if (!skipAnimation) { selectRightMenuWithAnimation('rm_group_chats_block'); } // render tags applyTagsOnGroupSelect(groupId); // render characters list printGroupCandidates(); printGroupMembers(); const groupHasMembers = !!$('#rm_group_members').children().length; $('#rm_group_submit').prop('disabled', !groupHasMembers); $('#rm_group_allow_self_responses').prop('checked', group && group.allow_self_responses); $('#rm_group_hidemutedsprites').prop('checked', group && group.hideMutedSprites); $('#rm_group_automode_delay').val(group?.auto_mode_delay ?? DEFAULT_AUTO_MODE_DELAY); $('#rm_group_generation_mode_join_prefix').val(group?.generation_mode_join_prefix ?? '').attr('setting', 'generation_mode_join_prefix'); $('#rm_group_generation_mode_join_suffix').val(group?.generation_mode_join_suffix ?? '').attr('setting', 'generation_mode_join_suffix'); toggleHiddenControls(group, generationMode); // bottom buttons if (openGroupId) { $('#rm_group_submit').hide(); $('#rm_group_delete').show(); $('#rm_group_scenario').show(); $('#group-metadata-controls .chat_lorebook_button').removeClass('disabled').prop('disabled', false); $('#group_open_media_overrides').show(); const isMediaAllowed = isExternalMediaAllowed(); $('#group_media_allowed_icon').toggle(isMediaAllowed); $('#group_media_forbidden_icon').toggle(!isMediaAllowed); } else { $('#rm_group_submit').show(); if ($('#groupAddMemberListToggle .inline-drawer-content').css('display') !== 'block') { $('#groupAddMemberListToggle').trigger('click'); } $('#rm_group_delete').hide(); $('#rm_group_scenario').hide(); $('#group-metadata-controls .chat_lorebook_button').addClass('disabled').prop('disabled', true); $('#group_open_media_overrides').hide(); } updateFavButtonState(group?.fav ?? false); setAutoModeWorker(); // top bar if (group) { $('#rm_group_automode_label').show(); $('#rm_button_selected_ch').children('h2').text(groupName); } else { $('#rm_group_automode_label').hide(); } // Toggle textbox sizes, as input events have not fired here $('#rm_group_chats_block .autoSetHeight').each(element => { resetScrollHeight(element); }); eventSource.emit('groupSelected', { detail: { id: openGroupId, group: group } }); } /** * Handles the upload and processing of a group avatar. * The selected image is read, cropped using a popup, processed into a thumbnail, * and then uploaded to the server. * * @param {Event} event - The event triggered by selecting a file input, containing the image file to upload. * * @returns {Promise} - A promise that resolves when the processing and upload is complete. */ async function uploadGroupAvatar(event) { if (!(event.target instanceof HTMLInputElement) || !event.target.files.length) { return; } const file = event.target.files[0]; if (!file) { return; } const result = await getBase64Async(file); $('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup'); const croppedImage = await callGenericPopup('Set the crop position of the avatar image', POPUP_TYPE.CROP, '', { cropImage: result }); if (!croppedImage) { return; } let thumbnail = await createThumbnail(String(croppedImage), 200, 300); //remove data:image/whatever;base64 thumbnail = thumbnail.replace(/^data:image\/[a-z]+;base64,/, ''); let _thisGroup = groups.find((x) => x.id == openGroupId); // filename should be group id + human readable timestamp const filename = _thisGroup ? `${_thisGroup.id}_${humanizedDateTime()}` : humanizedDateTime(); let thumbnailUrl = await saveBase64AsFile(thumbnail, String(openGroupId ?? ''), filename, 'jpg'); if (!openGroupId) { $('#group_avatar_preview img').attr('src', thumbnailUrl); $('#rm_group_restore_avatar').show(); return; } _thisGroup.avatar_url = thumbnailUrl; $('#group_avatar_preview').empty().append(getGroupAvatar(_thisGroup)); $('#rm_group_restore_avatar').show(); await editGroup(openGroupId, true, true); } async function restoreGroupAvatar() { const confirm = await Popup.show.confirm('Are you sure you want to restore the group avatar?', 'Your custom image will be deleted, and a collage will be used instead.'); if (!confirm) { return; } if (!openGroupId) { $('#group_avatar_preview img').attr('src', default_avatar); $('#rm_group_restore_avatar').hide(); return; } let _thisGroup = groups.find((x) => x.id == openGroupId); _thisGroup.avatar_url = ''; $('#group_avatar_preview').empty().append(getGroupAvatar(_thisGroup)); $('#rm_group_restore_avatar').hide(); await editGroup(openGroupId, true, true); } async function onGroupActionClick(event) { event.stopPropagation(); const action = $(this).data('action'); const member = $(this).closest('.group_member'); if (action === 'remove') { await modifyGroupMember(openGroupId, member, true); } if (action === 'add') { await modifyGroupMember(openGroupId, member, false); } if (action === 'enable') { member.removeClass('disabled'); const _thisGroup = groups.find(x => x.id === openGroupId); const index = _thisGroup.disabled_members.indexOf(member.data('id')); if (index !== -1) { _thisGroup.disabled_members.splice(index, 1); await editGroup(openGroupId, false, false); } } if (action === 'disable') { member.addClass('disabled'); const _thisGroup = groups.find(x => x.id === openGroupId); if (!_thisGroup.disabled_members.includes(member.data('id'))) { _thisGroup.disabled_members.push(member.data('id')); await editGroup(openGroupId, false, false); } } if (action === 'up' || action === 'down') { await reorderGroupMember(openGroupId, member, action); } if (action === 'view') { openCharacterDefinition(member); } if (action === 'speak') { const chid = Number(member.attr('chid')); if (Number.isInteger(chid)) { Generate('normal', { force_chid: chid }); } } await eventSource.emit(event_types.GROUP_UPDATED); } function updateFavButtonState(state) { fav_grp_checked = state; $('#rm_group_fav').val(fav_grp_checked); $('#group_favorite_button').toggleClass('fav_on', fav_grp_checked); $('#group_favorite_button').toggleClass('fav_off', !fav_grp_checked); } export async function openGroupById(groupId) { if (isChatSaving) { toastr.info('Please wait until the chat is saved before switching characters.', 'Your chat is still saving...'); return; } if (!groups.find(x => x.id === groupId)) { console.log('Group not found', groupId); return; } if (!is_send_press && !is_group_generating) { select_group_chats(groupId); if (selected_group !== groupId) { groupChatQueueOrder = new Map(); await clearChat(); cancelTtsPlay(); selected_group = groupId; setCharacterId(undefined); setCharacterName(''); setEditedMessageId(undefined); updateChatMetadata({}, true); chat.length = 0; await getGroupChat(groupId); } } } function openCharacterDefinition(characterSelect) { if (is_group_generating) { toastr.warning('Can\'t peek a character while group reply is being generated'); console.warn('Can\'t peek a character def while group reply is being generated'); return; } const chid = characterSelect.attr('chid'); if (chid === null || chid === undefined) { return; } setCharacterId(chid); select_selected_character(chid); // Gentle nudge to recalculate tokens RA_CountCharTokens(); // Do a little tomfoolery to spoof the tag selector applyTagsOnCharacterSelect.call(characterSelect); } function filterGroupMembers() { const searchValue = String($(this).val()).toLowerCase(); groupCandidatesFilter.setFilterData(FILTER_TYPES.SEARCH, searchValue); } async function createGroup() { let name = $('#rm_group_chat_name').val(); let allowSelfResponses = !!$('#rm_group_allow_self_responses').prop('checked'); let activationStrategy = Number($('#rm_group_activation_strategy').find(':selected').val()) ?? group_activation_strategy.NATURAL; let generationMode = Number($('#rm_group_generation_mode').find(':selected').val()) ?? group_generation_mode.SWAP; let autoModeDelay = Number($('#rm_group_automode_delay').val()) ?? DEFAULT_AUTO_MODE_DELAY; const members = newGroupMembers; const memberNames = characters.filter(x => members.includes(x.avatar)).map(x => x.name).join(', '); if (!name) { name = `Group: ${memberNames}`; } const avatar_url = $('#group_avatar_preview img').attr('src'); const chatName = humanizedDateTime(); const chats = [chatName]; const createGroupResponse = await fetch('/api/groups/create', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ name: name, members: members, avatar_url: isValidImageUrl(avatar_url) ? avatar_url : default_avatar, allow_self_responses: allowSelfResponses, hideMutedSprites: hideMutedSprites, activation_strategy: activationStrategy, generation_mode: generationMode, disabled_members: [], chat_metadata: {}, fav: fav_grp_checked, chat_id: chatName, chats: chats, auto_mode_delay: autoModeDelay, }), }); if (createGroupResponse.ok) { newGroupMembers = []; const data = await createGroupResponse.json(); createTagMapFromList('#groupTagList', data.id); await getCharacters(); select_rm_info('group_create', data.id); } } export async function createNewGroupChat(groupId) { const group = groups.find(x => x.id === groupId); if (!group) { return; } const oldChatName = group.chat_id; const newChatName = humanizedDateTime(); if (typeof group.past_metadata !== 'object') { group.past_metadata = {}; } await clearChat(); chat.length = 0; if (oldChatName) { group.past_metadata[oldChatName] = Object.assign({}, chat_metadata); } group.chats.push(newChatName); group.chat_id = newChatName; group.chat_metadata = {}; updateChatMetadata(group.chat_metadata, true); await editGroup(group.id, true, false); await getGroupChat(group.id); } export async function getGroupPastChats(groupId) { const group = groups.find(x => x.id === groupId); if (!group) { return []; } const chats = []; try { for (const chatId of group.chats) { const messages = await loadGroupChat(chatId); let this_chat_file_size = (JSON.stringify(messages).length / 1024).toFixed(2) + 'kb'; let chat_items = messages.length; const lastMessage = messages.length ? messages[messages.length - 1].mes : '[The chat is empty]'; const lastMessageDate = messages.length ? (messages[messages.length - 1].send_date || Date.now()) : Date.now(); chats.push({ 'file_name': chatId, 'mes': lastMessage, 'last_mes': lastMessageDate, 'file_size': this_chat_file_size, 'chat_items': chat_items, }); } } catch (err) { console.error(err); } return chats; } export async function openGroupChat(groupId, chatId) { const group = groups.find(x => x.id === groupId); if (!group || !group.chats.includes(chatId)) { return; } await clearChat(); chat.length = 0; const previousChat = group.chat_id; group.past_metadata[previousChat] = Object.assign({}, chat_metadata); group.chat_id = chatId; group.chat_metadata = group.past_metadata[chatId] || {}; group['date_last_chat'] = Date.now(); updateChatMetadata(group.chat_metadata, true); await editGroup(groupId, true, false); await getGroupChat(groupId); } export async function renameGroupChat(groupId, oldChatId, newChatId) { const group = groups.find(x => x.id === groupId); if (!group || !group.chats.includes(oldChatId)) { return; } if (group.chat_id === oldChatId) { group.chat_id = newChatId; } group.chats.splice(group.chats.indexOf(oldChatId), 1); group.chats.push(newChatId); group.past_metadata[newChatId] = (group.past_metadata[oldChatId] || {}); delete group.past_metadata[oldChatId]; await editGroup(groupId, true, true); } export async function deleteGroupChat(groupId, chatId) { const group = groups.find(x => x.id === groupId); if (!group || !group.chats.includes(chatId)) { return; } group.chats.splice(group.chats.indexOf(chatId), 1); group.chat_metadata = {}; group.chat_id = ''; delete group.past_metadata[chatId]; updateChatMetadata(group.chat_metadata, true); const response = await fetch('/api/chats/group/delete', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ id: chatId }), }); if (response.ok) { if (group.chats.length) { await openGroupChat(groupId, group.chats[group.chats.length - 1]); } else { await createNewGroupChat(groupId); } await eventSource.emit(event_types.GROUP_CHAT_DELETED, chatId); } } export async function importGroupChat(formData) { await jQuery.ajax({ type: 'POST', url: '/api/chats/group/import', data: formData, beforeSend: function () { }, cache: false, contentType: false, processData: false, success: async function (data) { if (data.res) { const chatId = data.res; const group = groups.find(x => x.id == selected_group); if (group) { group.chats.push(chatId); await editGroup(selected_group, true, true); await displayPastChats(); } } }, error: function () { $('#create_button').removeAttr('disabled'); }, }); } export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) { const group = groups.find(x => x.id === groupId); if (!group) { return; } group.past_metadata[name] = { ...chat_metadata, ...(metadata || {}) }; group.chats.push(name); const trimmed_chat = (mesId !== undefined && mesId >= 0 && mesId < chat.length) ? chat.slice(0, parseInt(mesId) + 1) : chat; await editGroup(groupId, true, false); const response = await fetch('/api/chats/group/save', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ id: name, chat: [...trimmed_chat] }), }); if (!response.ok) { toastr.error('Check the server connection and reload the page to prevent data loss.', 'Group chat could not be saved'); console.error('Group chat could not be saved', response); } } function onSendTextareaInput() { if (is_group_automode_enabled) { // Wait for current automode generation to finish is_group_automode_enabled = false; $('#rm_group_automode').prop('checked', false); } } function stopAutoModeGeneration() { if (groupAutoModeAbortController) { groupAutoModeAbortController.abort(); } is_group_automode_enabled = false; $('#rm_group_automode').prop('checked', false); } function doCurMemberListPopout() { //repurposes the zoomed avatar template to server as a floating group member list if ($('#groupMemberListPopout').length === 0) { console.debug('did not see popout yet, creating'); const memberListClone = $(this).parent().parent().find('.inline-drawer-content').html(); const template = $('#zoomed_avatar_template').html(); const controlBarHtml = `
`; const newElement = $(template); newElement.attr('id', 'groupMemberListPopout') .removeClass('zoomed_avatar') .addClass('draggable') .empty() .append(controlBarHtml) .append(memberListClone); // Remove pagination from popout newElement.find('.group_pagination').empty(); $('body').append(newElement); loadMovingUIState(); $('#groupMemberListPopout').fadeIn(animation_duration); dragElement(newElement); $('#groupMemberListPopoutClose').off('click').on('click', function () { $('#groupMemberListPopout').fadeOut(animation_duration, () => { $('#groupMemberListPopout').remove(); }); }); // Re-add pagination not working in popout printGroupMembers(); } else { console.debug('saw existing popout, removing'); $('#groupMemberListPopout').fadeOut(animation_duration, () => { $('#groupMemberListPopout').remove(); }); } } jQuery(() => { $(document).on('input', '#rm_group_chats_block .autoSetHeight', function () { resetScrollHeight($(this)); }); $(document).on('click', '.group_select', function () { const groupId = $(this).attr('chid') || $(this).attr('grid') || $(this).data('id'); openGroupById(groupId); }); $('#rm_group_filter').on('input', filterGroupMembers); $('#rm_group_submit').on('click', createGroup); $('#rm_group_scenario').on('click', setScenarioOverride); $('#rm_group_automode').on('input', function () { const value = $(this).prop('checked'); is_group_automode_enabled = value; eventSource.once(event_types.GENERATION_STOPPED, stopAutoModeGeneration); }); $('#rm_group_hidemutedsprites').on('input', function () { const value = $(this).prop('checked'); hideMutedSprites = value; onHideMutedSpritesClick(value); }); $('#send_textarea').on('keyup', onSendTextareaInput); $('#groupCurrentMemberPopoutButton').on('click', doCurMemberListPopout); $('#rm_group_chat_name').on('input', onGroupNameInput); $('#rm_group_delete').off().on('click', onDeleteGroupClick); $('#group_favorite_button').on('click', onFavoriteGroupClick); $('#rm_group_allow_self_responses').on('input', onGroupSelfResponsesClick); $('#rm_group_activation_strategy').on('change', onGroupActivationStrategyInput); $('#rm_group_generation_mode').on('change', onGroupGenerationModeInput); $('#rm_group_automode_delay').on('input', onGroupAutoModeDelayInput); $('#rm_group_generation_mode_join_prefix').on('input', onGroupGenerationModeTemplateInput); $('#rm_group_generation_mode_join_suffix').on('input', onGroupGenerationModeTemplateInput); $('#group_avatar_button').on('input', uploadGroupAvatar); $('#rm_group_restore_avatar').on('click', restoreGroupAvatar); $(document).on('click', '.group_member .right_menu_button', onGroupActionClick); });