Merge branch 'staging' into tool-calling

This commit is contained in:
Cohee
2024-10-05 20:31:28 +03:00
22 changed files with 496 additions and 248 deletions

View File

@ -294,10 +294,17 @@ DOMPurify.addHook('afterSanitizeAttributes', function (node) {
}
});
DOMPurify.addHook('uponSanitizeAttribute', (_, data, config) => {
DOMPurify.addHook('uponSanitizeAttribute', (node, data, config) => {
if (!config['MESSAGE_SANITIZE']) {
return;
}
/* Retain the classes on UI elements of messages that interact with the main UI */
const permittedNodeTypes = ['BUTTON', 'DIV'];
if (config['MESSAGE_ALLOW_SYSTEM_UI'] && node.classList.contains('menu_button') && permittedNodeTypes.includes(node.nodeName)) {
return;
}
switch (data.attrName) {
case 'class': {
if (data.attrValue) {
@ -385,8 +392,8 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
if (localStorage.getItem(warningShownKey) === null) {
const warningToast = toastr.warning(
'Use the "Ext. Media" button to allow it. Click on this message to dismiss.',
'External media has been blocked',
t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`,
t`External media has been blocked`,
{
timeOut: 0,
preventDuplicates: true,
@ -651,6 +658,7 @@ async function getSystemMessages() {
force_avatar: system_avatar,
is_user: false,
is_system: true,
uses_system_ui: true,
mes: await renderTemplateAsync('welcome', { displayVersion }),
},
group: {
@ -926,7 +934,7 @@ async function firstLoadInit() {
token = tokenData.token;
} catch {
hideLoader();
toastr.error('Couldn\'t get CSRF token. Please refresh the page.', 'Error', { timeOut: 0, extendedTimeOut: 0, preventDuplicates: true });
toastr.error(t`Couldn't get CSRF token. Please refresh the page.`, t`Error`, { timeOut: 0, extendedTimeOut: 0, preventDuplicates: true });
throw new Error('Initialization failed');
}
@ -1142,7 +1150,7 @@ async function getStatusKobold() {
// We didn't get a 200 status code, but the endpoint has an explanation. Which means it DID connect, but I digress.
if (online_status === 'no_connection' && data.response) {
toastr.error(data.response, 'API Error', { timeOut: 5000, preventDuplicates: true });
toastr.error(data.response, t`API Error`, { timeOut: 5000, preventDuplicates: true });
}
} catch (err) {
console.error('Error getting status', err);
@ -1228,7 +1236,7 @@ async function getStatusTextgen() {
// We didn't get a 200 status code, but the endpoint has an explanation. Which means it DID connect, but I digress.
if (online_status === 'no_connection' && data.response) {
toastr.error(data.response, 'API Error', { timeOut: 5000, preventDuplicates: true });
toastr.error(data.response, t`API Error`, { timeOut: 5000, preventDuplicates: true });
}
} catch (err) {
if (err instanceof AbortReason) {
@ -1280,7 +1288,7 @@ export async function selectCharacterById(id) {
}
if (isChatSaving) {
toastr.info('Please wait until the chat is saved before switching characters.', 'Your chat is still saving...');
toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);
return;
}
@ -1634,7 +1642,7 @@ export async function getOneCharacter(avatarUrl) {
if (indexOf !== -1) {
characters[indexOf] = getData;
} else {
toastr.error(`Character ${avatarUrl} not found in the list`, 'Error', { timeOut: 5000, preventDuplicates: true });
toastr.error(t`Character ${avatarUrl} not found in the list`, t`Error`, { timeOut: 5000, preventDuplicates: true });
}
}
}
@ -1917,9 +1925,10 @@ export async function sendTextareaMessage() {
* @param {boolean} isSystem If the message was sent by the system
* @param {boolean} isUser If the message was sent by the user
* @param {number} messageId Message index in chat array
* @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides
* @returns {string} HTML string
*/
export function messageFormatting(mes, ch_name, isSystem, isUser, messageId) {
export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}) {
if (!mes) {
return '';
}
@ -1994,15 +2003,33 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId) {
});
}
mes = mes.replace(/```[\s\S]*?```|``[\s\S]*?``|`[\s\S]*?`|(".+?")|(\u201C.+?\u201D)/gm, function (match, p1, p2) {
if (p1) {
return '<q>"' + p1.replace(/"/g, '') + '"</q>';
} else if (p2) {
return '<q>“' + p2.replace(/\u201C|\u201D/g, '') + '”</q>';
} else {
return match;
mes = mes.replace(
/```[\s\S]*?```|``[\s\S]*?``|`[\s\S]*?`|(".*?")|(\u201C.*?\u201D)|(\u00AB.*?\u00BB)|(\u300C.*?\u300D)|(\u300E.*?\u300F)|(\uFF02.*?\uFF02)/gm,
function (match, p1, p2, p3, p4, p5, p6) {
if (p1) {
// English double quotes
return `<q>"${p1.slice(1, -1)}"</q>`;
} else if (p2) {
// Curly double quotes “ ”
return `<q>“${p2.slice(1, -1)}”</q>`;
} else if (p3) {
// Guillemets « »
return `<q>«${p3.slice(1, -1)}»</q>`;
} else if (p4) {
// Corner brackets 「 」
return `<q>「${p4.slice(1, -1)}」</q>`;
} else if (p5) {
// White corner brackets 『 』
return `<q>『${p5.slice(1, -1)}』</q>`;
} else if (p6) {
// Fullwidth quotes
return `<q>${p6.slice(1, -1)}</q>`;
} else {
// Return the original match if no quotes are found
return match;
}
}
});
);
// Restore double quotes in tags
if (!power_user.encode_tags) {
@ -2030,7 +2057,7 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId) {
}
/** @type {any} */
const config = { MESSAGE_SANITIZE: true, ADD_TAGS: ['custom-style'] };
const config = { MESSAGE_SANITIZE: true, ADD_TAGS: ['custom-style'], ...sanitizerOverrides };
mes = encodeStyleTags(mes);
mes = DOMPurify.sanitize(mes, config);
mes = decodeStyleTags(mes);
@ -2235,6 +2262,18 @@ export function addCopyToCodeBlocks(messageElement) {
}
/**
* Adds a single message to the chat.
* @param {object} mes Message object
* @param {object} [options] Options
* @param {string} [options.type='normal'] Message type
* @param {number} [options.insertAfter=null] Message ID to insert the new message after
* @param {boolean} [options.scroll=true] Whether to scroll to the new message
* @param {number} [options.insertBefore=null] Message ID to insert the new message before
* @param {number} [options.forceId=null] Force the message ID
* @param {boolean} [options.showSwipes=true] Whether to show swipe buttons
* @returns {void}
*/
export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true } = {}) {
let messageText = mes['mes'];
const momentDate = timestampToMoment(mes.send_date);
@ -2263,7 +2302,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
} else if (this_chid === undefined) {
avatarImg = system_avatar;
} else {
if (characters[this_chid].avatar != 'none') {
if (characters[this_chid].avatar !== 'none') {
avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar);
} else {
avatarImg = default_avatar;
@ -2278,12 +2317,16 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
avatarImg = mes['force_avatar'];
}
// if mes.uses_system_ui is true, set an override on the sanitizer options
const sanitizerOverrides = mes.uses_system_ui ? { MESSAGE_ALLOW_SYSTEM_UI: true } : {};
messageText = messageFormatting(
messageText,
mes.name,
isSystem,
mes.is_user,
chat.indexOf(mes),
sanitizerOverrides,
);
const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);
let bookmarkLink = mes?.extra?.bookmark_link ?? '';
@ -2335,7 +2378,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
}
//shows or hides the Prompt display button
let mesIdToFind = type == 'swipe' ? params.mesId - 1 : params.mesId; //Number(newMessage.attr('mesId'));
let mesIdToFind = type === 'swipe' ? params.mesId - 1 : params.mesId; //Number(newMessage.attr('mesId'));
//if we have itemized messages, and the array isn't null..
if (params.isUser === false && Array.isArray(itemizedPrompts) && itemizedPrompts.length > 0) {
@ -2656,7 +2699,7 @@ export function sendSystemMessage(type, text, extra = {}) {
newMessage.mes = text;
}
if (type == system_message_types.SLASH_COMMANDS) {
if (type === system_message_types.SLASH_COMMANDS) {
newMessage.mes = getSlashCommandsHelp();
}
@ -2670,7 +2713,7 @@ export function sendSystemMessage(type, text, extra = {}) {
chat.push(newMessage);
addOneMessage(newMessage);
is_send_press = false;
if (type == system_message_types.SLASH_COMMANDS) {
if (type === system_message_types.SLASH_COMMANDS) {
const browser = new SlashCommandBrowser();
const spinner = document.querySelector('#chat .last_mes .custom-slashHelp');
const parent = spinner.parentElement;
@ -3398,7 +3441,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
await eventSource.emit(event_types.GENERATION_AFTER_COMMANDS, type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage }, dryRun);
if (main_api == 'kobold' && kai_settings.streaming_kobold && !kai_flags.can_use_streaming) {
toastr.error('Streaming is enabled, but the version of Kobold used does not support token streaming.', undefined, { timeOut: 10000, preventDuplicates: true });
toastr.error(t`Streaming is enabled, but the version of Kobold used does not support token streaming.`, undefined, { timeOut: 10000, preventDuplicates: true });
unblockGeneration(type);
return Promise.resolve();
}
@ -3407,7 +3450,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
textgen_settings.streaming &&
textgen_settings.legacy_api &&
textgen_settings.type === OOBA) {
toastr.error('Streaming is not supported for the Legacy API. Update Ooba and use new API to enable streaming.', undefined, { timeOut: 10000, preventDuplicates: true });
toastr.error(t`Streaming is not supported for the Legacy API. Update Ooba and use new API to enable streaming.`, undefined, { timeOut: 10000, preventDuplicates: true });
unblockGeneration(type);
return Promise.resolve();
}
@ -3423,7 +3466,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
if (!pingResult) {
unblockGeneration(type);
toastr.error('Verify that the server is running and accessible.', 'ST Server cannot be reached');
toastr.error(t`Verify that the server is running and accessible.`, t`ST Server cannot be reached`);
throw new Error('Server unreachable');
}
@ -4476,7 +4519,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
generatedPromptCache = '';
if (data?.response) {
toastr.error(data.response, 'API Error', { preventDuplicates: true });
toastr.error(data.response, t`API Error`, { preventDuplicates: true });
}
throw new Error(data?.response);
}
@ -4585,7 +4628,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
function onError(exception) {
if (typeof exception?.error?.message === 'string') {
toastr.error(exception.error.message, 'Error', { timeOut: 10000, extendedTimeOut: 20000 });
toastr.error(exception.error.message, t`Error`, { timeOut: 10000, extendedTimeOut: 20000 });
}
generatedPromptCache = '';
@ -4989,7 +5032,7 @@ function addChatsSeparator(mesSendString) {
async function duplicateCharacter() {
if (!this_chid) {
toastr.warning('You must first select a character to duplicate!');
toastr.warning(t`You must first select a character to duplicate!`);
return '';
}
@ -5008,7 +5051,7 @@ async function duplicateCharacter() {
body: JSON.stringify(body),
});
if (response.ok) {
toastr.success('Character Duplicated');
toastr.success(t`Character Duplicated`);
const data = await response.json();
await eventSource.emit(event_types.CHARACTER_DUPLICATED, { oldAvatar: body.avatar_url, newAvatar: data.path });
await getCharacters();
@ -5855,23 +5898,23 @@ export function setSendButtonState(value) {
export async function renameCharacter(name = null, { silent = false, renameChats = null } = {}) {
if (!name && silent) {
toastr.warning('No character name provided.', 'Rename Character');
toastr.warning(t`No character name provided.`, t`Rename Character`);
return false;
}
if (this_chid === undefined) {
toastr.warning('No character selected.', 'Rename Character');
toastr.warning(t`No character selected.`, t`Rename Character`);
return false;
}
const oldAvatar = characters[this_chid].avatar;
const newValue = name || await callGenericPopup('<h3>New name:</h3>', POPUP_TYPE.INPUT, characters[this_chid].name);
const newValue = name || await callGenericPopup('<h3>' + t`New name:` + '</h3>', POPUP_TYPE.INPUT, characters[this_chid].name);
if (!newValue) {
toastr.warning('No character name provided.', 'Rename Character');
toastr.warning(t`No character name provided.`, t`Rename Character`);
return false;
}
if (newValue === characters[this_chid].name) {
toastr.info('Same character name provided, so name did not change.', 'Rename Character');
toastr.info(t`Same character name provided, so name did not change.`, t`Rename Character`);
return false;
}
@ -5918,9 +5961,9 @@ export async function renameCharacter(name = null, { silent = false, renameChats
if (renamePastChatsConfirm) {
await renamePastChats(newAvatar, newValue);
await reloadCurrentChat();
toastr.success('Character renamed and past chats updated!', 'Rename Character');
toastr.success(t`Character renamed and past chats updated!`, t`Rename Character`);
} else {
toastr.success('Character renamed!', 'Rename Character');
toastr.success(t`Character renamed!`, t`Rename Character`);
}
}
else {
@ -5933,8 +5976,8 @@ export async function renameCharacter(name = null, { silent = false, renameChats
}
catch (error) {
// Reloading to prevent data corruption
if (!silent) await callPopup('Something went wrong. The page will be reloaded.', 'text');
else toastr.error('Something went wrong. The page will be reloaded.', 'Rename Character');
if (!silent) await callPopup(t`Something went wrong. The page will be reloaded.`, 'text');
else toastr.error(t`Something went wrong. The page will be reloaded.`, t`Rename Character`);
console.log('Renaming character error:', error);
location.reload();
@ -5991,7 +6034,7 @@ async function renamePastChats(newAvatar, newValue) {
}
}
} catch (error) {
toastr.error(`Past chat could not be updated: ${file_name}`);
toastr.error(t`Past chat could not be updated: ${file_name}`);
console.error(error);
}
}
@ -6040,7 +6083,7 @@ export async function saveChat(chatName, withMetadata, mesId) {
characters[this_chid]['date_last_chat'] = Date.now();
chat.forEach(function (item, i) {
if (item['is_group']) {
toastr.error('Trying to save group chat with regular saveChat function. Aborting to prevent corruption.');
toastr.error(t`Trying to save group chat with regular saveChat function. Aborting to prevent corruption.`);
throw new Error('Group chat saved from saveChat');
}
/*
@ -6085,7 +6128,7 @@ export async function saveChat(chatName, withMetadata, mesId) {
contentType: 'application/json',
success: function (data) { },
error: function (jqXHR, exception) {
toastr.error('Check the server connection and reload the page to prevent data loss.', 'Chat could not be saved');
toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Chat could not be saved`);
console.log(exception);
console.log(jqXHR);
},
@ -6482,7 +6525,7 @@ export async function getSettings() {
if (!response.ok) {
reloadLoop();
toastr.error('Settings could not be loaded after multiple attempts. Please try again later.');
toastr.error(t`Settings could not be loaded after multiple attempts. Please try again later.`);
throw new Error('Error getting settings');
}
@ -6709,7 +6752,7 @@ export async function saveSettings(type) {
eventSource.emit(event_types.SETTINGS_UPDATED);
},
error: function (jqXHR, exception) {
toastr.error('Check the server connection and reload the page to prevent data loss.', 'Settings could not be saved');
toastr.error(t`Check the server connection and reload the page to prevent data loss.t`, t`Settings could not be saved`);
console.log(exception);
console.log(jqXHR);
},
@ -6975,7 +7018,7 @@ export async function displayPastChats() {
const data = await (selected_group ? getGroupPastChats(selected_group) : getPastCharacterChats());
if (!data) {
toastr.error('Could not load chat data. Try reloading the page.');
toastr.error(t`Could not load chat data. Try reloading the page.`);
return;
}
@ -7108,7 +7151,7 @@ export function selectRightMenuWithAnimation(selectedMenuId) {
export function select_rm_info(type, charId, previousCharId = null) {
if (!type) {
toastr.error('Invalid process (no \'type\')');
toastr.error(t`Invalid process (no 'type')`);
return;
}
if (type !== 'group_create') {
@ -7116,20 +7159,20 @@ export function select_rm_info(type, charId, previousCharId = null) {
}
if (type === 'char_delete') {
toastr.warning(`Character Deleted: ${displayName}`);
toastr.warning(t`Character Deleted: ${displayName}`);
}
if (type === 'char_create') {
toastr.success(`Character Created: ${displayName}`);
toastr.success(t`Character Created: ${displayName}`);
}
if (type === 'group_create') {
toastr.success('Group Created');
toastr.success(t`Group Created`);
}
if (type === 'group_delete') {
toastr.warning('Group Deleted');
toastr.warning(t`Group Deleted`);
}
if (type === 'char_import') {
toastr.success(`Character Imported: ${displayName}`);
toastr.success(t`Character Imported: ${displayName}`);
}
selectRightMenuWithAnimation('rm_characters_block');
@ -7587,25 +7630,25 @@ export function hideSwipeButtons() {
*/
export async function deleteSwipe(swipeId = null) {
if (swipeId && (isNaN(swipeId) || swipeId < 0)) {
toastr.warning(`Invalid swipe ID: ${swipeId + 1}`);
toastr.warning(t`Invalid swipe ID: ${swipeId + 1}`);
return;
}
const lastMessage = chat[chat.length - 1];
if (!lastMessage || !Array.isArray(lastMessage.swipes) || !lastMessage.swipes.length) {
toastr.warning('No messages to delete swipes from.');
toastr.warning(t`No messages to delete swipes from.`);
return;
}
if (lastMessage.swipes.length <= 1) {
toastr.warning('Can\'t delete the last swipe.');
toastr.warning(t`Can't delete the last swipe.`);
return;
}
swipeId = swipeId ?? lastMessage.swipe_id;
if (swipeId < 0 || swipeId >= lastMessage.swipes.length) {
toastr.warning(`Invalid swipe ID: ${swipeId + 1}`);
toastr.warning(t`Invalid swipe ID: ${swipeId + 1}`);
return;
}
@ -7744,7 +7787,7 @@ export function setGenerationProgress(progress) {
function isHordeGenerationNotAllowed() {
if (main_api == 'koboldhorde' && preset_settings == 'gui') {
toastr.error('GUI Settings preset is not supported for Horde. Please select another preset.');
toastr.error(t`GUI Settings preset is not supported for Horde. Please select another preset.`);
return true;
}
@ -7793,7 +7836,7 @@ function openCharacterWorldPopup() {
}
$('#character_json_data').val(JSON.stringify(data));
toastr.info('Embedded lorebook will be removed from this character.');
toastr.info(t`Embedded lorebook will be removed from this character.`);
} catch {
console.error('Failed to parse character JSON data.');
}
@ -7984,11 +8027,11 @@ async function createOrEditCharacter(e) {
if ($('#form_create').attr('actiontype') == 'createcharacter') {
if (String($('#character_name_pole').val()).length === 0) {
toastr.error('Name is required');
toastr.error(t`Name is required`);
return;
}
if (is_group_generating || is_send_press) {
toastr.error('Cannot create characters while generating. Stop the request and try again.', 'Creation aborted');
toastr.error(t`Cannot create characters while generating. Stop the request and try again.`, t`Creation aborted`);
return;
}
try {
@ -8072,7 +8115,7 @@ async function createOrEditCharacter(e) {
} catch (error) {
console.error('Error creating character', error);
toastr.error('Failed to create character');
toastr.error(t`Failed to create character`);
}
} else {
try {
@ -8131,7 +8174,7 @@ async function createOrEditCharacter(e) {
}
} catch (error) {
console.log(error);
toastr.error('Something went wrong while saving the character, or the image file provided was in an invalid format. Double check that the image is not a webp.');
toastr.error(t`Something went wrong while saving the character, or the image file provided was in an invalid format. Double check that the image is not a webp.`);
}
}
}
@ -8641,7 +8684,7 @@ async function selectContextCallback(args, name) {
const result = fuse.search(name);
if (result.length === 0) {
!quiet && toastr.warning(`Context template "${name}" not found`);
!quiet && toastr.warning(t`Context template '${name}' not found`);
return '';
}
@ -8661,7 +8704,7 @@ async function selectInstructCallback(args, name) {
const result = fuse.search(name);
if (result.length === 0) {
!quiet && toastr.warning(`Instruct template "${name}" not found`);
!quiet && toastr.warning(t`Instruct template '${name}' not found`);
return '';
}
@ -8713,7 +8756,7 @@ async function connectAPISlash(args, text) {
const apiConfig = CONNECT_API_MAP[text.toLowerCase()];
if (!apiConfig) {
toastr.error(`Error: ${text} is not a valid API`);
toastr.error(t`Error: ${text} is not a valid API`);
return '';
}
@ -8742,7 +8785,7 @@ async function connectAPISlash(args, text) {
}
const quiet = isTrueBoolean(args?.quiet);
const toast = quiet ? jQuery() : toastr.info(`API set to ${text}, trying to connect..`);
const toast = quiet ? jQuery() : toastr.info(t`API set to ${text}, trying to connect..`);
try {
await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100);
@ -8781,7 +8824,7 @@ export async function processDroppedFiles(files, data = new Map()) {
const preservedName = data instanceof Map && data.get(file);
await importCharacter(file, preservedName);
} else {
toastr.warning('Unsupported file type: ' + file.name);
toastr.warning(t`Unsupported file type: ` + file.name);
}
}
}
@ -8794,7 +8837,7 @@ export async function processDroppedFiles(files, data = new Map()) {
*/
async function importCharacter(file, preserveFileName = '') {
if (is_group_generating || is_send_press) {
toastr.error('Cannot import characters while generating. Stop the request and try again.', 'Import aborted');
toastr.error(t`Cannot import characters while generating. Stop the request and try again.`, t`Import aborted`);
throw new Error('Cannot import character while generating');
}
@ -8821,7 +8864,7 @@ async function importCharacter(file, preserveFileName = '') {
});
if (data.error) {
toastr.error('The file is likely invalid or corrupted.', 'Could not import character');
toastr.error(t`The file is likely invalid or corrupted.`, t`Could not import character`);
return;
}
@ -8874,7 +8917,7 @@ async function doImpersonate(args, prompt) {
await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
} catch {
console.warn('Timeout waiting for generation unlock');
toastr.warning('Cannot run /impersonate command while the reply is being generated.');
toastr.warning(t`Cannot run /impersonate command while the reply is being generated.`);
return '';
}
@ -8936,19 +8979,19 @@ async function doDeleteChat() {
async function doRenameChat(_, chatName) {
if (!chatName) {
toastr.warning('Name must be provided as an argument to rename this chat.');
toastr.warning(t`Name must be provided as an argument to rename this chat.`);
return '';
}
const currentChatName = getCurrentChatId();
if (!currentChatName) {
toastr.warning('No chat selected that can be renamed.');
toastr.warning(t`No chat selected that can be renamed.`);
return '';
}
await renameChat(currentChatName, chatName);
toastr.success(`Successfully renamed chat to: ${chatName}`);
toastr.success(t`Successfully renamed chat to: ${chatName}`);
return '';
}
@ -9063,7 +9106,7 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {})
for (const key of characterKey) {
const character = characters.find(x => x.avatar == key);
if (!character) {
toastr.warning(`Character ${key} not found. Skipping deletion.`);
toastr.warning(t`Character ${key} not found. Skipping deletion.`);
continue;
}
@ -9080,7 +9123,7 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {})
});
if (!response.ok) {
toastr.error(`${response.status} ${response.statusText}`, 'Failed to delete character');
toastr.error(`${response.status} ${response.statusText}`, t`Failed to delete character`);
continue;
}
@ -9132,6 +9175,90 @@ function doTogglePanels() {
return '';
}
/**
* Event handler to open a navbar drawer when a drawer open button is clicked.
* Handles click events on .drawer-opener elements.
* Opens the drawer associated with the clicked button according to the data-target attribute.
* @returns {void}
*/
function doDrawerOpenClick() {
const targetDrawerID = $(this).attr('data-target');
const drawer = $(`#${targetDrawerID}`);
const drawerToggle = drawer.find('.drawer-toggle');
const drawerWasOpenAlready = drawerToggle.parent().find('.drawer-content').hasClass('openDrawer');
if (drawerWasOpenAlready || drawer.hasClass('resizing')) { return; }
doNavbarIconClick.call(drawerToggle);
}
/**
* Event handler to open or close a navbar drawer when a navbar icon is clicked.
* Handles click events on .drawer-toggle elements.
* @returns {void}
*/
function doNavbarIconClick() {
var icon = $(this).find('.drawer-icon');
var drawer = $(this).parent().find('.drawer-content');
if (drawer.hasClass('resizing')) { return; }
var drawerWasOpenAlready = $(this).parent().find('.drawer-content').hasClass('openDrawer');
let targetDrawerID = $(this).parent().find('.drawer-content').attr('id');
const pinnedDrawerClicked = drawer.hasClass('pinnedOpen');
if (!drawerWasOpenAlready) { //to open the drawer
$('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () {
await delay(50); $(this).closest('.drawer-content').removeClass('resizing');
});
$('.openIcon').toggleClass('closedIcon openIcon');
$('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer');
icon.toggleClass('openIcon closedIcon');
drawer.toggleClass('openDrawer closedDrawer');
//console.log(targetDrawerID);
if (targetDrawerID === 'right-nav-panel') {
$(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle({
duration: 200,
easing: 'swing',
start: function () {
jQuery(this).css('display', 'flex'); //flex needed to make charlist scroll
},
complete: async function () {
favsToHotswap();
await delay(50);
$(this).closest('.drawer-content').removeClass('resizing');
$('#rm_print_characters_block').trigger('scroll');
},
});
} else {
$(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle(200, 'swing', async function () {
await delay(50); $(this).closest('.drawer-content').removeClass('resizing');
});
}
// Set the height of "autoSetHeight" textareas within the drawer to their scroll height
if (!CSS.supports('field-sizing', 'content')) {
$(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(async function () {
await resetScrollHeight($(this));
return;
});
}
} else if (drawerWasOpenAlready) { //to close manually
icon.toggleClass('closedIcon openIcon');
if (pinnedDrawerClicked) {
$(drawer).addClass('resizing').slideToggle(200, 'swing', async function () {
await delay(50); $(this).removeClass('resizing');
});
}
else {
$('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () {
await delay(50); $(this).closest('.drawer-content').removeClass('resizing');
});
}
drawer.toggleClass('closedDrawer openDrawer');
}
}
function addDebugFunctions() {
const doBackfill = async () => {
for (const message of chat) {
@ -9755,12 +9882,7 @@ jQuery(async function () {
let deleteChats = false;
const confirm = await Popup.show.confirm('Delete the character?', `
<b>THIS IS PERMANENT!<br><br>
<label for="del_char_checkbox" class="checkbox_label justifyCenter">
<input type="checkbox" id="del_char_checkbox" />
<small>Also delete the chat files</small>
</label></b>`, {
const confirm = await Popup.show.confirm(t`Delete the character?`, await renderTemplateAsync('deleteConfirm'), {
onClose: () => deleteChats = !!$('#del_char_checkbox').prop('checked'),
});
if (!confirm) {
@ -10721,69 +10843,8 @@ jQuery(async function () {
stopScriptExecution();
});
$('.drawer-toggle').on('click', function () {
var icon = $(this).find('.drawer-icon');
var drawer = $(this).parent().find('.drawer-content');
if (drawer.hasClass('resizing')) { return; }
var drawerWasOpenAlready = $(this).parent().find('.drawer-content').hasClass('openDrawer');
let targetDrawerID = $(this).parent().find('.drawer-content').attr('id');
const pinnedDrawerClicked = drawer.hasClass('pinnedOpen');
if (!drawerWasOpenAlready) { //to open the drawer
$('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () {
await delay(50); $(this).closest('.drawer-content').removeClass('resizing');
});
$('.openIcon').not('.drawerPinnedOpen').toggleClass('closedIcon openIcon');
$('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer');
icon.toggleClass('openIcon closedIcon');
drawer.toggleClass('openDrawer closedDrawer');
//console.log(targetDrawerID);
if (targetDrawerID === 'right-nav-panel') {
$(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle({
duration: 200,
easing: 'swing',
start: function () {
jQuery(this).css('display', 'flex'); //flex needed to make charlist scroll
},
complete: async function () {
favsToHotswap();
await delay(50);
$(this).closest('.drawer-content').removeClass('resizing');
$('#rm_print_characters_block').trigger('scroll');
},
});
} else {
$(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle(200, 'swing', async function () {
await delay(50); $(this).closest('.drawer-content').removeClass('resizing');
});
}
// Set the height of "autoSetHeight" textareas within the drawer to their scroll height
if (!CSS.supports('field-sizing', 'content')) {
$(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(async function () {
await resetScrollHeight($(this));
return;
});
}
} else if (drawerWasOpenAlready) { //to close manually
icon.toggleClass('closedIcon openIcon');
if (pinnedDrawerClicked) {
$(drawer).addClass('resizing').slideToggle(200, 'swing', async function () {
await delay(50); $(this).removeClass('resizing');
});
}
else {
$('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () {
await delay(50); $(this).closest('.drawer-content').removeClass('resizing');
});
}
drawer.toggleClass('closedDrawer openDrawer');
}
});
$(document).on('click', '.drawer-opener', doDrawerOpenClick);
$('.drawer-toggle').on('click', doNavbarIconClick);
$('html').on('touchstart mousedown', function (e) {
var clickTarget = $(e.target);