diff --git a/public/scripts/extensions/tts/system.js b/public/scripts/extensions/tts/system.js
index 2a6acad30..169d89124 100644
--- a/public/scripts/extensions/tts/system.js
+++ b/public/scripts/extensions/tts/system.js
@@ -97,9 +97,9 @@ class SystemTtsProvider {
return `
Uses the voices provided by your operating system
-
+
-
`;
+
`;
}
onSettingsChange() {
@@ -147,7 +147,7 @@ class SystemTtsProvider {
// Trigger updates
$('#system_tts_rate').on('input', () => { this.onSettingsChange(); });
- $('#system_tts_rate').on('input', () => { this.onSettingsChange(); });
+ $('#system_tts_pitch').on('input', () => { this.onSettingsChange(); });
$('#system_tts_pitch_output').text(this.settings.pitch);
$('#system_tts_rate_output').text(this.settings.rate);
@@ -198,8 +198,8 @@ class SystemTtsProvider {
const text = getPreviewString(voice.lang);
const utterance = new SpeechSynthesisUtterance(text);
utterance.voice = voice;
- utterance.rate = 1;
- utterance.pitch = 1;
+ utterance.rate = this.settings.rate || 1;
+ utterance.pitch = this.settings.pitch || 1;
speechSynthesis.speak(utterance);
}
diff --git a/public/scripts/openai.js b/public/scripts/openai.js
index 160d31b1f..53bcffd91 100644
--- a/public/scripts/openai.js
+++ b/public/scripts/openai.js
@@ -4220,6 +4220,12 @@ async function onModelChange() {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
}
+ else if (oai_settings.groq_model.includes('llama-3.1')) {
+ $('#openai_max_context').attr('max', max_128k);
+ }
+ else if (oai_settings.groq_model.includes('llama3-groq')) {
+ $('#openai_max_context').attr('max', max_8k);
+ }
else if (['llama3-8b-8192', 'llama3-70b-8192', 'gemma-7b-it', 'gemma2-9b-it'].includes(oai_settings.groq_model)) {
$('#openai_max_context').attr('max', max_8k);
}
diff --git a/public/scripts/util/StructuredCloneMap.js b/public/scripts/util/StructuredCloneMap.js
new file mode 100644
index 000000000..42dea5faa
--- /dev/null
+++ b/public/scripts/util/StructuredCloneMap.js
@@ -0,0 +1,56 @@
+/**
+ * A specialized Map class that provides consistent data storage by performing deep cloning of values.
+ *
+ * @template K, V
+ * @extends Map
+ */
+export class StructuredCloneMap extends Map {
+ /**
+ * Constructs a new StructuredCloneMap.
+ * @param {object} options - Options for the map
+ * @param {boolean} options.cloneOnGet - Whether to clone the value when getting it from the map
+ * @param {boolean} options.cloneOnSet - Whether to clone the value when setting it in the map
+ */
+ constructor({ cloneOnGet, cloneOnSet } = { cloneOnGet: true, cloneOnSet: true }) {
+ super();
+ this.cloneOnGet = cloneOnGet;
+ this.cloneOnSet = cloneOnSet;
+ }
+
+ /**
+ * Adds a new element with a specified key and value to the Map. If an element with the same key already exists, the element will be updated.
+ *
+ * The set value will always be a deep clone of the provided value to provide consistent data storage.
+ *
+ * @param {K} key - The key to set
+ * @param {V} value - The value to set
+ * @returns {this} The updated map
+ */
+ set(key, value) {
+ if (!this.cloneOnSet) {
+ return super.set(key, value);
+ }
+
+ const clonedValue = structuredClone(value);
+ super.set(key, clonedValue);
+ return this;
+ }
+
+ /**
+ * Returns a specified element from the Map object.
+ * If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.
+ *
+ * The returned value will always be a deep clone of the cached value.
+ *
+ * @param {K} key - The key to get the value for
+ * @returns {V | undefined} Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.
+ */
+ get(key) {
+ if (!this.cloneOnGet) {
+ return super.get(key);
+ }
+
+ const value = super.get(key);
+ return structuredClone(value);
+ }
+}
diff --git a/public/scripts/utils.js b/public/scripts/utils.js
index c023d70f6..9ba423a38 100644
--- a/public/scripts/utils.js
+++ b/public/scripts/utils.js
@@ -270,6 +270,13 @@ export function getStringHash(str, seed = 0) {
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
}
+/**
+ * Map of debounced functions to their timers.
+ * Weak map is used to avoid memory leaks.
+ * @type {WeakMap}
+ */
+const debounceMap = new WeakMap();
+
/**
* Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked.
* @param {function} func The function to debounce.
@@ -278,10 +285,26 @@ export function getStringHash(str, seed = 0) {
*/
export function debounce(func, timeout = debounce_timeout.standard) {
let timer;
- return (...args) => {
+ let fn = (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
+ debounceMap.set(func, timer);
+ debounceMap.set(fn, timer);
};
+
+ return fn;
+}
+
+/**
+ * Cancels a scheduled debounced function.
+ * Does nothing if the function is not debounced or not scheduled.
+ * @param {function} func The function to cancel. Either the original or the debounced function.
+ */
+export function cancelDebounce(func) {
+ if (debounceMap.has(func)) {
+ clearTimeout(debounceMap.get(func));
+ debounceMap.delete(func);
+ }
}
/**
diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js
index df432f875..25a974dfe 100644
--- a/public/scripts/world-info.js
+++ b/public/scripts/world-info.js
@@ -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, getSanitizedFilename, checkOverwriteExistingData, getStringHash, parseStringArray } 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, getSanitizedFilename, checkOverwriteExistingData, getStringHash, parseStringArray, cancelDebounce } 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';
@@ -16,6 +16,7 @@ import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandE
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
+import { StructuredCloneMap } from './util/StructuredCloneMap.js';
export {
world_info,
@@ -746,7 +747,8 @@ export const wi_anchor_position = {
after: 1,
};
-const worldInfoCache = new Map();
+/** @type {StructuredCloneMap} */
+const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOnSet: false });
/**
* Gets the world info based on chat messages.
@@ -885,9 +887,15 @@ function setWorldInfoSettings(settings, data) {
}
function registerWorldInfoSlashCommands() {
- function reloadEditor(file) {
+ /**
+ * Reloads the editor with the specified world info file
+ * @param {string} file - The file to load in the editor
+ * @param {boolean} [loadIfNotSelected=false] - Indicates whether to load the file even if it's not currently selected
+ */
+ function reloadEditor(file, loadIfNotSelected = false) {
+ const currentIndex = $('#world_editor_select').val();
const selectedIndex = world_names.indexOf(file);
- if (selectedIndex !== -1) {
+ if (selectedIndex !== -1 && (loadIfNotSelected || currentIndex === selectedIndex)) {
$('#world_editor_select').val(selectedIndex).trigger('change');
}
}
@@ -1049,7 +1057,7 @@ function registerWorldInfoSlashCommands() {
entry.content = content;
}
- await saveWorldInfo(file, data, true);
+ await saveWorldInfo(file, data);
reloadEditor(file);
return String(entry.uid);
@@ -1100,7 +1108,7 @@ function registerWorldInfoSlashCommands() {
setOriginalDataValue(data, uid, originalDataKeyMap[field], entry[field]);
}
- await saveWorldInfo(file, data, true);
+ await saveWorldInfo(file, data);
reloadEditor(file);
return '';
}
@@ -1840,7 +1848,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
nextText: '>',
formatNavigator: PAGINATION_TEMPLATE,
showNavigator: true,
- callback: function (/** @type {object[]} */ page) {
+ callback: async function (/** @type {object[]} */ page) {
// We save costly performance by removing all events before emptying. Because we know there are no relevant event handlers reacting on removing elements
// This prevents jQuery from actually going through all registered events on the controls for each entry when removing it
worldEntriesList.find('*').off();
@@ -1926,7 +1934,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
if (counter > 0) {
toastr.info(`Backfilled ${counter} titles`);
- await saveWorldInfo(name, data, true);
+ await saveWorldInfo(name, data);
updateEditor(navigation_option.previous);
}
});
@@ -2026,7 +2034,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
console.table(Object.keys(data.entries).map(uid => data.entries[uid]).map(x => ({ uid: x.uid, key: x.key.join(','), displayIndex: x.displayIndex })));
- await saveWorldInfo(name, data, true);
+ await saveWorldInfo(name, data);
},
});
//$("#world_popup_entries_list").disableSelection();
@@ -2298,7 +2306,7 @@ function getWorldEntry(name, data, entry) {
templateResult: item => templateStyling(item, { searchStyle: true }),
templateSelection: item => templateStyling(item),
});
- input.on('change', function (_, { skipReset, noSave } = {}) {
+ input.on('change', async function (_, { skipReset, noSave } = {}) {
const uid = $(this).data('uid');
/** @type {string[]} */
const keys = ($(this).select2('data')).map(x => x.text);
@@ -2307,7 +2315,7 @@ function getWorldEntry(name, data, entry) {
if (!noSave) {
data.entries[uid][entryPropName] = keys;
setOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
}
});
input.on('select2:select', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data]));
@@ -2338,14 +2346,14 @@ function getWorldEntry(name, data, entry) {
template.find(`select[name="${entryPropName}"]`).hide();
input.show();
- input.on('input', function (_, { skipReset, noSave } = {}) {
+ input.on('input', async function (_, { skipReset, noSave } = {}) {
const uid = $(this).data('uid');
const value = String($(this).val());
!skipReset && resetScrollHeight(this);
if (!noSave) {
data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value);
setOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
}
});
input.val(entry[entryPropName].join(', ')).trigger('input', { skipReset: true });
@@ -2384,12 +2392,12 @@ function getWorldEntry(name, data, entry) {
event.stopPropagation();
});
- selectiveLogicDropdown.on('input', function () {
+ selectiveLogicDropdown.on('input', async function () {
const uid = $(this).data('uid');
const value = Number($(this).val());
data.entries[uid].selectiveLogic = !isNaN(value) ? value : world_info_logic.AND_ANY;
setOriginalDataValue(data, uid, 'selectiveLogic', data.entries[uid].selectiveLogic);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
template
@@ -2404,7 +2412,7 @@ function getWorldEntry(name, data, entry) {
// exclude characters checkbox
const characterExclusionInput = template.find('input[name="character_exclusion"]');
characterExclusionInput.data('uid', entry.uid);
- characterExclusionInput.on('input', function () {
+ characterExclusionInput.on('input', async function () {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
characterFilterLabel.text(value ? 'Exclude Character(s)' : 'Filter to Character(s)');
@@ -2438,7 +2446,7 @@ function getWorldEntry(name, data, entry) {
}
setOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
characterExclusionInput.prop('checked', entry.characterFilter?.isExclude ?? false).trigger('input');
@@ -2500,24 +2508,24 @@ function getWorldEntry(name, data, entry) {
);
}
setOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
// comment
const commentInput = template.find('textarea[name="comment"]');
const commentToggle = template.find('input[name="addMemo"]');
commentInput.data('uid', entry.uid);
- commentInput.on('input', function (_, { skipReset } = {}) {
+ commentInput.on('input', async function (_, { skipReset } = {}) {
const uid = $(this).data('uid');
const value = $(this).val();
!skipReset && resetScrollHeight(this);
data.entries[uid].comment = value;
setOriginalDataValue(data, uid, 'comment', data.entries[uid].comment);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
commentToggle.data('uid', entry.uid);
- commentToggle.on('input', function () {
+ commentToggle.on('input', async function () {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
//console.log(value)
@@ -2525,7 +2533,7 @@ function getWorldEntry(name, data, entry) {
.closest('.world_entry')
.find('.commentContainer');
data.entries[uid].addMemo = value;
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
value ? commentContainer.show() : commentContainer.hide();
});
@@ -2543,13 +2551,13 @@ function getWorldEntry(name, data, entry) {
const contentInput = template.find('textarea[name="content"]');
contentInput.data('uid', entry.uid);
- contentInput.on('input', function (_, { skipCount } = {}) {
+ contentInput.on('input', async function (_, { skipCount } = {}) {
const uid = $(this).data('uid');
const value = $(this).val();
data.entries[uid].content = value;
setOriginalDataValue(data, uid, 'content', data.entries[uid].content);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
if (skipCount) {
return;
@@ -2573,13 +2581,13 @@ function getWorldEntry(name, data, entry) {
// selective
const selectiveInput = template.find('input[name="selective"]');
selectiveInput.data('uid', entry.uid);
- selectiveInput.on('input', function () {
+ selectiveInput.on('input', async function () {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
data.entries[uid].selective = value;
setOriginalDataValue(data, uid, 'selective', data.entries[uid].selective);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
const keysecondary = $(this)
.closest('.world_entry')
@@ -2608,12 +2616,12 @@ function getWorldEntry(name, data, entry) {
/*
const constantInput = template.find('input[name="constant"]');
constantInput.data("uid", entry.uid);
- constantInput.on("input", function () {
+ constantInput.on("input", async function () {
const uid = $(this).data("uid");
const value = $(this).prop("checked");
data.entries[uid].constant = value;
setOriginalDataValue(data, uid, "constant", data.entries[uid].constant);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
constantInput.prop("checked", entry.constant).trigger("input");
*/
@@ -2621,14 +2629,14 @@ function getWorldEntry(name, data, entry) {
// order
const orderInput = template.find('input[name="order"]');
orderInput.data('uid', entry.uid);
- orderInput.on('input', function () {
+ orderInput.on('input', async function () {
const uid = $(this).data('uid');
const value = Number($(this).val());
data.entries[uid].order = !isNaN(value) ? value : 0;
updatePosOrdDisplay(uid);
setOriginalDataValue(data, uid, 'insertion_order', data.entries[uid].order);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
orderInput.val(entry.order).trigger('input');
orderInput.css('width', 'calc(3em + 15px)');
@@ -2636,13 +2644,13 @@ function getWorldEntry(name, data, entry) {
// group
const groupInput = template.find('input[name="group"]');
groupInput.data('uid', entry.uid);
- groupInput.on('input', function () {
+ groupInput.on('input', async function () {
const uid = $(this).data('uid');
const value = String($(this).val()).trim();
data.entries[uid].group = value;
setOriginalDataValue(data, uid, 'extensions.group', data.entries[uid].group);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
groupInput.val(entry.group ?? '').trigger('input');
setTimeout(() => createEntryInputAutocomplete(groupInput, getInclusionGroupCallback(data), { allowMultiple: true }), 1);
@@ -2650,19 +2658,19 @@ function getWorldEntry(name, data, entry) {
// inclusion priority
const groupOverrideInput = template.find('input[name="groupOverride"]');
groupOverrideInput.data('uid', entry.uid);
- groupOverrideInput.on('input', function () {
+ groupOverrideInput.on('input', async function () {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
data.entries[uid].groupOverride = value;
setOriginalDataValue(data, uid, 'extensions.group_override', data.entries[uid].groupOverride);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
groupOverrideInput.prop('checked', entry.groupOverride).trigger('input');
// group weight
const groupWeightInput = template.find('input[name="groupWeight"]');
groupWeightInput.data('uid', entry.uid);
- groupWeightInput.on('input', function () {
+ groupWeightInput.on('input', async function () {
const uid = $(this).data('uid');
let value = Number($(this).val());
const min = Number($(this).attr('min'));
@@ -2679,46 +2687,46 @@ function getWorldEntry(name, data, entry) {
data.entries[uid].groupWeight = !isNaN(value) ? Math.abs(value) : 1;
setOriginalDataValue(data, uid, 'extensions.group_weight', data.entries[uid].groupWeight);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
groupWeightInput.val(entry.groupWeight ?? DEFAULT_WEIGHT).trigger('input');
// sticky
const sticky = template.find('input[name="sticky"]');
sticky.data('uid', entry.uid);
- sticky.on('input', function () {
+ sticky.on('input', async function () {
const uid = $(this).data('uid');
const value = Number($(this).val());
data.entries[uid].sticky = !isNaN(value) ? value : null;
setOriginalDataValue(data, uid, 'extensions.sticky', data.entries[uid].sticky);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
sticky.val(entry.sticky > 0 ? entry.sticky : '').trigger('input');
// cooldown
const cooldown = template.find('input[name="cooldown"]');
cooldown.data('uid', entry.uid);
- cooldown.on('input', function () {
+ cooldown.on('input', async function () {
const uid = $(this).data('uid');
const value = Number($(this).val());
data.entries[uid].cooldown = !isNaN(value) ? value : null;
setOriginalDataValue(data, uid, 'extensions.cooldown', data.entries[uid].cooldown);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
cooldown.val(entry.cooldown > 0 ? entry.cooldown : '').trigger('input');
// delay
const delay = template.find('input[name="delay"]');
delay.data('uid', entry.uid);
- delay.on('input', function () {
+ delay.on('input', async function () {
const uid = $(this).data('uid');
const value = Number($(this).val());
data.entries[uid].delay = !isNaN(value) ? value : null;
setOriginalDataValue(data, uid, 'extensions.delay', data.entries[uid].delay);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
delay.val(entry.delay > 0 ? entry.delay : '').trigger('input');
@@ -2731,14 +2739,14 @@ function getWorldEntry(name, data, entry) {
const depthInput = template.find('input[name="depth"]');
depthInput.data('uid', entry.uid);
- depthInput.on('input', function () {
+ depthInput.on('input', async function () {
const uid = $(this).data('uid');
const value = Number($(this).val());
data.entries[uid].depth = !isNaN(value) ? value : 0;
updatePosOrdDisplay(uid);
setOriginalDataValue(data, uid, 'extensions.depth', data.entries[uid].depth);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
depthInput.val(entry.depth ?? DEFAULT_DEPTH).trigger('input');
depthInput.css('width', 'calc(3em + 15px)');
@@ -2750,7 +2758,7 @@ function getWorldEntry(name, data, entry) {
const probabilityInput = template.find('input[name="probability"]');
probabilityInput.data('uid', entry.uid);
- probabilityInput.on('input', function () {
+ probabilityInput.on('input', async function () {
const uid = $(this).data('uid');
const value = Number($(this).val());
@@ -2766,7 +2774,7 @@ function getWorldEntry(name, data, entry) {
}
setOriginalDataValue(data, uid, 'extensions.probability', data.entries[uid].probability);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
probabilityInput.val(entry.probability).trigger('input');
probabilityInput.css('width', 'calc(3em + 15px)');
@@ -2778,14 +2786,14 @@ function getWorldEntry(name, data, entry) {
const probabilityToggle = template.find('input[name="useProbability"]');
probabilityToggle.data('uid', entry.uid);
- probabilityToggle.on('input', function () {
+ probabilityToggle.on('input', async function () {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
data.entries[uid].useProbability = value;
const probabilityContainer = $(this)
.closest('.world_entry')
.find('.probabilityContainer');
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
value ? probabilityContainer.show() : probabilityContainer.hide();
if (value && data.entries[uid].probability === null) {
@@ -2814,7 +2822,7 @@ function getWorldEntry(name, data, entry) {
// Prevent closing the drawer on clicking the input
event.stopPropagation();
});
- positionInput.on('input', function () {
+ positionInput.on('input', async function () {
const uid = $(this).data('uid');
const value = Number($(this).val());
data.entries[uid].position = !isNaN(value) ? value : 0;
@@ -2836,7 +2844,7 @@ function getWorldEntry(name, data, entry) {
// Write the original value as extensions field
setOriginalDataValue(data, uid, 'extensions.position', data.entries[uid].position);
setOriginalDataValue(data, uid, 'extensions.role', data.entries[uid].role);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
const roleValue = entry.position === world_info_position.atDepth ? String(entry.role ?? extension_prompt_roles.SYSTEM) : '';
@@ -2852,12 +2860,12 @@ function getWorldEntry(name, data, entry) {
/*
const disableInput = template.find('input[name="disable"]');
disableInput.data("uid", entry.uid);
- disableInput.on("input", function () {
+ disableInput.on("input", async function () {
const uid = $(this).data("uid");
const value = $(this).prop("checked");
data.entries[uid].disable = value;
setOriginalDataValue(data, uid, "enabled", !data.entries[uid].disable);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
disableInput.prop("checked", entry.disable).trigger("input");
*/
@@ -2869,7 +2877,7 @@ function getWorldEntry(name, data, entry) {
// Prevent closing the drawer on clicking the input
event.stopPropagation();
});
- entryStateSelector.on('input', function () {
+ entryStateSelector.on('input', async function () {
const uid = entry.uid;
const value = $(this).val();
switch (value) {
@@ -2910,7 +2918,7 @@ function getWorldEntry(name, data, entry) {
template.addClass('disabledWIEntry');
break;
}
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
@@ -2930,52 +2938,50 @@ function getWorldEntry(name, data, entry) {
.prop('selected', true)
.trigger('input');
- saveWorldInfo(name, data);
-
// exclude recursion
const excludeRecursionInput = template.find('input[name="exclude_recursion"]');
excludeRecursionInput.data('uid', entry.uid);
- excludeRecursionInput.on('input', function () {
+ excludeRecursionInput.on('input', async function () {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
data.entries[uid].excludeRecursion = value;
setOriginalDataValue(data, uid, 'extensions.exclude_recursion', data.entries[uid].excludeRecursion);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
excludeRecursionInput.prop('checked', entry.excludeRecursion).trigger('input');
// prevent recursion
const preventRecursionInput = template.find('input[name="prevent_recursion"]');
preventRecursionInput.data('uid', entry.uid);
- preventRecursionInput.on('input', function () {
+ preventRecursionInput.on('input', async function () {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
data.entries[uid].preventRecursion = value;
setOriginalDataValue(data, uid, 'extensions.prevent_recursion', data.entries[uid].preventRecursion);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
preventRecursionInput.prop('checked', entry.preventRecursion).trigger('input');
// delay until recursion
const delayUntilRecursionInput = template.find('input[name="delay_until_recursion"]');
delayUntilRecursionInput.data('uid', entry.uid);
- delayUntilRecursionInput.on('input', function () {
+ delayUntilRecursionInput.on('input', async function () {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
data.entries[uid].delayUntilRecursion = value;
setOriginalDataValue(data, uid, 'extensions.delay_until_recursion', data.entries[uid].delayUntilRecursion);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
delayUntilRecursionInput.prop('checked', entry.delayUntilRecursion).trigger('input');
// duplicate button
const duplicateButton = template.find('.duplicate_entry_button');
duplicateButton.data('uid', entry.uid);
- duplicateButton.on('click', function () {
+ duplicateButton.on('click', async function () {
const uid = $(this).data('uid');
const entry = duplicateWorldInfoEntry(data, uid);
if (entry) {
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
updateEditor(entry.uid);
}
});
@@ -2983,18 +2989,18 @@ function getWorldEntry(name, data, entry) {
// delete button
const deleteButton = template.find('.delete_entry_button');
deleteButton.data('uid', entry.uid);
- deleteButton.on('click', function () {
+ deleteButton.on('click', async function () {
const uid = $(this).data('uid');
deleteWorldInfoEntry(data, uid);
deleteOriginalDataValue(data, uid);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
updateEditor(navigation_option.previous);
});
// scan depth
const scanDepthInput = template.find('input[name="scanDepth"]');
scanDepthInput.data('uid', entry.uid);
- scanDepthInput.on('input', function () {
+ scanDepthInput.on('input', async function () {
const uid = $(this).data('uid');
const isEmpty = $(this).val() === '';
const value = Number($(this).val());
@@ -3014,59 +3020,59 @@ function getWorldEntry(name, data, entry) {
data.entries[uid].scanDepth = !isEmpty && !isNaN(value) && value >= 0 && value <= MAX_SCAN_DEPTH ? Math.floor(value) : null;
setOriginalDataValue(data, uid, 'extensions.scan_depth', data.entries[uid].scanDepth);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
scanDepthInput.val(entry.scanDepth ?? null).trigger('input');
// case sensitive select
const caseSensitiveSelect = template.find('select[name="caseSensitive"]');
caseSensitiveSelect.data('uid', entry.uid);
- caseSensitiveSelect.on('input', function () {
+ caseSensitiveSelect.on('input', async function () {
const uid = $(this).data('uid');
const value = $(this).val();
data.entries[uid].caseSensitive = value === 'null' ? null : value === 'true';
setOriginalDataValue(data, uid, 'extensions.case_sensitive', data.entries[uid].caseSensitive);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
caseSensitiveSelect.val((entry.caseSensitive === null || entry.caseSensitive === undefined) ? 'null' : entry.caseSensitive ? 'true' : 'false').trigger('input');
// match whole words select
const matchWholeWordsSelect = template.find('select[name="matchWholeWords"]');
matchWholeWordsSelect.data('uid', entry.uid);
- matchWholeWordsSelect.on('input', function () {
+ matchWholeWordsSelect.on('input', async function () {
const uid = $(this).data('uid');
const value = $(this).val();
data.entries[uid].matchWholeWords = value === 'null' ? null : value === 'true';
setOriginalDataValue(data, uid, 'extensions.match_whole_words', data.entries[uid].matchWholeWords);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
matchWholeWordsSelect.val((entry.matchWholeWords === null || entry.matchWholeWords === undefined) ? 'null' : entry.matchWholeWords ? 'true' : 'false').trigger('input');
// use group scoring select
const useGroupScoringSelect = template.find('select[name="useGroupScoring"]');
useGroupScoringSelect.data('uid', entry.uid);
- useGroupScoringSelect.on('input', function () {
+ useGroupScoringSelect.on('input', async function () {
const uid = $(this).data('uid');
const value = $(this).val();
data.entries[uid].useGroupScoring = value === 'null' ? null : value === 'true';
setOriginalDataValue(data, uid, 'extensions.use_group_scoring', data.entries[uid].useGroupScoring);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
useGroupScoringSelect.val((entry.useGroupScoring === null || entry.useGroupScoring === undefined) ? 'null' : entry.useGroupScoring ? 'true' : 'false').trigger('input');
// automation id
const automationIdInput = template.find('input[name="automationId"]');
automationIdInput.data('uid', entry.uid);
- automationIdInput.on('input', function () {
+ automationIdInput.on('input', async function () {
const uid = $(this).data('uid');
const value = $(this).val();
data.entries[uid].automationId = value;
setOriginalDataValue(data, uid, 'extensions.automation_id', data.entries[uid].automationId);
- saveWorldInfo(name, data);
+ await saveWorldInfo(name, data);
});
automationIdInput.val(entry.automationId ?? '').trigger('input');
setTimeout(() => createEntryInputAutocomplete(automationIdInput, getAutomationIdCallback(data)), 1);
@@ -3301,6 +3307,9 @@ function createWorldInfoEntry(_name, data) {
}
async function _save(name, data) {
+ // Prevent double saving if both immediate and debounced save are called
+ cancelDebounce(saveWorldDebounced);
+
await fetch('/api/worldinfo/edit', {
method: 'POST',
headers: getRequestHeaders(),
@@ -3309,12 +3318,13 @@ async function _save(name, data) {
eventSource.emit(event_types.WORLDINFO_UPDATED, name, data);
}
-async function saveWorldInfo(name, data, immediately) {
+async function saveWorldInfo(name, data, immediately = false) {
if (!name || !data) {
return;
}
- worldInfoCache.delete(name);
+ // Update cache immediately, so any future call can pull from this
+ worldInfoCache.set(name, data);
if (immediately) {
return await _save(name, data);