mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-01-05 21:46:49 +01:00
Implement failsafe for world creation with same name
- Fixes #2297 - Added another utils function for string comparison
This commit is contained in:
parent
5e970c8a51
commit
29d817d549
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user