Implement failsafe for world creation with same name

- Fixes #2297
- Added another utils function for string comparison
This commit is contained in:
Wolfsblvt 2024-05-22 18:19:01 +02:00
parent 5e970c8a51
commit 29d817d549
2 changed files with 90 additions and 29 deletions

View File

@ -1469,23 +1469,47 @@ export function flashHighlight(element, timespan = 2000) {
setTimeout(() => element.removeClass('flash animated'), timespan);
}
/**
* A common base function for case-insensitive and accent-insensitive string comparisons.
*
* @param {string} a - The first string to compare.
* @param {string} b - The second string to compare.
* @param {(a:string,b:string)=>boolean} comparisonFunction - The function to use for the comparison.
* @returns {*} - The result of the comparison.
*/
export function compareIgnoreCaseAndAccents(a, b, comparisonFunction) {
if (!a || !b) return comparisonFunction(a, b); // Return the comparison result if either string is empty
// Normalize and remove diacritics, then convert to lower case
const normalizedA = a.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
const normalizedB = b.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
// Check if the normalized strings are equal
return comparisonFunction(normalizedA, normalizedB);
}
/**
* Performs a case-insensitive and accent-insensitive substring search.
* This function normalizes the strings to remove diacritical marks and converts them to lowercase to ensure the search is insensitive to case and accents.
*
* @param {string} text - The text in which to search for the substring.
* @param {string} searchTerm - The substring to search for in the text.
* @returns {boolean} - Returns true if the searchTerm is found within the text, otherwise returns false.
* @param {string} text - The text in which to search for the substring
* @param {string} searchTerm - The substring to search for in the text
* @returns {boolean} true if the searchTerm is found within the text, otherwise returns false
*/
export function includesIgnoreCaseAndAccents(text, searchTerm) {
if (!text || !searchTerm) return false; // Return false if either string is empty
return compareIgnoreCaseAndAccents(text, searchTerm, (a, b) => a?.includes(b) === true);
}
// Normalize and remove diacritics, then convert to lower case
const normalizedText = text.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
const normalizedSearchTerm = searchTerm.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
// Check if the normalized text includes the normalized search term
return normalizedText.includes(normalizedSearchTerm);
/**
* Performs a case-insensitive and accent-insensitive equality check.
* This function normalizes the strings to remove diacritical marks and converts them to lowercase to ensure the search is insensitive to case and accents.
*
* @param {string} a - The first string to compare
* @param {string} b - The second string to compare
* @returns {boolean} true if the strings are equal, otherwise returns false
*/
export function equalsIgnoreCaseAndAccents(a, b) {
return compareIgnoreCaseAndAccents(a, b, (a, b) => a === b);
}
/**

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 { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean } 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 } from './utils.js';
import { extension_settings, getContext } from './extensions.js';
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
import { isMobile } from './RossAscends-mods.js';
@ -50,6 +50,7 @@ const WI_ENTRY_EDIT_TEMPLATE = $('#entry_edit_template .world_entry');
let world_info = {};
let selected_world_info = [];
/** @type {string[]} */
let world_names;
let world_info_depth = 2;
let world_info_min_activations = 0; // if > 0, will continue seeking chat until minimum world infos are activated
@ -2544,9 +2545,15 @@ async function renameWorldInfo(name, data) {
}
}
/**
* Deletes a world info with the given name
*
* @param {string} worldInfoName - The name of the world info to delete
* @returns {Promise<boolean>} A promise that resolves to true if the world info was successfully deleted, false otherwise
*/
async function deleteWorldInfo(worldInfoName) {
if (!world_names.includes(worldInfoName)) {
return;
return false;
}
const response = await fetch('/api/worldinfo/delete', {
@ -2555,24 +2562,28 @@ async function deleteWorldInfo(worldInfoName) {
body: JSON.stringify({ name: worldInfoName }),
});
if (response.ok) {
const existingWorldIndex = selected_world_info.findIndex((e) => e === worldInfoName);
if (existingWorldIndex !== -1) {
selected_world_info.splice(existingWorldIndex, 1);
saveSettingsDebounced();
}
if (!response.ok) {
return false;
}
await updateWorldInfoList();
$('#world_editor_select').trigger('change');
const existingWorldIndex = selected_world_info.findIndex((e) => e === worldInfoName);
if (existingWorldIndex !== -1) {
selected_world_info.splice(existingWorldIndex, 1);
saveSettingsDebounced();
}
if ($('#character_world').val() === worldInfoName) {
$('#character_world').val('').trigger('change');
setWorldInfoButtonClass(undefined, false);
if (menu_type != 'create') {
saveCharacterDebounced();
}
await updateWorldInfoList();
$('#world_editor_select').trigger('change');
if ($('#character_world').val() === worldInfoName) {
$('#character_world').val('').trigger('change');
setWorldInfoButtonClass(undefined, false);
if (menu_type != 'create') {
saveCharacterDebounced();
}
}
return true;
}
function getFreeWorldEntryUid(data) {
@ -2604,11 +2615,35 @@ function getFreeWorldName() {
return undefined;
}
async function createNewWorldInfo(worldInfoName) {
/**
* 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.
*
* @param {string} worldInfoName - The name of the new world info
* @param {Object} options - Optional parameters
* @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
*/
async function createNewWorldInfo(worldInfoName, { interactive = false } = {}) {
const worldInfoTemplate = { entries: {} };
if (!worldInfoName) {
return;
return false;
}
const existingWorld = world_names.find(x => equalsIgnoreCaseAndAccents(x, worldInfoName));
if (existingWorld) {
const overwrite = interactive ? await callPopup(`<h3>Creating New World Info</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 creation cancelled. A world with the same name already exists:<br />${existingWorld}`, 'Creating New World Info', { escapeHtml: false });
return false;
}
toastr.info(`Overwriting Existing World Info:<br />${existingWorld}`, 'Creating New World Info', { 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);
}
await saveWorldInfo(worldInfoName, worldInfoTemplate, true);
@ -2620,6 +2655,8 @@ async function createNewWorldInfo(worldInfoName) {
} else {
hideWorldEditor();
}
return true;
}
async function getCharacterLore() {
@ -3644,7 +3681,7 @@ jQuery(() => {
const finalName = await callPopup('<h3>Create a new World Info?</h3>Enter a name for the new file:', 'input', tempName);
if (finalName) {
await createNewWorldInfo(finalName);
await createNewWorldInfo(finalName, { interactive: true });
}
});