mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Those two debugs always floods the console in group chats, making it imposible to debug anything else, like extensions or activated wi entries.
2156 lines
73 KiB
JavaScript
2156 lines
73 KiB
JavaScript
import { Fuse } from '../lib.js';
|
|
|
|
import {
|
|
shuffle,
|
|
onlyUnique,
|
|
debounce,
|
|
delay,
|
|
isDataURL,
|
|
createThumbnail,
|
|
extractAllWords,
|
|
saveBase64AsFile,
|
|
PAGINATION_TEMPLATE,
|
|
getBase64Async,
|
|
resetScrollHeight,
|
|
initScrollHeight,
|
|
localizePagination,
|
|
renderPaginationDropdown,
|
|
paginationDropdownChangeHandler,
|
|
waitUntilCondition,
|
|
} 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,
|
|
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,
|
|
unshallowCharacter,
|
|
} 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 = false;
|
|
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,
|
|
POOLED: 3,
|
|
};
|
|
|
|
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;
|
|
});
|
|
|
|
// Remove duplicate chat ids
|
|
if (Array.isArray(group.chats)) {
|
|
const lengthBefore = group.chats.length;
|
|
group.chats = group.chats.filter(onlyUnique);
|
|
const lengthAfter = group.chats.length;
|
|
if (lengthBefore !== lengthAfter) {
|
|
dirty = true;
|
|
}
|
|
}
|
|
|
|
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
|
|
await validateGroup(group);
|
|
await unshallowGroupMembers(groupId);
|
|
|
|
const chat_id = group.chat_id;
|
|
const data = await loadGroupChat(chat_id);
|
|
const metadata = group.chat_metadata ?? {};
|
|
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 {
|
|
freshChat = !metadata.tainted;
|
|
if (group && Array.isArray(group.members) && freshChat) {
|
|
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), 'first_message');
|
|
addOneMessage(mes);
|
|
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1), 'first_message');
|
|
}
|
|
await saveGroupChat(groupId, false);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
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
|
|
* @param {boolean} trim Whether to trim the value
|
|
* @returns {string} Replaced text
|
|
* */
|
|
function customBaseChatReplace(value, fieldName, characterName, trim) {
|
|
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);
|
|
value = trim ? value.trim() : value;
|
|
return baseChatReplace(value, 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, false);
|
|
const suffix = customBaseChatReplace(group.generation_mode_join_suffix, fieldName, characterName, false);
|
|
// Also run the macro replacement on the actual content
|
|
value = customBaseChatReplace(value, fieldName, characterName, true);
|
|
|
|
return `${prefix}${value}${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) {
|
|
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 ? t`characters` : t`character`));
|
|
template.find('.group_select_block_list').text(namesList.join(', '));
|
|
|
|
// Display inline tags
|
|
const tagsElement = template.find('.tags');
|
|
printTagList(tagsElement, { forEntityOrKey: group.id, tagOptions: { isCharacterList: true } });
|
|
|
|
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 {
|
|
await unshallowGroupMembers(selected_group);
|
|
|
|
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.POOLED) {
|
|
activatedMembers = activatePooledOrder(enabledMembers, lastMessage, isUserInput);
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
await eventSource.emit(event_types.GROUP_WRAPPER_STARTED, { selected_group, type });
|
|
// 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();
|
|
await eventSource.emit(event_types.GROUP_WRAPPER_FINISHED, { selected_group, type });
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Activate group members based on the last message.
|
|
* @param {string[]} members List of member avatars
|
|
* @param {Object} lastMessage Last message
|
|
* @param {boolean} isUserInput Whether the user has input text
|
|
* @returns {number[]} List of character ids
|
|
*/
|
|
function activatePooledOrder(members, lastMessage, isUserInput) {
|
|
/** @type {string} */
|
|
let activatedMember = null;
|
|
/** @type {string[]} */
|
|
const spokenSinceUser = [];
|
|
|
|
for (const message of chat.slice().reverse()) {
|
|
if (message.is_user || isUserInput) {
|
|
break;
|
|
}
|
|
|
|
if (message.is_system || message.extra?.type === system_message_types.NARRATOR) {
|
|
continue;
|
|
}
|
|
|
|
if (message.original_avatar) {
|
|
spokenSinceUser.push(message.original_avatar);
|
|
}
|
|
}
|
|
|
|
const haveNotSpoken = members.filter(x => !spokenSinceUser.includes(x));
|
|
|
|
if (haveNotSpoken.length) {
|
|
activatedMember = haveNotSpoken[Math.floor(Math.random() * haveNotSpoken.length)];
|
|
}
|
|
|
|
if (activatedMember === null) {
|
|
const lastMessageAvatar = members.length > 1 && lastMessage && !lastMessage.is_user && lastMessage.original_avatar;
|
|
const randomPool = lastMessageAvatar ? members.filter(x => x !== lastMessage.original_avatar) : members;
|
|
activatedMember = randomPool[Math.floor(Math.random() * randomPool.length)];
|
|
}
|
|
|
|
const memberId = characters.findIndex(y => y.avatar === activatedMember);
|
|
return memberId !== -1 ? [memberId] : [];
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (id === selected_group) {
|
|
group['chat_metadata'] = structuredClone(chat_metadata);
|
|
}
|
|
|
|
if (immediately) {
|
|
return await _save(group, reload);
|
|
}
|
|
|
|
saveGroupDebounced(group, reload);
|
|
}
|
|
|
|
/**
|
|
* Unshallows all definitions of group members.
|
|
* @param {string} groupId Id of the group
|
|
* @returns {Promise<void>} Promise that resolves when all group members are unshallowed
|
|
*/
|
|
export async function unshallowGroupMembers(groupId) {
|
|
const group = groups.find(x => x.id == groupId);
|
|
if (!group) {
|
|
return;
|
|
}
|
|
const members = group.members;
|
|
if (!Array.isArray(members)) {
|
|
return;
|
|
}
|
|
for (const member of members) {
|
|
const index = characters.findIndex(x => x.avatar === member);
|
|
if (index === -1) {
|
|
continue;
|
|
}
|
|
await unshallowCharacter(String(index));
|
|
}
|
|
}
|
|
|
|
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(groupId, groupMember, isDelete) {
|
|
const id = groupMember.data('id');
|
|
const thisGroup = groups.find((x) => x.id == groupId);
|
|
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 unshallowGroupMembers(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';
|
|
const pageSize = Number(accountStorage.getItem(storageKey)) || 5;
|
|
const sizeChangerOptions = [5, 10, 25, 50, 100, 200, 500, 1000];
|
|
$('#rm_group_add_members_pagination').pagination({
|
|
dataSource: getGroupCharacters({ doFilter: true, onlyMembers: false }),
|
|
pageRange: 1,
|
|
position: 'top',
|
|
showPageNumbers: false,
|
|
prevText: '<',
|
|
nextText: '>',
|
|
formatNavigator: PAGINATION_TEMPLATE,
|
|
formatSizeChanger: renderPaginationDropdown(pageSize, sizeChangerOptions),
|
|
showNavigator: true,
|
|
showSizeChanger: true,
|
|
pageSize,
|
|
afterSizeSelectorChange: function (e, size) {
|
|
accountStorage.setItem(storageKey, e.target.value);
|
|
paginationDropdownChangeHandler(e, size);
|
|
},
|
|
callback: function (data) {
|
|
$('#rm_group_add_members').empty();
|
|
for (const i of data) {
|
|
$('#rm_group_add_members').append(getGroupCharacterBlock(i.item));
|
|
}
|
|
localizePagination($('#rm_group_add_members_pagination'));
|
|
},
|
|
});
|
|
}
|
|
|
|
function printGroupMembers() {
|
|
const storageKey = 'GroupMembers_PerPage';
|
|
$('.rm_group_members_pagination').each(function () {
|
|
let that = this;
|
|
const pageSize = Number(accountStorage.getItem(storageKey)) || 5;
|
|
const sizeChangerOptions = [5, 10, 25, 50, 100, 200, 500, 1000];
|
|
$(this).pagination({
|
|
dataSource: getGroupCharacters({ doFilter: false, onlyMembers: true }),
|
|
pageRange: 1,
|
|
position: 'top',
|
|
showPageNumbers: false,
|
|
prevText: '<',
|
|
nextText: '>',
|
|
formatNavigator: PAGINATION_TEMPLATE,
|
|
showNavigator: true,
|
|
showSizeChanger: true,
|
|
formatSizeChanger: renderPaginationDropdown(pageSize, sizeChangerOptions),
|
|
pageSize,
|
|
afterSizeSelectorChange: function (e, size) {
|
|
accountStorage.setItem(storageKey, e.target.value);
|
|
paginationDropdownChangeHandler(e, size);
|
|
},
|
|
callback: function (data) {
|
|
$('.rm_group_members').empty();
|
|
for (const i of data) {
|
|
$('.rm_group_members').append(getGroupCharacterBlock(i.item));
|
|
}
|
|
localizePagination($(that));
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
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), tagOptions: { isCharacterList: true } });
|
|
|
|
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);
|
|
await eventSource.emit(event_types.GROUP_UPDATED);
|
|
}
|
|
}
|
|
|
|
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);
|
|
});
|
|
}
|
|
|
|
hideMutedSprites = group?.hideMutedSprites ?? false;
|
|
$('#rm_group_hidemutedsprites').prop('checked', hideMutedSprites);
|
|
|
|
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') {
|
|
await 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;
|
|
}
|
|
|
|
async 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;
|
|
}
|
|
|
|
await unshallowCharacter(chid);
|
|
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 = t`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) {
|
|
await waitUntilCondition(() => !isChatSaving, debounce_timeout.extended, 10);
|
|
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);
|
|
});
|