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, callPopup, clearChat, Generate, select_rm_info, setCharacterId, setCharacterName, setEditedMessageId, is_send_press, name1, resetChatState, setSendButtonState, getCharacters, system_message_types, online_status, talkativeness_default, selectRightMenuWithAnimation, default_ch_mes, 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, getCropPopup, system_avatar, isChatSaving, setExternalAbortController, baseChatReplace, depth_prompt_depth_default, loadItemizedPrompts, animation_duration, depth_prompt_role_default, shouldAutoContinue, } from '../script.js'; import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map } from './tags.js'; import { FILTER_TYPES, FilterHelper } from './filters.js'; import { isExternalMediaAllowed } from './chats.js'; import { extension_settings } from './extensions.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); 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 []; } export async function getGroupChat(groupId, reload = false) { const group = groups.find((x) => x.id === groupId); const chat_id = group.chat_id; const data = await loadGroupChat(chat_id); 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); 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); } if (group) { let metadata = group.chat_metadata ?? {}; updateChatMetadata(metadata, true); } if (reload) { select_group_chats(groupId, true); } await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); } /** * 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)) ?? []; } /** * Retrieves the member names of a group. If the group is not selected, an empty array is returned. * @returns {string[]} An array of character names representing the members of the group. */ export function getGroupNames() { const groupMembers = selected_group ? groups.find(x => x.id == selected_group)?.members : null; return Array.isArray(groupMembers) ? groupMembers.map(x => characters.find(y => y.avatar === x)?.name).filter(x => x) : []; } /** * Finds the character ID for a group member. * @param {string} arg 1-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; } // Index is 1-based const index = parseInt(arg) - 1; 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 */ // Initialize depth prompt for the current character // skip if the current character is not in the group // Skip if the current character is disabled // Current character always is first character in the group // All Characters should have <{{char}}>X in theirs prompts 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 = []; let sffix = group.generation_mode_join_suffix ?? '' let prfix = group.generation_mode_join_prefix ?? '' 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; } switch (character.name) { case '': var CharStart = '<' + member + '>'; var CharEnd = ''; var Chnm = member; break; case undefined: CharStart = '<' + member + '>'; CharEnd = ''; Chnm = member; break; default: CharStart = '<' + character.name + '>'; CharEnd = ''; Chnm = character.name; break; } switch (character.description.trim()) { case '': descriptions.push(''); break; default: descriptions.push(baseChatReplace(prfix + '\n#' + Chnm + '\n' + CharStart + '\n' + character.description.trim() + '\n' + CharEnd + '\n- - -' + '\n' + sffix, name1, character.name)); break; } switch (character.personality.trim()) { case '': personalities.push(''); break; default: personalities.push(baseChatReplace(prfix + '\n#' + Chnm + '\n' + CharStart + '\n' + character.personality.trim() + '\n' + CharEnd + '\n- - -' + '\n' + sffix, name1, character.name)); break; } switch (character.scenario.trim()) { case '': scenarios.push(''); break; default: scenarios.push(baseChatReplace(prfix + '\n#' + Chnm + '\n' + CharStart + character.scenario.trim() + CharEnd + '\n- - -' + '\n' + sffix, name1, character.name)); break; } switch (character.mes_example.trim()) { case '': mesExamplesArray.push(''); break; default: mesExamplesArray.push(baseChatReplace(prfix + '\n#' + Chnm + '\n' + CharStart + character.mes_example.trim() + CharEnd + '\n- - -' + '\n' + sffix, name1, character.name)); break; } } switch (extension_settings.Nvkun.SubPromptsUsage){ case true: //var Df1Prompt = '### This section contains characters descriptions. The AI should accurately convey the original descriptions provided by the system, while consider fact, that characters may be written by various peoples, and had very different styles.'; var Df1Prompt = '### This section contains characters descriptions. The AI should accurately convey the original descriptions provided by the system.'; var Df2Prompt = '### This section contains characters personalities.'; var Df3Prompt = '### [Story buildup and other miscellaneous information.]'; var Df4Prompt = '### [The information within is not to be taken literally as what happens within the current story.]'; break; default: Df1Prompt = ''; Df2Prompt = ''; Df3Prompt = ''; Df4Prompt = ''; break; } var Df1 = descriptions.join(''); switch (Df1) { case '': var description = ''; break; default: description = '\n\n\n' + Df1Prompt + '\n' + Df1 + ''; break; } var Df2 = personalities.join(''); switch (Df2) { case '': var personality = ''; break; default: personality = '\n\n\n' + Df2Prompt + '\n' + Df2 + ''; break; } var Df3 = scenarios.join(''); switch (Df3) { case '': var scenario = ''; break; default: scenario = scenarioOverride?.trim() || '\n\n\n' + Df3Prompt + '\n' + Df3 + ''; break; } var Df4 = mesExamplesArray.join(''); switch (extension_settings.Nvkun.ExamplesExclude){ default: switch (Df4) { case '': break; default: var mesExamples = '\n\n\n' + Df4Prompt + '\n' + Df4 + ''; break; } break; case true: mesExamples = ''; break; } /* == 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'); >>> 47b65626 (Merge pull request #2112 from SillyTavern/staging)*/ 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) : default_ch_mes; 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 (shouldSaveGroup && response.ok) { 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) { 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')); } 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' || type === 'lookaround') { 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') { $('#send_textarea').attr('disabled', true); 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('').trigger('input'); } // 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 == 'lookaround' || type == 'continue' ? type : 'group_chat'; setCharacterId(chId); setCharacterName(characters[chId].name); 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; } } } } finally { typingIndicator.hide(); is_group_generating = false; $('#send_textarea').attr('disabled', false); setSendButtonState(false); setCharacterId(undefined); 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; } } } } // 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(); let talkativeness = Number(character.talkativeness); talkativeness = Number.isNaN(talkativeness) ? talkativeness_default : talkativeness; if (talkativeness >= rollValue) { activatedMembers.push(member); } } // pick 1 at random if no one was activated let retries = 0; while (activatedMembers.length === 0 && ++retries <= members.length) { const randomIndex = Math.floor(Math.random() * members.length); const character = characters.find((x) => x.avatar === members[randomIndex]); if (!character) { continue; } activatedMembers.push(members[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); 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)); } function onDeleteGroupClick() { if (is_group_generating) { toastr.warning('Not so fast! Wait for the characters to stop typing before deleting the group.'); return; } $('#dialogue_popup').data('group_id', openGroupId); callPopup('

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.

', 'del_group'); } 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 printTagList($('#groupTagList'), { forEntityOrKey: groupId, tagOptions: { removable: true } }); // 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) { 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 callPopup(getCropPopup(result), 'avatarToCrop'); if (!croppedImage) { return; } let thumbnail = await createThumbnail(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 callPopup('

Are you sure you want to restore the group avatar?

Your custom image will be deleted, and a collage will be used instead.', 'confirm'); 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); _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) { if (selected_group !== groupId) { await clearChat(); cancelTtsPlay(); selected_group = groupId; setCharacterId(undefined); setCharacterName(''); setEditedMessageId(undefined); updateChatMetadata({}, true); chat.length = 0; await getGroupChat(groupId); } select_group_chats(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); await fetch('/api/chats/group/save', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ id: name, chat: [...trimmed_chat] }), }); } 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); });