mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			2050 lines
		
	
	
		
			69 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			2050 lines
		
	
	
		
			69 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import { Fuse } from '../lib.js';
 | |
| 
 | |
| 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,
 | |
| } 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';
 | |
| import { t } from './i18n.js';
 | |
| import { accountStorage } from './util/AccountStorage.js';
 | |
| 
 | |
| export {
 | |
|     selected_group,
 | |
|     openGroupId,
 | |
|     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,
 | |
|     MANUAL: 2,
 | |
| };
 | |
| 
 | |
| 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<string, number>} */
 | |
| 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 = t`Warning: Listed member ${member} does not exist as a character. It will be removed from the group.`;
 | |
|             toastr.warning(msg, t`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 {
 | |
|         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)) ?? [];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 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() {
 | |
|     if (!selected_group) {
 | |
|         return [];
 | |
|     }
 | |
|     const groupMembers = groups.find(x => x.id == selected_group)?.members;
 | |
|     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 {number|string} arg 0-based member index or character name
 | |
|  * @param {Boolean} full Whether to return a key-value object containing extra data
 | |
|  * @returns {number|Object} 0-based character ID or key-value object if full is true
 | |
|  */
 | |
| export function findGroupMemberId(arg, full = false) {
 | |
|     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 searchByString = isNaN(index);
 | |
| 
 | |
|     if (searchByString) {
 | |
|         const memberNames = group.members.map(x => ({
 | |
|             avatar: x,
 | |
|             name: characters.find(y => y.avatar === x)?.name,
 | |
|             index: characters.findIndex(y => y.avatar === x),
 | |
|         }));
 | |
|         const fuse = new Fuse(memberNames, { keys: ['avatar', 'name'] });
 | |
|         const result = fuse.search(arg);
 | |
| 
 | |
|         if (!result.length) {
 | |
|             console.warn(`WARN: No group member found using string ${arg}`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const chid = result[0].item.index;
 | |
| 
 | |
|         if (chid === -1) {
 | |
|             console.warn(`WARN: No character found for group member ${arg}`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         console.log(`Targeting group member ${chid} (${arg}) from search result`, result[0]);
 | |
| 
 | |
|         return !full ? chid : { ...{ id: chid }, ...result[0].item };
 | |
|     }
 | |
|     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(`Targeting group member ${memberAvatar} at index ${index}`);
 | |
| 
 | |
|         return !full ? chid : {
 | |
|             id: chid,
 | |
|             avatar: memberAvatar,
 | |
|             name: characters.find(y => y.avatar === memberAvatar)?.name,
 | |
|             index: index,
 | |
|         };
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 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 <FIELDNAME> 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(/<FIELDNAME>/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
 | |
|      * @param {function(string): string} [preprocess] Preprocess function
 | |
|      * @returns {string} Prepared text
 | |
|      * */
 | |
|     function replaceAndPrepareForJoin(value, characterName, fieldName, preprocess = null) {
 | |
|         value = value.trim();
 | |
|         if (!value) {
 | |
|             return '';
 | |
|         }
 | |
| 
 | |
|         // Run preprocess function
 | |
|         if (typeof preprocess === 'function') {
 | |
|             value = preprocess(value);
 | |
|         }
 | |
| 
 | |
|         // 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', (x) => !x.startsWith('<START>') ? `<START>\n${x}` : x));
 | |
|     }
 | |
| 
 | |
|     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(t`Check the server connection and reload the page to prevent data loss.`, t`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('data-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 $(`<div class="avatar"><img src="${default_avatar}"></div>`);
 | |
|     }
 | |
|     // if isDataURL or if it's a valid local file url
 | |
|     if (isValidImageUrl(group.avatar_url)) {
 | |
|         return $(`<div class="avatar" title="[Group] ${group.name}"><img src="${group.avatar_url}"></div>`);
 | |
|     }
 | |
| 
 | |
|     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 $('<div class="missing-avatar fa-solid fa-user-slash"></div>');
 | |
|     }
 | |
| 
 | |
|     // 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 = '';
 | |
|     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());
 | |
| 
 | |
|         // 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(t`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);
 | |
|         }
 | |
|         else if (activationStrategy === group_activation_strategy.MANUAL && !isUserInput) {
 | |
|             activatedMembers = shuffle(enabledMembers).slice(0, 1).map(x => characters.findIndex(y => y.avatar === x)).filter(x => x !== -1);
 | |
|         }
 | |
| 
 | |
|         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);
 | |
| 
 | |
|             // 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 {
 | |
|         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) => ({ item: x, id: characters.indexOf(x), type: 'character' }));
 | |
| 
 | |
|     if (doFilter) {
 | |
|         candidates = groupCandidatesFilter.applyFilters(candidates);
 | |
|     }
 | |
| 
 | |
|     if (onlyMembers) {
 | |
|         candidates.sort(sortMembersFn);
 | |
|     } else {
 | |
|         const useFilterOrder = doFilter && !!$('#rm_group_filter').val();
 | |
|         sortEntitiesList(candidates, useFilterOrder, groupCandidatesFilter);
 | |
|     }
 | |
| 
 | |
|     groupCandidatesFilter.clearFuzzySearchCaches();
 | |
|     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(accountStorage.getItem(storageKey)) || 5,
 | |
|         sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
 | |
|         afterSizeSelectorChange: function (e) {
 | |
|             accountStorage.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(accountStorage.getItem(storageKey)) || 5,
 | |
|             sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
 | |
|             afterSizeSelectorChange: function (e) {
 | |
|                 accountStorage.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('data-chid', characters.indexOf(character));
 | |
|     template.find('.ch_fav').val(isFav);
 | |
|     template.toggleClass('is_fav', isFav);
 | |
| 
 | |
|     const auxFieldName = power_user.aux_field || 'character_version';
 | |
|     const auxFieldValue = (character.data && character.data[auxFieldName]) || '';
 | |
|     if (auxFieldValue) {
 | |
|         template.find('.character_version').text(auxFieldValue);
 | |
|     }
 | |
|     else {
 | |
|         template.find('.character_version').hide();
 | |
|     }
 | |
| 
 | |
|     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(t`Currently no group selected.`);
 | |
|         return;
 | |
|     }
 | |
|     if (is_group_generating) {
 | |
|         toastr.warning(t`Not so fast! Wait for the characters to stop typing before deleting the group.`);
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     const confirm = await Popup.show.confirm(t`Delete the group?`, '<p>' + t`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.` + '</p>');
 | |
|     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);
 | |
| 
 | |
|     if (!CSS.supports('field-sizing', 'content')) {
 | |
|         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
 | |
|     if (!CSS.supports('field-sizing', 'content')) {
 | |
|         $('#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<void>} - 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('data-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(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     if (!groups.find(x => x.id === groupId)) {
 | |
|         console.log('Group not found', groupId);
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     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);
 | |
|             return true;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return false;
 | |
| }
 | |
| 
 | |
| function openCharacterDefinition(characterSelect) {
 | |
|     if (is_group_generating) {
 | |
|         toastr.warning(t`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('data-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);
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Imports a group chat from a file and adds it to the group.
 | |
|  * @param {FormData} formData Form data to send to the server
 | |
|  * @param {EventTarget} eventTarget Element that triggered the import
 | |
|  */
 | |
| export async function importGroupChat(formData, eventTarget) {
 | |
|     const headers = getRequestHeaders();
 | |
|     delete headers['Content-Type'];
 | |
|     const fetchResult = await fetch('/api/chats/group/import', {
 | |
|         method: 'POST',
 | |
|         headers: headers,
 | |
|         body: formData,
 | |
|         cache: 'no-cache',
 | |
|     });
 | |
| 
 | |
|     if (fetchResult.ok) {
 | |
|         const data = await fetchResult.json();
 | |
|         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();
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     if (eventTarget instanceof HTMLInputElement) {
 | |
|         eventTarget.value = '';
 | |
|     }
 | |
| }
 | |
| 
 | |
| 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(t`Check the server connection and reload the page to prevent data loss.`, t`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 = `<div class="panelControlBar flex-container">
 | |
|         <div id="groupMemberListPopoutheader" class="fa-solid fa-grip drag-grabber hoverglow"></div>
 | |
|         <div id="groupMemberListPopoutClose" class="fa-solid fa-circle-xmark hoverglow"></div>
 | |
|     </div>`;
 | |
|         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(() => {
 | |
|     if (!CSS.supports('field-sizing', 'content')) {
 | |
|         $(document).on('input', '#rm_group_chats_block .autoSetHeight', function () {
 | |
|             resetScrollHeight($(this));
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     $(document).on('click', '.group_select', function () {
 | |
|         const groupId = $(this).attr('data-chid') || $(this).attr('data-grid');
 | |
|         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);
 | |
| });
 |