`;
},
};
/**
* Static object representing the actions of the
* character context menu override.
*/
class CharacterContextMenu {
/**
* Tag one or more characters,
* opens a popup.
*
* @param {Array} selectedCharacters
*/
static tag = (selectedCharacters) => {
BulkTagPopupHandler.show(selectedCharacters);
};
/**
* Duplicate one or more characters
*
* @param {number} characterId
* @returns {Promise}
*/
static duplicate = async (characterId) => {
const character = CharacterContextMenu.#getCharacter(characterId);
const body = { avatar_url: character.avatar };
const result = await fetch('/api/characters/duplicate', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(body),
});
if (!result.ok) {
throw new Error('Character not duplicated');
}
const data = await result.json();
await eventSource.emit(event_types.CHARACTER_DUPLICATED, { oldAvatar: body.avatar_url, newAvatar: data.path });
};
/**
* Favorite a character
* and highlight it.
*
* @param {number} characterId
* @returns {Promise}
*/
static favorite = async (characterId) => {
const character = CharacterContextMenu.#getCharacter(characterId);
const newFavState = !character.data.extensions.fav;
const data = {
name: character.name,
avatar: character.avatar,
data: {
extensions: {
fav: newFavState,
},
},
fav: newFavState,
};
const mergeResponse = await fetch('/api/characters/merge-attributes', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(data),
});
if (!mergeResponse.ok) {
mergeResponse.json().then(json => toastr.error(`Character not saved. Error: ${json.message}. Field: ${json.error}`));
}
const element = document.getElementById(`CharID${characterId}`);
element.classList.toggle('is_fav');
};
/**
* Convert one or more characters to persona,
* may open a popup for one or more characters.
*
* @param {number} characterId
* @returns {Promise}
*/
static persona = async (characterId) => await convertCharacterToPersona(characterId);
/**
* Delete one or more characters,
* opens a popup.
*
* @param {number} characterId
* @param {boolean} [deleteChats]
* @returns {Promise}
*/
static delete = async (characterId, deleteChats = false) => {
const character = CharacterContextMenu.#getCharacter(characterId);
return fetch('/api/characters/delete', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ avatar_url: character.avatar, delete_chats: deleteChats }),
cache: 'no-cache',
}).then(response => {
if (response.ok) {
eventSource.emit(event_types.CHARACTER_DELETED, { id: characterId, character: character });
return deleteCharacter(character.name, character.avatar, false).then(() => {
if (deleteChats) getPastCharacterChats(characterId).then(pastChats => {
for (const chat of pastChats) {
const name = chat.file_name.replace('.jsonl', '');
eventSource.emit(event_types.CHAT_DELETED, name);
}
});
});
}
});
};
static #getCharacter = (characterId) => characters[characterId] ?? null;
/**
* Show the context menu at the given position
*
* @param positionX
* @param positionY
*/
static show = (positionX, positionY) => {
let contextMenu = document.getElementById(BulkEditOverlay.contextMenuId);
contextMenu.style.left = `${positionX}px`;
contextMenu.style.top = `${positionY}px`;
document.getElementById(BulkEditOverlay.contextMenuId).classList.remove('hidden');
// Adjust position if context menu is outside of viewport
const boundingRect = contextMenu.getBoundingClientRect();
if (boundingRect.right > window.innerWidth) {
contextMenu.style.left = `${positionX - (boundingRect.right - window.innerWidth)}px`;
}
if (boundingRect.bottom > window.innerHeight) {
contextMenu.style.top = `${positionY - (boundingRect.bottom - window.innerHeight)}px`;
}
};
/**
* Hide the context menu
*/
static hide = () => document.getElementById(BulkEditOverlay.contextMenuId).classList.add('hidden');
/**
* Sets up the context menu for the given overlay
*
* @param characterGroupOverlay
*/
constructor(characterGroupOverlay) {
const contextMenuItems = [
{ id: 'character_context_menu_favorite', callback: characterGroupOverlay.handleContextMenuFavorite },
{ id: 'character_context_menu_duplicate', callback: characterGroupOverlay.handleContextMenuDuplicate },
{ id: 'character_context_menu_delete', callback: characterGroupOverlay.handleContextMenuDelete },
{ id: 'character_context_menu_persona', callback: characterGroupOverlay.handleContextMenuPersona },
{ id: 'character_context_menu_tag', callback: characterGroupOverlay.handleContextMenuTag },
];
contextMenuItems.forEach(contextMenuItem => document.getElementById(contextMenuItem.id).addEventListener('click', contextMenuItem.callback));
}
}
/**
* Represents a tag control not bound to a single character
*/
class BulkTagPopupHandler {
static #getHtml = (characterIds) => {
const characterData = JSON.stringify({ characterIds: characterIds });
return `
Modify tags of ${characterIds.length} characters
Add or remove the mutual tags of all selected characters.
All
Mutual
Close
`;
};
/**
* Append and show the tag control
*
* @param {Array} characterIds - The characters assigned to this control
*/
static show(characterIds) {
if (characterIds.length == 0) {
console.log('No characters selected for bulk edit tags.');
return;
}
document.body.insertAdjacentHTML('beforeend', this.#getHtml(characterIds));
const entities = characterIds.map(id => characterToEntity(characters[id], id)).filter(entity => entity.item !== undefined);
buildAvatarList($('#bulk_tags_avatars_block'), entities);
// Print the tag list with all mutuable tags, marking them as removable. That is the initial fill
printTagList($('#bulkTagList'), { tags: () => this.getMutualTags(characterIds), tagOptions: { removable: true } });
// Tag input with resolvable list for the mutual tags to get redrawn, so that newly added tags get sorted correctly
createTagInput('#bulkTagInput', '#bulkTagList', { tags: () => this.getMutualTags(characterIds), tagOptions: { removable: true }});
document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this, characterIds));
document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this, characterIds));
document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this));
}
/**
* Builds a list of all tags that the provided characters have in common.
*
* @param {Array} characterIds - The characters to find mutual tags for
* @returns {Array