diff --git a/public/script.js b/public/script.js index aa6e5f16b..13ece6069 100644 --- a/public/script.js +++ b/public/script.js @@ -101,6 +101,7 @@ import { proxies, loadProxyPresets, selected_proxy, + initOpenai, } from './scripts/openai.js'; import { @@ -915,6 +916,7 @@ async function firstLoadInit() { initKeyboard(); initDynamicStyles(); initTags(); + initOpenai(); await getUserAvatars(true, user_avatar); await getCharacters(); await getBackgrounds(); @@ -9232,12 +9234,15 @@ jQuery(async function () { * @param {HTMLTextAreaElement} e Textarea element to auto-fit */ function autoFitEditTextArea(e) { + const computedStyle = window.getComputedStyle(e); + const maxHeight = parseInt(computedStyle.maxHeight, 10); scroll_holder = chatElement[0].scrollTop; - e.style.height = '0'; - e.style.height = `${e.scrollHeight + 4}px`; + e.style.height = computedStyle.minHeight; + const newHeight = e.scrollHeight + 4; + e.style.height = (newHeight < maxHeight) ? `${newHeight}px` : `${maxHeight}px`; is_use_scroll_holder = true; } - const autoFitEditTextAreaDebounced = debouncedThrottle(autoFitEditTextArea, debounce_timeout.standard); + const autoFitEditTextAreaDebounced = debouncedThrottle(autoFitEditTextArea, debounce_timeout.short); document.addEventListener('input', e => { if (e.target instanceof HTMLTextAreaElement && e.target.classList.contains('edit_textarea')) { const immediately = e.target.scrollHeight > e.target.offsetHeight || e.target.value === ''; diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index dceba9063..5c56b3be5 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -31,7 +31,7 @@ import { SECRET_KEYS, secret_state, } from './secrets.js'; -import { debounce, getStringHash, isValidUrl } from './utils.js'; +import { debounce, debouncedThrottle, getStringHash, isValidUrl } from './utils.js'; import { chat_completion_sources, oai_settings } from './openai.js'; import { getTokenCountAsync } from './tokenizers.js'; import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js'; @@ -694,20 +694,23 @@ const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; * this makes the chat input text area resize vertically to match the text size (limited by CSS at 50% window height) */ function autoFitSendTextArea() { + // Needs to be pulled dynamically because it is affected by font size changes + const computedStyle = window.getComputedStyle(sendTextArea); const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight); if (Math.ceil(sendTextArea.scrollHeight + 3) >= Math.floor(sendTextArea.offsetHeight)) { - // Needs to be pulled dynamically because it is affected by font size changes - const sendTextAreaMinHeight = window.getComputedStyle(sendTextArea).getPropertyValue('min-height'); + const sendTextAreaMinHeight = computedStyle.minHeight; sendTextArea.style.height = sendTextAreaMinHeight; } - sendTextArea.style.height = sendTextArea.scrollHeight + 3 + 'px'; + const maxHeight = parseInt(computedStyle.maxHeight, 10); + const newHeight = sendTextArea.scrollHeight + 3; + sendTextArea.style.height = (newHeight < maxHeight) ? `${newHeight}px` : `${maxHeight}px`; if (!isFirefox) { const newScrollTop = Math.round(chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom)); chatBlock.scrollTop = newScrollTop; } } -export const autoFitSendTextAreaDebounced = debounce(autoFitSendTextArea); +export const autoFitSendTextAreaDebounced = debouncedThrottle(autoFitSendTextArea, debounce_timeout.short); // --------------------------------------------------- diff --git a/public/scripts/extensions/caption/index.js b/public/scripts/extensions/caption/index.js index b47f73a2f..dbfbc0d1d 100644 --- a/public/scripts/extensions/caption/index.js +++ b/public/scripts/extensions/caption/index.js @@ -333,8 +333,9 @@ async function getCaptionForFile(file, prompt, quiet) { return caption; } catch (error) { - toastr.error('Failed to caption image.'); - console.log(error); + const errorMessage = error.message || 'Unknown error'; + toastr.error(errorMessage, "Failed to caption image."); + console.error(error); return ''; } finally { diff --git a/public/scripts/extensions/memory/index.js b/public/scripts/extensions/memory/index.js index 2bb5c1269..0bc2ff577 100644 --- a/public/scripts/extensions/memory/index.js +++ b/public/scripts/extensions/memory/index.js @@ -914,7 +914,7 @@ jQuery(async function () { await addExtensionControls(); loadSettings(); - eventSource.on(event_types.MESSAGE_RECEIVED, onChatEvent); + eventSource.makeLast(event_types.CHARACTER_MESSAGE_RENDERED, onChatEvent); eventSource.on(event_types.MESSAGE_DELETED, onChatEvent); eventSource.on(event_types.MESSAGE_EDITED, onChatEvent); eventSource.on(event_types.MESSAGE_SWIPED, onChatEvent); diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index 5c5cf7d7e..7bd108243 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -239,7 +239,7 @@ eventSource.on(event_types.CHAT_CHANGED, (...args)=>executeIfReadyElseQueue(onCh const onUserMessage = async () => { await autoExec.handleUser(); }; -eventSource.on(event_types.USER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onUserMessage, args)); +eventSource.makeFirst(event_types.USER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onUserMessage, args)); const onAiMessage = async (messageId) => { if (['...'].includes(chat[messageId]?.mes)) { @@ -249,7 +249,7 @@ const onAiMessage = async (messageId) => { await autoExec.handleAi(); }; -eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onAiMessage, args)); +eventSource.makeFirst(event_types.CHARACTER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onAiMessage, args)); const onGroupMemberDraft = async () => { await autoExec.handleGroupMemberDraft(); diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 00d319121..b30894f41 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -4679,22 +4679,23 @@ function runProxyCallback(_, value) { return foundName; } -SlashCommandParser.addCommandObject(SlashCommand.fromProps({ - name: 'proxy', - callback: runProxyCallback, - returns: 'current proxy', - namedArgumentList: [], - unnamedArgumentList: [ - SlashCommandArgument.fromProps({ - description: 'name', - typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - enumProvider: () => proxies.map(preset => new SlashCommandEnumValue(preset.name, preset.url)), - }), - ], - helpString: 'Sets a proxy preset by name.', -})); - +export function initOpenai() { + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'proxy', + callback: runProxyCallback, + returns: 'current proxy', + namedArgumentList: [], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: () => proxies.map(preset => new SlashCommandEnumValue(preset.name, preset.url)), + }), + ], + helpString: 'Sets a proxy preset by name.', + })); +} $(document).ready(async function () { $('#test_api_button').on('click', testApiConnection); diff --git a/public/scripts/popup.js b/public/scripts/popup.js index 5eba4db4c..ed0a52577 100644 --- a/public/scripts/popup.js +++ b/public/scripts/popup.js @@ -194,7 +194,7 @@ export class Popup { const buttonElement = document.createElement('div'); buttonElement.classList.add('menu_button', 'popup-button-custom', 'result-control'); buttonElement.classList.add(...(button.classes ?? [])); - buttonElement.dataset.result = String(button.result ?? undefined); + buttonElement.dataset.result = String(button.result); // This is expected to also write 'null' or 'staging', to indicate cancel and no action respectively buttonElement.textContent = button.text; buttonElement.dataset.i18n = buttonElement.textContent; buttonElement.tabIndex = 0; @@ -317,9 +317,14 @@ export class Popup { // Bind event listeners for all result controls to their defined event type this.dlg.querySelectorAll('[data-result]').forEach(resultControl => { if (!(resultControl instanceof HTMLElement)) return; - const result = Number(resultControl.dataset.result); - if (String(undefined) === String(resultControl.dataset.result)) return; - if (isNaN(result)) throw new Error('Invalid result control. Result must be a number. ' + resultControl.dataset.result); + // If no value was set, we exit out and don't bind an action + if (String(resultControl.dataset.result) === String(undefined)) return; + + // Make sure that both `POPUP_RESULT` numbers and also `null` as 'cancelled' are supported + const result = String(resultControl.dataset.result) === String(null) ? null + : Number(resultControl.dataset.result); + + if (result !== null && isNaN(result)) throw new Error('Invalid result control. Result must be a number. ' + resultControl.dataset.result); const type = resultControl.dataset.resultEvent || 'click'; resultControl.addEventListener(type, async () => await this.complete(result)); }); diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index f57de63ff..08fbfb504 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -2734,45 +2734,26 @@ async function doDelMode(_, text) { return ''; } - //first enter delmode - $('#option_delete_mes').trigger('click', { fromSlashCommand: true }); - - //parse valid args - if (text) { - await delay(300); //same as above, need event signal for 'entered del mode' - console.debug('parsing msgs to del'); - let numMesToDel = Number(text); - let lastMesID = Number($('#chat .mes').last().attr('mesid')); - let oldestMesIDToDel = lastMesID - numMesToDel + 1; - - if (oldestMesIDToDel < 0) { - toastr.warning(`Cannot delete more than ${chat.length} messages.`); - return ''; - } - - let oldestMesToDel = $('#chat').find(`.mes[mesid=${oldestMesIDToDel}]`); - - if (!oldestMesIDToDel && lastMesID > 0) { - oldestMesToDel = await loadUntilMesId(oldestMesIDToDel); - - if (!oldestMesToDel || !oldestMesToDel.length) { - return ''; - } - } - - let oldestDelMesCheckbox = $(oldestMesToDel).find('.del_checkbox'); - let newLastMesID = oldestMesIDToDel - 1; - console.debug(`DelMesReport -- numMesToDel: ${numMesToDel}, lastMesID: ${lastMesID}, oldestMesIDToDel:${oldestMesIDToDel}, newLastMesID: ${newLastMesID}`); - oldestDelMesCheckbox.trigger('click'); - let trueNumberOfDeletedMessage = lastMesID - oldestMesIDToDel + 1; - - //await delay(1) - $('#dialogue_del_mes_ok').trigger('click'); - toastr.success(`Deleted ${trueNumberOfDeletedMessage} messages.`); + // Just enter the delete mode. + if (!text) { + $('#option_delete_mes').trigger('click', { fromSlashCommand: true }); return ''; } - return ''; + const count = Number(text); + + // Nothing to delete. + if (count < 1) { + return ''; + } + + if (count > chat.length) { + toastr.warning(`Cannot delete more than ${chat.length} messages.`); + return ''; + } + + const range = `${chat.length - count}-${chat.length - 1}`; + return doMesCut(_, range); } function doResetPanels() { diff --git a/public/scripts/preset-manager.js b/public/scripts/preset-manager.js index 90fe232ba..5d25140dc 100644 --- a/public/scripts/preset-manager.js +++ b/public/scripts/preset-manager.js @@ -1,6 +1,5 @@ import { amount_gen, - callPopup, characters, eventSource, event_types, @@ -19,6 +18,7 @@ import { import { groups, selected_group } from './group-chats.js'; import { instruct_presets } from './instruct-mode.js'; import { kai_settings } from './kai-settings.js'; +import { Popup } from './popup.js'; import { context_presets, getContextSettings, power_user } from './power-user.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js'; @@ -165,11 +165,8 @@ class PresetManager { async savePresetAs() { const inputValue = this.getSelectedPresetName(); - const popupText = ` -

Preset name:

- ${!this.isNonGenericApi() ? '

Hint: Use a character/group name to bind preset to a specific chat.

' : ''}`; - const name = await callPopup(popupText, 'input', inputValue); - + const popupText = !this.isNonGenericApi() ? '

Hint: Use a character/group name to bind preset to a specific chat.

' : ''; + const name = await Popup.show.input('Preset name:', popupText, inputValue); if (!name) { console.log('Preset name not provided'); return; @@ -372,7 +369,7 @@ class PresetManager { if (Object.keys(preset_names).length) { const nextPresetName = Object.keys(preset_names)[0]; const newValue = preset_names[nextPresetName]; - $(this.select).find(`option[value="${newValue}"]`).attr('selected', true); + $(this.select).find(`option[value="${newValue}"]`).attr('selected', 'true'); $(this.select).trigger('change'); } @@ -597,8 +594,7 @@ export async function initPresetManager() { return; } - const confirm = await callPopup('Delete the preset? This action is irreversible and your current settings will be overwritten.', 'confirm'); - + const confirm = await Popup.show.confirm('Delete the preset?', 'This action is irreversible and your current settings will be overwritten.'); if (!confirm) { return; } @@ -641,8 +637,7 @@ export async function initPresetManager() { return; } - const confirm = await callPopup('

Are you sure?

Resetting a default preset will restore the default settings.', 'confirm'); - + const confirm = await Popup.show.confirm('Are you sure?', 'Resetting a default preset will restore the default settings.'); if (!confirm) { return; } @@ -653,8 +648,7 @@ export async function initPresetManager() { presetManager.selectPreset(option); toastr.success('Default preset restored'); } else { - const confirm = await callPopup('

Are you sure?

Resetting a custom preset will restore to the last saved state.', 'confirm'); - + const confirm = await Popup.show.confirm('Are you sure?', 'Resetting a custom preset will restore to the last saved state.'); if (!confirm) { return; } diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 43d065f54..46b862cc2 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -955,14 +955,36 @@ export function initDefaultSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'fuzzy', callback: fuzzyCallback, - returns: 'first matching item', + returns: 'matching item', namedArgumentList: [ - new SlashCommandNamedArgument( - 'list', 'list of items to match against', [ARGUMENT_TYPE.LIST], true, - ), - new SlashCommandNamedArgument( - 'threshold', 'fuzzy match threshold (0.0 to 1.0)', [ARGUMENT_TYPE.NUMBER], false, false, '0.4', - ), + SlashCommandNamedArgument.fromProps({ + name: 'list', + description: 'list of items to match against', + acceptsMultiple: false, + isRequired: true, + typeList: [ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.VARIABLE_NAME], + enumProvider: commonEnumProviders.variables('all'), + }), + SlashCommandNamedArgument.fromProps({ + name: 'threshold', + description: 'fuzzy match threshold (0.0 to 1.0)', + typeList: [ARGUMENT_TYPE.NUMBER], + isRequired: false, + defaultValue: '0.4', + acceptsMultiple: false, + }), + SlashCommandNamedArgument.fromProps({ + name: 'mode', + description: 'fuzzy match mode', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: false, + defaultValue: 'first', + acceptsMultiple: false, + enumList: [ + new SlashCommandEnumValue('first', 'first match below the threshold', enumTypes.enum, enumIcons.default), + new SlashCommandEnumValue('best', 'best match below the threshold', enumTypes.enum, enumIcons.default), + ], + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -979,6 +1001,13 @@ export function initDefaultSlashCommands() { A low value (min 0.0) means the match is very strict. At 1.0 (max) the match is very loose and will match anything. +
+ The optional mode argument allows to control the behavior when multiple items match the text. + +
The returned value passes to the next command through the pipe.
@@ -1882,7 +1911,7 @@ async function inputCallback(args, prompt) { * @param {FuzzyCommandArgs} args - arguments containing "list" (JSON array) and optionaly "threshold" (float between 0.0 and 1.0) * @param {string} searchInValue - the string where items of list are searched * @returns {string} - the matched item from the list - * @typedef {{list: string, threshold: string}} FuzzyCommandArgs - arguments for /fuzzy command + * @typedef {{list: string, threshold: string, mode:string}} FuzzyCommandArgs - arguments for /fuzzy command * @example /fuzzy list=["down","left","up","right"] "he looks up" | /echo // should return "up" * @link https://www.fusejs.io/ */ @@ -1912,7 +1941,7 @@ function fuzzyCallback(args, searchInValue) { }; // threshold determines how strict is the match, low threshold value is very strict, at 1 (nearly?) everything matches if ('threshold' in args) { - params.threshold = parseFloat(resolveVariable(args.threshold)); + params.threshold = parseFloat(args.threshold); if (isNaN(params.threshold)) { console.warn('WARN: \'threshold\' argument must be a float between 0.0 and 1.0 for /fuzzy command'); return ''; @@ -1925,16 +1954,42 @@ function fuzzyCallback(args, searchInValue) { } } - const fuse = new Fuse([searchInValue], params); - // each item in the "list" is searched within "search_item", if any matches it returns the matched "item" - for (const searchItem of list) { - const result = fuse.search(searchItem); - if (result.length > 0) { - console.info('fuzzyCallback Matched: ' + searchItem); - return searchItem; + function getFirstMatch() { + const fuse = new Fuse([searchInValue], params); + // each item in the "list" is searched within "search_item", if any matches it returns the matched "item" + for (const searchItem of list) { + const result = fuse.search(searchItem); + console.debug('/fuzzy: result', result); + if (result.length > 0) { + console.info('/fuzzy: first matched', searchItem); + return searchItem; + } } + + console.info('/fuzzy: no match'); + return ''; + } + + function getBestMatch() { + const fuse = new Fuse(list, params); + const result = fuse.search(searchInValue); + console.debug('/fuzzy: result', result); + if (result.length > 0) { + console.info('/fuzzy: best matched', result[0].item); + return result[0].item; + } + + console.info('/fuzzy: no match'); + return ''; + } + + switch (String(args.mode).trim().toLowerCase()) { + case 'best': + return getBestMatch(); + case 'first': + default: + return getFirstMatch(); } - return ''; } catch { console.warn('WARN: Invalid list argument provided for /fuzzy command'); return ''; diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js index 7305a5c82..6ef10b59b 100644 --- a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js +++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js @@ -1,11 +1,11 @@ -import { chat_metadata, characters, substituteParams, chat, extension_prompt_roles, extension_prompt_types } from "../../script.js"; -import { extension_settings } from "../extensions.js"; -import { getGroupMembers, groups, selected_group } from "../group-chats.js"; -import { power_user } from "../power-user.js"; -import { searchCharByName, getTagsList, tags } from "../tags.js"; -import { SlashCommandClosure } from "./SlashCommandClosure.js"; -import { SlashCommandEnumValue, enumTypes } from "./SlashCommandEnumValue.js"; -import { SlashCommandExecutor } from "./SlashCommandExecutor.js"; +import { chat_metadata, characters, substituteParams, chat, extension_prompt_roles, extension_prompt_types } from '../../script.js'; +import { extension_settings } from '../extensions.js'; +import { getGroupMembers, groups } from '../group-chats.js'; +import { power_user } from '../power-user.js'; +import { searchCharByName, getTagsList, tags } from '../tags.js'; +import { world_names } from '../world-info.js'; +import { SlashCommandClosure } from './SlashCommandClosure.js'; +import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js'; import { SlashCommandScope } from "./SlashCommandScope.js"; /** @@ -104,8 +104,8 @@ export const enumIcons = { // Remove possible nullable types definition to match type icon type = type.replace(/\?$/, ''); return enumIcons[type] ?? enumIcons.default; - } -} + }, +}; /** * A collection of common enum providers @@ -181,7 +181,7 @@ export const commonEnumProviders = { * @param {('all' | 'existing' | 'not-existing')?} [mode='all'] - Which types of tags to show * @returns {() => SlashCommandEnumValue[]} */ - tagsForChar: (mode = 'all') => (/** @type {SlashCommandExecutor} */ executor) => { + tagsForChar: (mode = 'all') => (/** @type {import('./SlashCommandExecutor.js').SlashCommandExecutor} */ executor) => { // Try to see if we can find the char during execution to filter down the tags list some more. Otherwise take all tags. const charName = executor.namedArgumentList.find(it => it.name == 'name')?.value; if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures'); @@ -214,7 +214,7 @@ export const commonEnumProviders = { * * @returns {SlashCommandEnumValue[]} */ - worlds: () => $('#world_info').children().toArray().map(x => new SlashCommandEnumValue(x.textContent, null, enumTypes.name, enumIcons.world)), + worlds: () => world_names.map(worldName => new SlashCommandEnumValue(worldName, null, enumTypes.name, enumIcons.world)), /** * All existing injects for the current chat diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 355ac676b..d497e8020 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -14,7 +14,6 @@ import { SlashCommand } from './slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; -import { SlashCommandExecutor } from './slash-commands/SlashCommandExecutor.js'; import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js'; @@ -1215,7 +1214,7 @@ function registerWorldInfoSlashCommands() { enumTypes.enum, enumIcons.getDataTypeIcon(value.type))), /** All existing UIDs based on the file argument as world name */ - wiUids: (/** @type {SlashCommandExecutor} */ executor) => { + wiUids: (/** @type {import('./slash-commands/SlashCommandExecutor.js').SlashCommandExecutor} */ executor) => { const file = executor.namedArgumentList.find(it => it.name == 'file')?.value; if (file instanceof SlashCommandClosure) throw new Error('Argument \'file\' does not support closures'); // Try find world from cache @@ -3161,7 +3160,8 @@ function duplicateWorldInfoEntry(data, uid) { } // Exclude uid and gather the rest of the properties - const { uid: _, ...originalData } = data.entries[uid]; + const originalData = Object.assign({}, data.entries[uid]); + delete originalData.uid; // Create new entry and copy over data const entry = createWorldInfoEntry(data.name, data); @@ -4326,8 +4326,9 @@ function onWorldInfoChange(args, text) { $('#world_info').val(null).trigger('change'); } } else { //if it's a pointer selection - let tempWorldInfo = []; - let selectedWorlds = $('#world_info').val().map((e) => Number(e)).filter((e) => !isNaN(e)); + const tempWorldInfo = []; + const val = $('#world_info').val(); + const selectedWorlds = (Array.isArray(val) ? val : [val]).map((e) => Number(e)).filter((e) => !isNaN(e)); if (selectedWorlds.length > 0) { selectedWorlds.forEach((worldIndex) => { const existingWorldName = world_names[worldIndex];