Refactor overwrite check to utility function

- Refactor overwrite check to utility function
- Don't mind me refactoring character delete functions. I tried something, but I think the refactoring still makes sense
This commit is contained in:
Wolfsblvt 2024-05-22 23:52:35 +02:00
parent a251849f8f
commit ab8c67ede6
4 changed files with 88 additions and 79 deletions

View File

@ -154,6 +154,7 @@ import {
isValidUrl, isValidUrl,
ensureImageFormatSupported, ensureImageFormatSupported,
flashHighlight, flashHighlight,
checkOverwriteExistingData,
} from './scripts/utils.js'; } from './scripts/utils.js';
import { debounce_timeout } from './scripts/constants.js'; import { debounce_timeout } from './scripts/constants.js';
@ -6465,7 +6466,8 @@ export async function getChatsFromFiles(data, isGroupChat) {
* @param {null|number} [characterId=null] - When set, the function will use this character id instead of this_chid. * @param {null|number} [characterId=null] - When set, the function will use this character id instead of this_chid.
* *
* @returns {Promise<Array>} - An array containing metadata of all past chats of the character, sorted * @returns {Promise<Array>} - An array containing metadata of all past chats of the character, sorted
* in descending order by file name. Returns `undefined` if the fetch request is unsuccessful. * in descending order by file name. Returns an empty array if the fetch request is unsuccessful or the
* response is an object with an `error` property set to `true`.
*/ */
export async function getPastCharacterChats(characterId = null) { export async function getPastCharacterChats(characterId = null) {
characterId = characterId ?? this_chid; characterId = characterId ?? this_chid;
@ -6481,10 +6483,13 @@ export async function getPastCharacterChats(characterId = null) {
return []; return [];
} }
let data = await response.json(); const data = await response.json();
data = Object.values(data); if (typeof data === 'object' && data.error === true) {
data = data.sort((a, b) => a['file_name'].localeCompare(b['file_name'])).reverse(); return [];
return data; }
const chats = Object.values(data);
return chats.sort((a, b) => a['file_name'].localeCompare(b['file_name'])).reverse();
} }
/** /**
@ -8449,17 +8454,34 @@ function doCloseChat() {
* @param {string} this_chid - The character ID to be deleted. * @param {string} this_chid - The character ID to be deleted.
* @param {boolean} delete_chats - Whether to delete chats or not. * @param {boolean} delete_chats - Whether to delete chats or not.
*/ */
export async function handleDeleteCharacter(popup_type, this_chid, delete_chats) { async function handleDeleteCharacter(popup_type, this_chid, delete_chats) {
if (popup_type !== 'del_ch' || if (popup_type !== 'del_ch' ||
!characters[this_chid]) { !characters[this_chid]) {
return; return;
} }
const avatar = characters[this_chid].avatar; await deleteCharacter(characters[this_chid].avatar, { deleteChats: delete_chats });
const name = characters[this_chid].name; }
const pastChats = await getPastCharacterChats();
const msg = { avatar_url: avatar, delete_chats: delete_chats }; /**
* Deletes a character completely, including associated chats if specified
*
* @param {string} characterKey - The key (avatar) of the character to be deleted
* @param {Object} [options] - Optional parameters for the deletion
* @param {boolean} [options.deleteChats=true] - Whether to delete associated chats or not
* @return {Promise<void>} - A promise that resolves when the character is successfully deleted
*/
export async function deleteCharacter(characterKey, { deleteChats = true } = {}) {
const character = characters.find(x => x.avatar == characterKey);;
if (!character) {
toastr.warning(`Character ${characterKey} not found. Cannot be deleted.`);
return;
}
const chid = characters.indexOf(character);
const pastChats = await getPastCharacterChats(chid);
const msg = { avatar_url: character.avatar, delete_chats: deleteChats };
const response = await fetch('/api/characters/delete', { const response = await fetch('/api/characters/delete', {
method: 'POST', method: 'POST',
@ -8468,18 +8490,18 @@ export async function handleDeleteCharacter(popup_type, this_chid, delete_chats)
cache: 'no-cache', cache: 'no-cache',
}); });
if (response.ok) { if (!response.ok) {
await deleteCharacter(name, avatar); throw new Error(`Failed to delete character: ${response.status} ${response.statusText}`);
}
if (delete_chats) { await removeCharacterFromUI(character.name, character.avatar);
if (deleteChats) {
for (const chat of pastChats) { for (const chat of pastChats) {
const name = chat.file_name.replace('.jsonl', ''); const name = chat.file_name.replace('.jsonl', '');
await eventSource.emit(event_types.CHAT_DELETED, name); await eventSource.emit(event_types.CHAT_DELETED, name);
} }
} }
} else {
console.error('Failed to delete character: ', response.status, response.statusText);
}
} }
/** /**
@ -8495,7 +8517,7 @@ export async function handleDeleteCharacter(popup_type, this_chid, delete_chats)
* @param {string} avatar - The avatar URL of the character to be deleted. * @param {string} avatar - The avatar URL of the character to be deleted.
* @param {boolean} reloadCharacters - Whether the character list should be refreshed after deletion. * @param {boolean} reloadCharacters - Whether the character list should be refreshed after deletion.
*/ */
export async function deleteCharacter(name, avatar, reloadCharacters = true) { async function removeCharacterFromUI(name, avatar, reloadCharacters = true) {
await clearChat(); await clearChat();
$('#character_cross').click(); $('#character_cross').click();
this_chid = undefined; this_chid = undefined;

View File

@ -4,7 +4,6 @@ import {
characterGroupOverlay, characterGroupOverlay,
callPopup, callPopup,
characters, characters,
deleteCharacter,
event_types, event_types,
eventSource, eventSource,
getCharacters, getCharacters,
@ -13,6 +12,7 @@ import {
buildAvatarList, buildAvatarList,
characterToEntity, characterToEntity,
printCharactersDebounced, printCharactersDebounced,
deleteCharacter,
} from '../script.js'; } from '../script.js';
import { favsToHotswap } from './RossAscends-mods.js'; import { favsToHotswap } from './RossAscends-mods.js';
@ -115,24 +115,7 @@ class CharacterContextMenu {
static delete = async (characterId, deleteChats = false) => { static delete = async (characterId, deleteChats = false) => {
const character = CharacterContextMenu.#getCharacter(characterId); const character = CharacterContextMenu.#getCharacter(characterId);
return fetch('/api/characters/delete', { await deleteCharacter(character.avatar, { deleteChats: deleteChats });
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; static #getCharacter = (characterId) => characters[characterId] ?? null;

View File

@ -1,5 +1,5 @@
import { getContext } from './extensions.js'; import { getContext } from './extensions.js';
import { getRequestHeaders } from '../script.js'; import { callPopup, getRequestHeaders } from '../script.js';
import { isMobile } from './RossAscends-mods.js'; import { isMobile } from './RossAscends-mods.js';
import { collapseNewlines } from './power-user.js'; import { collapseNewlines } from './power-user.js';
import { debounce_timeout } from './constants.js'; import { debounce_timeout } from './constants.js';
@ -1720,3 +1720,38 @@ export function highlightRegex(regexStr) {
return `<span class="regex-highlight">${regexStr}</span>`; return `<span class="regex-highlight">${regexStr}</span>`;
} }
/**
* Confirms if the user wants to overwrite an existing data object (like character, world info, etc) if one exists.
* If no data with the name exists, this simply returns true.
*
* @param {string} type - The type of the check ("World Info", "Character", etc)
* @param {string[]} existingNames - The list of existing names to check against
* @param {string} name - The new name
* @param {object} options - Optional parameters
* @param {boolean} [options.interactive=false] - Whether to show a confirmation dialog when needing to overwrite an existing data object
* @param {string} [options.actionName='overwrite'] - The action name to display in the confirmation dialog
* @param {(existingName:string)=>void} [options.deleteAction=null] - Optional action to execute wen deleting an existing data object on overwrite
* @returns {Promise<boolean>} True if the user confirmed the overwrite or there is no overwrite needed, false otherwise
*/
export async function checkOverwriteExistingData(type, existingNames, name, { interactive = false, actionName = 'Overwrite', deleteAction = null } = {}) {
const existing = existingNames.find(x => equalsIgnoreCaseAndAccents(x, name));
if (!existing) {
return true;
}
const overwrite = interactive ? await callPopup(`<h3>${type} ${actionName}</h3><p>A ${type.toLowerCase()} with the same name already exists:<br />${existing}</p>Do you want to overwrite it?`, 'confirm') : false;
if (!overwrite) {
toastr.warning(`${type} ${actionName.toLowerCase()} cancelled. A ${type.toLowerCase()} with the same name already exists:<br />${existing}`, `${type} ${actionName}`, { escapeHtml: false });
return false;
}
toastr.info(`Overwriting Existing ${type}:<br />${existing}`, `${type} ${actionName}`, { escapeHtml: false });
// If there is an action to delete the existing data, do it, as the name might be slightly different so file name would not be the same
if (deleteAction) {
deleteAction(existing);
}
return true;
}

View File

@ -1,5 +1,5 @@
import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js'; import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js';
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean, equalsIgnoreCaseAndAccents, getSanitizedFilename } from './utils.js'; import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean, equalsIgnoreCaseAndAccents, getSanitizedFilename, checkOverwriteExistingData } from './utils.js';
import { extension_settings, getContext } from './extensions.js'; import { extension_settings, getContext } from './extensions.js';
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js'; import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
import { isMobile } from './RossAscends-mods.js'; import { isMobile } from './RossAscends-mods.js';
@ -2619,27 +2619,27 @@ function getFreeWorldName() {
* Creates a new world info/lorebook with the given name. * Creates a new world info/lorebook with the given name.
* Checks if a world with the same name already exists, providing a warning or optionally a user confirmation dialog. * Checks if a world with the same name already exists, providing a warning or optionally a user confirmation dialog.
* *
* @param {string} worldInfoName - The name of the new world info * @param {string} worldName - The name of the new world info
* @param {Object} options - Optional parameters * @param {Object} options - Optional parameters
* @param {boolean} [options.interactive=false] - Whether to show a confirmation dialog when overwriting an existing world * @param {boolean} [options.interactive=false] - Whether to show a confirmation dialog when overwriting an existing world
* @returns {Promise<boolean>} - True if the world info was successfully created, false otherwise * @returns {Promise<boolean>} - True if the world info was successfully created, false otherwise
*/ */
async function createNewWorldInfo(worldInfoName, { interactive = false } = {}) { async function createNewWorldInfo(worldName, { interactive = false } = {}) {
const worldInfoTemplate = { entries: {} }; const worldInfoTemplate = { entries: {} };
if (!worldInfoName) { if (!worldName) {
return false; return false;
} }
const allowed = await checkCanOverwriteWorldInfo(worldInfoName, { interactive: interactive, actionName: 'Create' }); const allowed = await checkOverwriteExistingData('World Info', world_names, worldName, { interactive: interactive, actionName: 'Create', deleteAction: (existingName) => deleteWorldInfo(existingName) });
if (!allowed) { if (!allowed) {
return false; return false;
} }
await saveWorldInfo(worldInfoName, worldInfoTemplate, true); await saveWorldInfo(worldName, worldInfoTemplate, true);
await updateWorldInfoList(); await updateWorldInfoList();
const selectedIndex = world_names.indexOf(worldInfoName); const selectedIndex = world_names.indexOf(worldName);
if (selectedIndex !== -1) { if (selectedIndex !== -1) {
$('#world_editor_select').val(selectedIndex).trigger('change'); $('#world_editor_select').val(selectedIndex).trigger('change');
} else { } else {
@ -2649,37 +2649,6 @@ async function createNewWorldInfo(worldInfoName, { interactive = false } = {}) {
return true; return true;
} }
/**
* Confirms if the user wants to overwrite an existing world info with the same name.
* If no world info with the name exists, this simply returns true
*
* @param {string} name - The name of the world info to create
* @param {Object} options - Optional parameters
* @param {boolean} [options.interactive=false] - Whether to show a confirmation dialog when overwriting an existing world
* @param {string} [options.actionName='overwrite'] - The action name to display in the confirmation dialog
* @returns {Promise<boolean>} True if the user confirmed the overwrite, false otherwise
*/
async function checkCanOverwriteWorldInfo(name, { interactive = false, actionName = 'Overwrite' } = {}) {
const existingWorld = world_names.find(x => equalsIgnoreCaseAndAccents(x, name));
if (!existingWorld) {
return true;
}
const overwrite = interactive ? await callPopup(`<h3>World Info ${actionName}</h3><p>A world with the same name already exists:<br />${existingWorld}</p>Do you want to overwrite it?`, 'confirm') : false;
if (!overwrite) {
toastr.warning(`World ${actionName.toLowerCase()} cancelled. A world with the same name already exists:<br />${existingWorld}`, `World Info ${actionName}`, { escapeHtml: false });
return false;
}
toastr.info(`Overwriting Existing World Info:<br />${existingWorld}`, `World Info ${actionName}`, { escapeHtml: false });
// Manually delete, as we want to overwrite. The name might be slightly different so file name would not be the same.
await deleteWorldInfo(existingWorld);
return true;
}
async function getCharacterLore() { async function getCharacterLore() {
const character = characters[this_chid]; const character = characters[this_chid];
const name = character?.name; const name = character?.name;
@ -3612,7 +3581,7 @@ export async function importWorldInfo(file) {
const worldName = file.name.substr(0, file.name.lastIndexOf(".")); const worldName = file.name.substr(0, file.name.lastIndexOf("."));
const sanitizedWorldName = await getSanitizedFilename(worldName); const sanitizedWorldName = await getSanitizedFilename(worldName);
const allowed = await checkCanOverwriteWorldInfo(sanitizedWorldName, { interactive: true, actionName: 'Import' }); const allowed = await checkOverwriteExistingData('World Info', world_names, sanitizedWorldName, { interactive: true, actionName: 'Import', deleteAction: (existingName) => deleteWorldInfo(existingName) });
if (!allowed) { if (!allowed) {
return false; return false;
} }