diff --git a/public/index.html b/public/index.html index f7f726422..b5a2eda86 100644 --- a/public/index.html +++ b/public/index.html @@ -5397,26 +5397,6 @@
-
-
- - Filter to Character(s) - - -
-
- -
-
@@ -5449,6 +5429,54 @@
+
+
+ + + Sticky + + + +
+
+ +
+
+
+
+ + + Cooldown + + + +
+
+ +
+
+
+
+
+
+ + Filter to Character(s) + + +
+
+ +
+
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'getentryfield', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'getentryfield', aliases: ['getlorefield', 'getwifield'], callback: getEntryFieldCallback, returns: 'field value', @@ -846,7 +1287,8 @@ function registerWorldInfoSlashCommands() {
`, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'createentry', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'createentry', callback: createEntryCallback, aliases: ['createlore', 'createwi'], returns: 'UID of the new record', @@ -881,7 +1323,8 @@ function registerWorldInfoSlashCommands() { `, })); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'setentryfield', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'setentryfield', callback: setEntryFieldCallback, aliases: ['setlorefield', 'setwifield'], namedArgumentList: [ @@ -926,7 +1369,110 @@ function registerWorldInfoSlashCommands() { `, })); - + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'wi-set-timed-effect', + callback: setTimedEffectCallback, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'file', + description: 'book name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.worlds, + }), + SlashCommandNamedArgument.fromProps({ + name: 'uid', + description: 'record UID', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.wiUids, + }), + SlashCommandNamedArgument.fromProps({ + name: 'effect', + description: 'effect name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.timedEffects, + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'new state of the effect', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + acceptsMultiple: false, + enumList: commonEnumProviders.boolean('onOffToggle')(), + }), + ], + helpString: ` +
+ Set a timed effect for the record with the UID from the specified book. The duration must be set in the entry itself. + Will only be applied for the current chat. Enabling an effect that was already active refreshes the duration. + If the last chat message is swiped or deleted, the effect will be removed. +
+
+ Example: + +
+ `, + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'wi-get-timed-effect', + callback: getTimedEffectCallback, + helpString: ` +
+ Get the current state of the timed effect for the record with the UID from the specified book. +
+
+ Example: + +
+ `, + returns: 'state of the effect', + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'file', + description: 'book name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: commonEnumProviders.worlds, + }), + SlashCommandNamedArgument.fromProps({ + name: 'effect', + description: 'effect name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.timedEffects, + }), + SlashCommandNamedArgument.fromProps({ + name: 'format', + description: 'output format', + isRequired: false, + typeList: [ARGUMENT_TYPE.STRING], + defaultValue: ARGUMENT_TYPE.BOOLEAN, + enumList: [ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.NUMBER], + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'record UID', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.wiUids, + }), + ], + })); } // World Info Editor @@ -945,8 +1491,8 @@ async function loadWorldInfoData(name) { return; } - if (worldInfoCache[name]) { - return worldInfoCache[name]; + if (worldInfoCache.has(name)) { + return worldInfoCache.get(name); } const response = await fetch('/api/worldinfo/get', { @@ -958,7 +1504,7 @@ async function loadWorldInfoData(name) { if (response.ok) { const data = await response.json(); - worldInfoCache[name] = data; + worldInfoCache.set(name, data); return data; } @@ -1390,6 +1936,8 @@ const originalDataKeyMap = { 'vectorized': 'extensions.vectorized', 'groupOverride': 'extensions.group_override', 'groupWeight': 'extensions.group_weight', + 'sticky': 'extensions.sticky', + 'cooldown': 'extensions.cooldown', }; /** Checks the state of the current search, and adds/removes the search sorting option accordingly */ @@ -1518,7 +2066,8 @@ function customTokenizer(input, _selection, callback) { // Now remove the token from the current input, and the comma too current = current.slice(i + 1); - insideRegex = false, regexClosed = false; + insideRegex = false; + regexClosed = false; i = 0; } } @@ -2011,6 +2560,32 @@ function getWorldEntry(name, data, entry) { }); 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 () { + 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); + }); + 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 () { + 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); + }); + cooldown.val(entry.cooldown > 0 ? entry.cooldown : '').trigger('input'); + // probability if (entry.probability === undefined) { entry.probability = null; @@ -2559,12 +3134,20 @@ const newEntryDefinition = { useGroupScoring: { default: null, type: 'boolean?' }, automationId: { default: '', type: 'string' }, role: { default: 0, type: 'enum' }, + sticky: { default: null, type: 'number?' }, + cooldown: { default: null, type: 'number?' }, }; const newEntryTemplate = Object.fromEntries( Object.entries(newEntryDefinition).map(([key, value]) => [key, value.default]), ); +/** + * Creates a new world info entry from template. + * @param {string} _name Name of the WI (unused) + * @param {any} data WI data + * @returns {object | undefined} New entry object or undefined if failed + */ function createWorldInfoEntry(_name, data) { const newUid = getFreeWorldEntryUid(data); @@ -2593,7 +3176,7 @@ async function saveWorldInfo(name, data, immediately) { return; } - delete worldInfoCache[name]; + worldInfoCache.delete(name); if (immediately) { return await _save(name, data); @@ -2863,10 +3446,11 @@ export async function getSortedEntries() { * Performs a scan on the chat and returns the world info activated. * @param {string[]} chat The chat messages to scan. * @param {number} maxContext The maximum context size of the generation. + * @param {boolean} isDryRun Whether to perform a dry run. * @typedef {{ worldInfoBefore: string, worldInfoAfter: string, EMEntries: any[], WIDepthEntries: any[], allActivatedEntries: Set }} WIActivated * @returns {Promise} The world info activated. */ -async function checkWorldInfo(chat, maxContext) { +async function checkWorldInfo(chat, maxContext, isDryRun) { const context = getContext(); const buffer = new WorldInfoBuffer(chat); @@ -2899,6 +3483,9 @@ async function checkWorldInfo(chat, maxContext) { console.debug(`Context size: ${maxContext}; WI budget: ${budget} (max% = ${world_info_budget}%, cap = ${world_info_budget_cap})`); const sortedEntries = await getSortedEntries(); + const timedEffects = new WorldInfoTimedEffects(chat, sortedEntries); + + !isDryRun && timedEffects.checkTimedEffects(); if (sortedEntries.length === 0) { return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() }; @@ -2941,6 +3528,14 @@ async function checkWorldInfo(chat, maxContext) { } } + const isSticky = timedEffects.isEffectActive('sticky', entry); + const isCooldown = timedEffects.isEffectActive('cooldown', entry); + + if (isCooldown && !isSticky) { + console.debug(`WI entry ${entry.uid} suppressed by cooldown`); + continue; + } + if (failedProbabilityChecks.has(entry)) { continue; } @@ -2949,7 +3544,7 @@ async function checkWorldInfo(chat, maxContext) { continue; } - if (entry.constant || buffer.isExternallyActivated(entry)) { + if (entry.constant || buffer.isExternallyActivated(entry) || isSticky) { activatedNow.add(entry); continue; } @@ -3037,9 +3632,12 @@ async function checkWorldInfo(chat, maxContext) { const rollValue = Math.random() * 100; if (entry.useProbability && rollValue > entry.probability) { - console.debug(`WI entry ${entry.uid} ${entry.key} failed probability check, skipping`); - failedProbabilityChecks.add(entry); - continue; + const isSticky = timedEffects.isEffectActive('sticky', entry); + if (!isSticky) { + console.debug(`WI entry ${entry.uid} ${entry.key} failed probability check, skipping`); + failedProbabilityChecks.add(entry); + continue; + } } else { console.debug(`uid:${entry.uid} passed probability check, inserting to prompt`); } // Substitute macros inline, for both this checking and also future processing @@ -3167,7 +3765,9 @@ async function checkWorldInfo(chat, maxContext) { context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan, chat_metadata[metadata_keys.role]); } - buffer.cleanExternalActivations(); + !isDryRun && timedEffects.setTimedEffects(Array.from(allActivatedEntries)); + buffer.resetExternalEffects(); + timedEffects.cleanUp(); return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, allActivatedEntries }; } @@ -3328,6 +3928,8 @@ function convertAgnaiMemoryBook(inputObj) { useGroupScoring: null, automationId: '', role: extension_prompt_roles.SYSTEM, + sticky: null, + cooldown: null, }; }); @@ -3367,6 +3969,8 @@ function convertRisuLorebook(inputObj) { useGroupScoring: null, automationId: '', role: extension_prompt_roles.SYSTEM, + sticky: null, + cooldown: null, }; }); @@ -3411,6 +4015,8 @@ function convertNovelLorebook(inputObj) { useGroupScoring: null, automationId: '', role: extension_prompt_roles.SYSTEM, + sticky: null, + cooldown: null, }; }); @@ -3457,6 +4063,8 @@ function convertCharacterBook(characterBook) { automationId: entry.extensions?.automation_id ?? '', role: entry.extensions?.role ?? extension_prompt_roles.SYSTEM, vectorized: entry.extensions?.vectorized ?? false, + sticky: entry.extensions?.sticky ?? null, + cooldown: entry.extensions?.cooldown ?? null, }; }); diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js index c5c15b2e3..17dcaa6b8 100644 --- a/src/endpoints/characters.js +++ b/src/endpoints/characters.js @@ -483,6 +483,8 @@ function convertWorldInfoToCharacterBook(name, entries) { automation_id: entry.automationId ?? '', role: entry.role ?? 0, vectorized: entry.vectorized ?? false, + sticky: entry.sticky ?? null, + cooldown: entry.cooldown ?? null, }, };