Add generation type triggers to world info (#4286) * Add generation type triggers to world info * Simplify includes check * Refactor getEntryField validation and default value handling * Remove invalid attribute * Check for a valid trigger

c292f6416322ba39892c580f4901a28566b6b9ad

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
6 files changed, +132 -26Showing whitespace changes
public/css/world-info.css+7 -0
@@ -286,6 +286,13 @@ select.keyselect+span.select2-container .select2-selection--multiple {
286286 display: none;
287287}
288288
289+.world_entry label[for="__invisible"] {
290+ visibility: hidden;
291+ pointer-events: none;
292+ opacity: 0;
293+ width: 0;
294+}
295+
289296#WIMultiSelector .select2-container .select2-selection--multiple {
290297 max-height: 25vh;
291298 overflow-y: auto;
public/index.html+24 -0
@@ -6494,6 +6494,30 @@
64946494 </select>
64956495 </div>
64966496 </div>
6497+ <div class="flex4">
6498+ <div class="flex-container justifySpaceBetween">
6499+ <small>
6500+ <span data-i18n="Filter to Generation Triggers">
6501+ Filter to Generation Triggers
6502+ </span>
6503+ </small>
6504+ <!-- Not a real control. Used to make label heights even. -->
6505+ <label class="checkbox_label" for="__invisible">
6506+ <input type="checkbox" name="__invisible">
6507+ <span><small>&nbsp;</small></span>
6508+ </label>
6509+ </div>
6510+ <div class="range-block-range">
6511+ <select name="triggers" multiple>
6512+ <option data-i18n="Normal" value="normal">Normal</option>
6513+ <option data-i18n="Continue" value="continue">Continue</option>
6514+ <option data-i18n="Impersonate" value="impersonate">Impersonate</option>
6515+ <option data-i18n="Swipe" value="swipe">Swipe</option>
6516+ <option data-i18n="Regenerate" value="regenerate">Regenerate</option>
6517+ <option data-i18n="Quiet" value="quiet">Quiet</option>
6518+ </select>
6519+ </div>
6520+ </div>
64976521 </div>
64986522 <div name="WIEntryBottomControls" class="flex-container flex1 justifySpaceBetween world_entry_form_horizontal">
64996523 <div class="flex-container flexFlowColumn flexNoGap wi-enter-footer-text">
public/script.js+3 -1
@@ -176,7 +176,7 @@ import {
176176 renderPaginationDropdown,
177177 paginationDropdownChangeHandler,
178178} from './scripts/utils.js';
179179import { debounce_timeout, GENERATION_TYPE_TRIGGERS, IGNORE_SYMBOL } from './scripts/constants.js';
180180
181181import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js';
182182import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js';
@@ -3721,6 +3721,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
37213721 // Make quiet prompt available for WIAN
37223722 setExtensionPrompt('QUIET_PROMPT', quiet_prompt || '', extension_prompt_types.IN_PROMPT, 0, true);
37233723 const chatForWI = coreChat.map(x => world_info_include_names ? `${x.name}: ${x.mes}` : x.mes).reverse();
3724+ /** @type {import('./scripts/world-info.js').WIGlobalScanData} */
37243725 const globalScanData = {
37253726 personaDescription: persona,
37263727 characterDescription: description,
@@ -3728,6 +3729,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
37283729 characterDepthPrompt: charDepthPrompt,
37293730 scenario: scenario,
37303731 creatorNotes: creatorNotes,
3732+ trigger: GENERATION_TYPE_TRIGGERS.includes(type) ? type : 'normal',
37313733 };
37323734 const { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoExamples, worldInfoDepth } = await getWorldInfoPrompt(chatForWI, this_max_context, dryRun, globalScanData);
37333735 setExtensionPrompt('QUIET_PROMPT', '', extension_prompt_types.IN_PROMPT, 0, true);
public/scripts/constants.js+12 -0
@@ -28,3 +28,15 @@ export const IGNORE_SYMBOL = Symbol.for('ignore');
2828 * https://ai.google.dev/gemini-api/docs/video-understanding#supported-formats
2929 */
3030export const VIDEO_EXTENSIONS = ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', '3gp', 'mkv', 'mpg'];
31+
32+/**
33+ * Known generation triggers that can be passed to Generate function.
34+ */
35+export const GENERATION_TYPE_TRIGGERS = [
36+ 'normal',
37+ 'continue',
38+ 'impersonate',
39+ 'swipe',
40+ 'regenerate',
41+ 'quiet',
42+];
public/scripts/world-info.js+85 -25
@@ -9,7 +9,7 @@ import { FILTER_TYPES, FilterHelper } from './filters.js';
99import { getTokenCountAsync } from './tokenizers.js';
1010import { power_user } from './power-user.js';
1111import { getTagKeyForEntity } from './tags.js';
1212import { debounce_timeout, GENERATION_TYPE_TRIGGERS } from './constants.js';
1313import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
1414import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
1515import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -107,6 +107,7 @@ const KNOWN_DECORATORS = ['@@activate', '@@dont_activate'];
107107 * @property {string} characterDepthPrompt Character depth prompt (sometimes referred to as character notes)
108108 * @property {string} scenario Character defined scenario
109109 * @property {string} creatorNotes Character creator notes
110+ * @property {string} trigger The type that triggered the scan, e.g. 'normal', 'continue', etc.
110111 */
111112
112113/**
@@ -145,6 +146,36 @@ const KNOWN_DECORATORS = ['@@activate', '@@dont_activate'];
145146 * @typedef TimedEffectType Type of timed effect
146147 * @type {'sticky'|'cooldown'|'delay'}
147148 */
149+
150+/**
151+ * @typedef {object} WIPromptResult
152+ * @property {string} worldInfoString - Complete world info string
153+ * @property {string} worldInfoBefore - World info that goes before the prompt
154+ * @property {string} worldInfoAfter - World info that goes after the prompt
155+ * @property {Array} worldInfoExamples - Array of example entries
156+ * @property {Array} worldInfoDepth - Array of depth entries
157+ * @property {Array} anBefore - Array of entries before Author's Note
158+ * @property {Array} anAfter - Array of entries after Author's Note
159+ */
160+
161+/**
162+ * @typedef {object} WIActivated
163+ * @property {string} worldInfoBefore The world info before the chat.
164+ * @property {string} worldInfoAfter The world info after the chat.
165+ * @property {any[]} EMEntries The entries for examples.
166+ * @property {any[]} WIDepthEntries The depth entries.
167+ * @property {any[]} ANBeforeEntries The entries before Author's Note.
168+ * @property {any[]} ANAfterEntries The entries after Author's Note.
169+ * @property {Set<any>} allActivatedEntries All entries.
170+ */
171+
172+/**
173+ * @typedef {object} WIEntryFieldDefinition
174+ * @property {any} default - Default value for the field
175+ * @property {string} type - Type of the field, can be 'string', 'number', 'boolean', 'array', 'enum'
176+ * @property {boolean} [excludeFromTemplate=false] - Whether to exclude this field from the template
177+ * @property {(value: any) => boolean} [arrayFilter] - Optional filter function for array fields to filter out unwanted values
178+ */
148179// End typedef area
149180
150181/**
@@ -801,14 +832,6 @@ export const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOn
801832 * @param {number} maxContext - The maximum context size of the generation.
802833 * @param {boolean} isDryRun - If true, the function will not emit any events.
803834 * @param {WIGlobalScanData} globalScanData Chat independent context to be scanned
804- * @typedef {object} WIPromptResult
805- * @property {string} worldInfoString - Complete world info string
806- * @property {string} worldInfoBefore - World info that goes before the prompt
807- * @property {string} worldInfoAfter - World info that goes after the prompt
808- * @property {Array} worldInfoExamples - Array of example entries
809- * @property {Array} worldInfoDepth - Array of depth entries
810- * @property {Array} anBefore - Array of entries before Author's Note
811- * @property {Array} anAfter - Array of entries after Author's Note
812835 * @returns {Promise<WIPromptResult>} The world info string and depth.
813836 */
814837export async function getWorldInfoPrompt(chat, maxContext, isDryRun, globalScanData) {
@@ -1139,7 +1162,7 @@ function registerWorldInfoSlashCommands() {
11391162 return '';
11401163 }
11411164
1142- if (newWorldInfoEntryTemplate[field] === undefined) {
1165+ if (!Object.hasOwn(newWorldInfoEntryDefinition, field)) {
11431166 toastr.warning('Valid field name is required');
11441167 return '';
11451168 }
@@ -1167,7 +1190,7 @@ function registerWorldInfoSlashCommands() {
11671190 }
11681191 break;
11691192 default:
11701193 fieldValue = entry[field] ?? newWorldInfoEntryDefinition[field]?.default;
11711194 }
11721195
11731196 if (fieldValue === undefined) {
@@ -1253,11 +1276,19 @@ function registerWorldInfoSlashCommands() {
12531276 return '';
12541277 }
12551278
1256- if (newWorldInfoEntryTemplate[field] === undefined) {
1279+ if (!Object.hasOwn(newWorldInfoEntryDefinition, field)) {
12571280 toastr.warning('Valid field name is required');
12581281 return '';
12591282 }
12601283
1284+ // Init a default value for the field if it does not exist
1285+ if (!Object.hasOwn(entry, field)) {
1286+ entry[field] = newWorldInfoEntryDefinition[field].default;
1287+ }
1288+
1289+ // Use an array filter if it exists for the field
1290+ const arrayFilter = newWorldInfoEntryDefinition[field]?.arrayFilter || (() => true);
1291+
12611292 // handle special cases, otherwise execute default logic
12621293 let tagNames;
12631294 let charNames;
@@ -1285,7 +1316,7 @@ function registerWorldInfoSlashCommands() {
12851316 break;
12861317 default:
12871318 if (Array.isArray(entry[field])) {
12881319 entry[field] = parseStringArray(value).filter(arrayFilter);
12891320 } else if (typeof entry[field] === 'boolean') {
12901321 entry[field] = isTrueBoolean(value);
12911322 } else if (typeof entry[field] === 'number') {
@@ -2438,6 +2469,7 @@ export const originalWIDataKeyMap = {
24382469 'sticky': 'extensions.sticky',
24392470 'cooldown': 'extensions.cooldown',
24402471 'delay': 'extensions.delay',
2472+ 'triggers': 'extensions.triggers',
24412473};
24422474
24432475/** Checks the state of the current search, and adds/removes the search sorting option accordingly */
@@ -3496,6 +3528,29 @@ export async function getWorldEntry(name, data, entry) {
34963528 automationIdInput.val(entry.automationId ?? '').trigger('input', { noSave: true });
34973529 setTimeout(() => createEntryInputAutocomplete(automationIdInput, getAutomationIdCallback(data)), 1);
34983530
3531+ // Generation Type Triggers
3532+ const generationTypeTriggers = editTemplate.find('select[name="triggers"]');
3533+ generationTypeTriggers.data('uid', entry.uid);
3534+ generationTypeTriggers.on('input', async function (_, { noSave = false } = {}) {
3535+ const uid = $(this).data('uid');
3536+ const value = $(this).val();
3537+ data.entries[uid].triggers = Array.isArray(value) ? value : [];
3538+ setWIOriginalDataValue(data, uid, 'extensions.triggers', data.entries[uid].triggers);
3539+ !noSave && await saveWorldInfo(name, data);
3540+ });
3541+ if (!isMobile()) {
3542+ generationTypeTriggers.select2({
3543+ placeholder: t`All types (default)`,
3544+ width: '100%',
3545+ closeOnSelect: false,
3546+ allowClear: true,
3547+ });
3548+ }
3549+ generationTypeTriggers
3550+ .val(Array.isArray(entry.triggers) ? entry.triggers : [])
3551+ .trigger('input', { noSave: true })
3552+ .trigger('change');
3553+
34993554 countTokensDebounced(counter, contentInput.val());
35003555
35013556 editTemplate.find('.inline-drawer-content').css('display', 'none');
@@ -3652,7 +3707,7 @@ export async function deleteWorldInfoEntry(data, uid, { silent = false } = {}) {
36523707 *
36533708 * Use `newEntryTemplate` if you just need the template that contains default values
36543709 *
36553710 * @type {{[key: string]: { default: any, type: string, excludeFromTemplate?: boolean }WIEntryFieldDefinition}}
36563711 */
36573712export const newWorldInfoEntryDefinition = {
36583713 key: { default: [], type: 'array' },
@@ -3694,6 +3749,7 @@ export const newWorldInfoEntryDefinition = {
36943749 characterFilterNames: { default: [], type: 'array', excludeFromTemplate: true },
36953750 characterFilterTags: { default: [], type: 'array', excludeFromTemplate: true },
36963751 characterFilterExclude: { default: false, type: 'boolean', excludeFromTemplate: true },
3752+ triggers: { default: [], type: 'array', arrayFilter: (value) => GENERATION_TYPE_TRIGGERS.includes(value) },
36973753};
36983754
36993755export const newWorldInfoEntryTemplate = Object.fromEntries(
@@ -4143,23 +4199,14 @@ function parseDecorators(content) {
41434199 * @param {number} maxContext The maximum context size of the generation.
41444200 * @param {boolean} isDryRun Whether to perform a dry run.
41454201 * @param {WIGlobalScanData} globalScanData Chat independent context to be scanned
4146- * @typedef {object} WIActivated
4147- * @property {string} worldInfoBefore The world info before the chat.
4148- * @property {string} worldInfoAfter The world info after the chat.
4149- * @property {any[]} EMEntries The entries for examples.
4150- * @property {any[]} WIDepthEntries The depth entries.
4151- * @property {any[]} ANBeforeEntries The entries before Author's Note.
4152- * @property {any[]} ANAfterEntries The entries after Author's Note.
4153- * @property {Set<any>} allActivatedEntries All entries.
41544202 * @returns {Promise<WIActivated>} The world info activated.
41554203 */
4156-
41574204//MARK: checkWorldInfo
41584205export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData) {
41594206 const context = getContext();
41604207 const buffer = new WorldInfoBuffer(chat, globalScanData);
41614208
41624209 console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages, trigger = ${globalScanData.trigger})${isDryRun ? ' (DRY RUN)' : ''} ---`);
41634210
41644211 // Combine the chat
41654212
@@ -4231,7 +4278,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData)
42314278 // Loop and find all entries that can activate here
42324279 let activatedNow = new Set();
42334280
42344281 for (letconst entry of sortedEntries) {
42354282 // Logging preparation
42364283 let headerLogged = false;
42374284 function log(...args) {
@@ -4252,6 +4299,15 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData)
42524299 continue;
42534300 }
42544301
4302+ // Check for generation type trigger filter
4303+ if (Array.isArray(entry.triggers) && entry.triggers.length > 0) {
4304+ const isTriggered = entry.triggers.includes(globalScanData.trigger);
4305+ if (!isTriggered) {
4306+ log(`skipped by generation type trigger filter (${globalScanData.trigger} ∉ ${entry.triggers})`);
4307+ continue;
4308+ }
4309+ }
4310+
42554311 // Check if this entry applies to the character or if it's excluded
42564312 if (entry.characterFilter && entry.characterFilter?.names?.length > 0) {
42574313 const nameIncluded = entry.characterFilter.names.includes(getCharaFilename());
@@ -4877,6 +4933,7 @@ function convertAgnaiMemoryBook(inputObj) {
48774933 sticky: null,
48784934 cooldown: null,
48794935 delay: null,
4936+ triggers: [],
48804937 };
48814938 });
48824939
@@ -4919,6 +4976,7 @@ function convertRisuLorebook(inputObj) {
49194976 sticky: null,
49204977 cooldown: null,
49214978 delay: null,
4979+ triggers: [],
49224980 };
49234981 });
49244982
@@ -4966,6 +5024,7 @@ function convertNovelLorebook(inputObj) {
49665024 sticky: null,
49675025 cooldown: null,
49685026 delay: null,
5027+ triggers: [],
49695028 };
49705029 });
49715030
@@ -5022,6 +5081,7 @@ export function convertCharacterBook(characterBook) {
50225081 matchScenario: entry.extensions?.match_scenario ?? false,
50235082 matchCreatorNotes: entry.extensions?.match_creator_notes ?? false,
50245083 extensions: entry.extensions ?? {},
5084+ triggers: entry.extensions?.triggers || [],
50255085 };
50265086 });
50275087
src/endpoints/characters.js+1 -0
@@ -707,6 +707,7 @@ function convertWorldInfoToCharacterBook(name, entries) {
707707 match_character_depth_prompt: entry.matchCharacterDepthPrompt ?? false,
708708 match_scenario: entry.matchScenario ?? false,
709709 match_creator_notes: entry.matchCreatorNotes ?? false,
710+ triggers: entry.triggers ?? [],
710711 },
711712 };
712713