Preset Regex: add preset type for script regexes (#4642) * feat: preset regex * refactor: unify `saveScriptsByType` * fix: default branch for `saveScriptsByType` * refactor: resolve some reviews * fix: lint * refactor: getPresetName -> getCurrentPresetName * refactor: unify regex allow operation * feat: notify whether to reload current chat when preset is changed * refacotr: set timeout for reload current chat notification * fix: _ -> lodash * i18n * Refactor API ID access * refactor: replace UNKNOWN type with a separate constant * refactor: consolidate CSS rules for scoped and preset script blocks * Extend toast timeout * Fix bug with moving, refactor * Refactor bulk toggle operations * Use for..of for preset application * refactor: enhance parameter documentation for scriptType in regex functions * fix: await loadRegexScripts in event handlers * fix: correct script type in saveScriptsByType for preset handling * fix: add null checks for preset manager and API ID in regex functions * feat: implement expandable regex script options in UI * Always show edit/delete buttons * Add title attribute to regex script name * fix: correct class name for regex script display * Remove extra paddings from script blocks * fix: ensure preset scripts are only disallowed if currently allowed during rename * fix: reload on bulk script toggle * feat: add PRESET_RENAMED_BEFORE event type and emit it during preset renaming * fix: always preserve preset extensions on rename * Handle horde in getPresetManager --------- Co-authored-by: ZoinkCN <zoinkcn@outlook.com> Co-authored-by: StageDog <aksanajw845@gmail.com>

df7f0469129ebaaddd307e0c06cb29fbbb2e1097

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

Signed
11 files changed, +581 -143Ignore whitespace
public/locales/zh-cn.json+14 -2
@@ -1646,8 +1646,9 @@
16461646 "macro for manual injection)": "宏用于手动注入)",
16471647 "Color": "颜色",
16481648 "Only apply color as accent": "仅应用颜色作为强调",
16491649 "ext_regex_new_global_script_desc": "新增「全局」正规表达式正则表达式",
16501650 "ext_regex_new_scoped_script_desc": "新增「局部」正规表达式正则表达式",
1651+ "ext_regex_new_preset_script_desc": "新增「预设」正则表达式",
16511652 "ext_regex_debugger_active_rules": "激活的规则",
16521653 "ext_regex_debugger_save_order": "保存此顺序",
16531654 "ext_regex_debugger_testing_area": "测试区域",
@@ -1668,6 +1669,7 @@
16681669 "ext_regex_preset_delete": "删除当前预设",
16691670 "ext_regex_new_global_script": "新建全局正则",
16701671 "ext_regex_new_scoped_script": "新建局部正则",
1672+ "ext_regex_new_preset_script": "新建预设正则",
16711673 "ext_regex_import_script": "导入正则",
16721674 "ext_regex_bulk_edit": "批量编辑",
16731675 "ext_regex_debugger_desc": "高级正则调试工具",
@@ -1678,8 +1680,12 @@
16781680 "No scripts found": "没有找到脚本",
16791681 "ext_regex_scoped_scripts": "局部正则脚本",
16801682 "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中",
1683+ "ext_regex_preset_scripts": "预设正则脚本",
1684+ "ext_regex_preset_scripts_desc": "只影响当前预设,保存在预设中",
16811685 "ext_regex_disallow_scoped": "不允许使用局部正则",
16821686 "ext_regex_allow_scoped": "允许使用局部正则",
1687+ "ext_regex_disallow_preset": "不允许使用预设正则",
1688+ "ext_regex_allow_preset": "允许使用预设正则",
16831689 "Regex Editor": "正则表达式编辑器",
16841690 "Test Mode": "测试模式",
16851691 "ext_regex_desc": "“正则”是一个使用“正则表达式”来查找/替换字符串的工具。如果您想了解更多信息,请点击标题旁边的“?”。",
@@ -1730,8 +1736,14 @@
17301736 "ext_regex_edit_script": "编辑脚本",
17311737 "ext_regex_move_to_global": "移至全局",
17321738 "ext_regex_move_to_scoped": "移至局部",
1739+ "ext_regex_move_to_preset": "移至预设",
17331740 "ext_regex_export_script": "导出脚本",
17341741 "ext_regex_delete_script": "删除脚本",
1742+ "This preset has embedded regex script(s).": "此预设包含内置正则脚本。",
1743+ "Preset '${0}' contains enabled regex scripts": "预设 '${0}' 包含被启用的正则脚本",
1744+ "Reload the chat for regex to take effect": "重新加载聊天以使正则生效",
1745+ "Click here to reload immediately": "点击此处立即重新加载",
1746+ "If you want to do it later, select \"Regex\" from the extensions menu.": "您可稍后从扩展菜单中的(Regex)启用。",
17351747 "Trigger Stable Diffusion": "触发Stable Diffusion",
17361748 "Abort current image generation task": "中止当前图像生成",
17371749 "Stop Image Generation": "停止图像生成",
public/scripts/events.js+1 -0
@@ -88,6 +88,7 @@ export const event_types = {
8888 PRESET_CHANGED: 'preset_changed',
8989 PRESET_DELETED: 'preset_deleted',
9090 PRESET_RENAMED: 'preset_renamed',
91+ PRESET_RENAMED_BEFORE: 'preset_renamed_before',
9192 MAIN_API_CHANGED: 'main_api_changed',
9293 WORLDINFO_ENTRIES_LOADED: 'worldinfo_entries_loaded',
9394};
public/scripts/extensions.js+3 -0
@@ -190,7 +190,10 @@ export const extension_settings = {
190190 regex: [],
191191 /** @type {import('./extensions/regex/index.js').RegexPreset[]} */
192192 regex_presets: [],
193+ /** @type {string[]} */
193194 character_allowed_regex: [],
195+ /** @type {Record<string, string[]>} */
196+ preset_allowed_regex: {},
194197 tts: {},
195198 sd: {
196199 prompts: {},
public/scripts/extensions/regex/dropdown.html+25 -2
@@ -16,6 +16,10 @@
1616 <i class="fa-solid fa-address-card"></i>
1717 <small data-i18n="ext_regex_new_scoped_script">+ Scoped</small>
1818 </div>
19+ <div id="open_preset_editor" class="menu_button menu_button_icon" data-i18n="[title]ext_regex_new_preset_script_desc" title="New preset regex script">
20+ <i class="fa-solid fa-sliders"></i>
21+ <small data-i18n="ext_regex_new_preset_script">+ Preset</small>
22+ </div>
1923 <div id="import_regex" class="menu_button menu_button_icon">
2024 <i class="fa-solid fa-file-import"></i>
2125 <small data-i18n="ext_regex_import_script">Import</small>
@@ -52,6 +56,10 @@
5256 <i class="fa-solid fa-address-card"></i>
5357 <small data-i18n="ext_regex_move_to_scoped">Move to scoped scripts</small>
5458 </div>
59+ <div id="bulk_regex_move_to_preset" class="menu_button menu_button_icon" hidden>
60+ <i class="fa-solid fa-sliders"></i>
61+ <small data-i18n="ext_regex_move_to_preset">Move to preset scripts</small>
62+ </div>
5563 <div id="bulk_export_regex" class="menu_button menu_button_icon">
5664 <i class="fa-solid fa-file-export"></i>
5765 <small data-i18n="Export">Export</small>
@@ -78,7 +86,7 @@
7886 </div>
7987 </div>
8088 <hr />
8189 <div id="global_scripts_block" class="padding5">
8290 <div>
8391 <strong data-i18n="ext_regex_global_scripts">Global Scripts</strong>
8492 </div>
@@ -88,7 +96,7 @@
8896 <div id="saved_regex_scripts" no-scripts-text="No scripts found" data-i18n="[no-scripts-text]No scripts found" class="flex-container regex-script-container flexFlowColumn"></div>
8997 </div>
9098 <hr />
9199 <div id="scoped_scripts_block" class="padding5">
92100 <div class="flex-container alignItemsBaseline">
93101 <strong class="flex1" data-i18n="ext_regex_scoped_scripts">Scoped Scripts</strong>
94102 <label id="toggle_scoped_regex" class="checkbox flex-container" for="regex_scoped_toggle">
@@ -102,6 +110,21 @@
102110 </small>
103111 <div id="saved_scoped_scripts" no-scripts-text="No scripts found" data-i18n="[no-scripts-text]No scripts found" class="flex-container regex-script-container flexFlowColumn"></div>
104112 </div>
113+ <hr />
114+ <div id="preset_scripts_block">
115+ <div class="flex-container alignItemsBaseline">
116+ <strong class="flex1" data-i18n="ext_regex_preset_scripts">Preset Scripts</strong>
117+ <label id="toggle_preset_regex" class="checkbox flex-container" for="regex_preset_toggle">
118+ <input type="checkbox" id="regex_preset_toggle" class="enable_scoped" />
119+ <span class="regex-toggle-on fa-solid fa-toggle-on fa-lg" data-i18n="[title]ext_regex_disallow_preset" title="Disallow using preset regex"></span>
120+ <span class="regex-toggle-off fa-solid fa-toggle-off fa-lg" data-i18n="[title]ext_regex_allow_preset" title="Allow using preset regex"></span>
121+ </label>
122+ </div>
123+ <small data-i18n="ext_regex_preset_scripts_desc">
124+ Only available for this preset. Saved to the preset data.
125+ </small>
126+ <div id="saved_preset_scripts" class="flex-container regex-script-container flexFlowColumn"></div>
127+ </div>
105128 </div>
106129 </div>
107130</div>
public/scripts/extensions/regex/engine.js+161 -4
@@ -1,18 +1,25 @@
11import { characters, saveSettingsDebounced, substituteParams, substituteParamsExtended, this_chid } from '../../../script.js';
22import { extension_settings, writeExtensionField } from '../../extensions.js';
3+import { getPresetManager } from '../../preset-manager.js';
34import { regexFromString } from '../../utils.js';
5+import { lodash } from '../../../lib.js';
46
57/**
68 * @enum {number} Regex scripts types
79 * @readonly
810 */
911export const SCRIPT_TYPES = {
10- UNKNOWN: -1,
1112 GLOBAL: 0,
1213 SCOPED: 1,
14+ PRESET: 2,
1315};
1416
1517/**
18+ * Special type for unknown/invalid script types.
19+ */
20+export const SCRIPT_TYPE_UNKNOWN = -1;
21+
22+/**
1623 * @typedef {import('../../char-data.js').RegexScriptData} RegexScript
1724 */
1825
@@ -44,7 +51,7 @@ export function getRegexScripts(options = DEFAULT_GET_REGEX_SCRIPTS_OPTIONS) {
4451 */
4552export function getScriptsByType(scriptType, { allowedOnly } = DEFAULT_GET_REGEX_SCRIPTS_OPTIONS) {
4653 switch (scriptType) {
4754 case SCRIPT_TYPES.UNKNOWNSCRIPT_TYPE_UNKNOWN:
4855 return [];
4956 case SCRIPT_TYPES.GLOBAL:
5057 return extension_settings.regex ?? [];
@@ -55,6 +62,14 @@ export function getScriptsByType(scriptType, { allowedOnly } = DEFAULT_GET_REGEX
5562 const scopedScripts = characters[this_chid]?.data?.extensions?.regex_scripts;
5663 return Array.isArray(scopedScripts) ? scopedScripts : [];
5764 }
65+ case SCRIPT_TYPES.PRESET: {
66+ if (allowedOnly && !extension_settings?.preset_allowed_regex?.[getCurrentPresetAPI()]?.includes(getCurrentPresetName())) {
67+ return [];
68+ }
69+ const presetManager = getPresetManager();
70+ const presetScripts = presetManager?.readPresetExtensionField({ path: 'regex_scripts' });
71+ return Array.isArray(presetScripts) ? presetScripts : [];
72+ }
5873 default:
5974 console.warn(`getScriptsByType: Invalid script type ${scriptType}`);
6075 return [];
@@ -62,6 +77,148 @@ export function getScriptsByType(scriptType, { allowedOnly } = DEFAULT_GET_REGEX
6277}
6378
6479/**
80+ * Saves an array of regex scripts for a specific type.
81+ * @param {RegexScript[]} scripts An array of regex scripts to save.
82+ * @param {SCRIPT_TYPES} scriptType The type of regex scripts to save.
83+ * @returns {Promise<void>}
84+ */
85+export async function saveScriptsByType(scripts, scriptType) {
86+ switch (scriptType) {
87+ case SCRIPT_TYPES.GLOBAL:
88+ extension_settings.regex = scripts;
89+ saveSettingsDebounced();
90+ break;
91+ case SCRIPT_TYPES.SCOPED:
92+ await writeExtensionField(this_chid, 'regex_scripts', scripts);
93+ break;
94+ case SCRIPT_TYPES.PRESET: {
95+ const presetManager = getPresetManager();
96+ await presetManager.writePresetExtensionField({ path: 'regex_scripts', value: scripts });
97+ break;
98+ }
99+ default:
100+ console.warn(`saveScriptsByType: Invalid script type ${scriptType}`);
101+ break;
102+ }
103+}
104+
105+/**
106+ * Check if character's regexes are allowed to be used; if character is undefined, returns false
107+ * @param {import('../../char-data.js').v1CharData|undefined} character
108+ * @returns {boolean}
109+ */
110+export function isScopedScriptsAllowed(character) {
111+ return !!extension_settings?.character_allowed_regex?.includes(character?.avatar);
112+}
113+
114+/**
115+ * Allow character's regexes to be used; if character is undefined, do nothing
116+ * @param {import('../../char-data.js').v1CharData|undefined} character
117+ * @returns {void}
118+ */
119+export function allowScopedScripts(character) {
120+ const avatar = character?.avatar;
121+ if (!avatar) {
122+ return;
123+ }
124+ if (!Array.isArray(extension_settings?.character_allowed_regex)) {
125+ extension_settings.character_allowed_regex = [];
126+ }
127+ if (!extension_settings.character_allowed_regex.includes(avatar)) {
128+ extension_settings.character_allowed_regex.push(avatar);
129+ saveSettingsDebounced();
130+ }
131+}
132+
133+/**
134+ * Disallow character's regexes to be used; if character is undefined, do nothing
135+ * @param {import('../../char-data.js').v1CharData|undefined} character
136+ * @returns {void}
137+ */
138+export function disallowScopedScripts(character) {
139+ const avatar = character?.avatar;
140+ if (!avatar) {
141+ return;
142+ }
143+ if (!Array.isArray(extension_settings?.character_allowed_regex)) {
144+ return;
145+ }
146+ const index = extension_settings.character_allowed_regex.indexOf(avatar);
147+ if (index !== -1) {
148+ extension_settings.character_allowed_regex.splice(index, 1);
149+ saveSettingsDebounced();
150+ }
151+}
152+
153+/**
154+ * Check if preset's regexes are allowed to be used
155+ * @param {string} apiId API ID
156+ * @param {string} presetName Preset name
157+ * @returns {boolean} True if allowed, false if not
158+ */
159+export function isPresetScriptsAllowed(apiId, presetName) {
160+ if (!apiId || !presetName) {
161+ return false;
162+ }
163+ return !!extension_settings?.preset_allowed_regex?.[apiId]?.includes(presetName);
164+}
165+
166+/**
167+ * Allow preset's regexes to be used
168+ * @param {string} apiId API ID
169+ * @param {string} presetName Preset name
170+ * @returns {void}
171+ */
172+export function allowPresetScripts(apiId, presetName) {
173+ if (!apiId || !presetName) {
174+ return;
175+ }
176+ if (!Array.isArray(extension_settings?.preset_allowed_regex?.[apiId])) {
177+ lodash.set(extension_settings, ['preset_allowed_regex', apiId], []);
178+ }
179+ if (!extension_settings.preset_allowed_regex[apiId].includes(presetName)) {
180+ extension_settings.preset_allowed_regex[apiId].push(presetName);
181+ saveSettingsDebounced();
182+ }
183+}
184+
185+/**
186+ * Disallow preset's regexes to be used
187+ * @param {string} apiId API ID
188+ * @param {string} presetName Preset name
189+ * @returns {void}
190+ */
191+export function disallowPresetScripts(apiId, presetName) {
192+ if (!apiId || !presetName) {
193+ return;
194+ }
195+ if (!Array.isArray(extension_settings?.preset_allowed_regex?.[apiId])) {
196+ return;
197+ }
198+ const index = extension_settings.preset_allowed_regex[apiId].indexOf(presetName);
199+ if (index !== -1) {
200+ extension_settings.preset_allowed_regex[apiId].splice(index, 1);
201+ saveSettingsDebounced();
202+ }
203+}
204+
205+/**
206+ * Gets the current API ID from the preset manager.
207+ * @returns {string|null} Current API ID, or null if no preset manager
208+ */
209+export function getCurrentPresetAPI() {
210+ return getPresetManager()?.apiId ?? null;
211+}
212+
213+/**
214+ * Gets the name of the currently selected preset.
215+ * @returns {string|null} The name of the currently selected preset, or null if no preset manager
216+ */
217+export function getCurrentPresetName() {
218+ return getPresetManager()?.getSelectedPresetName() ?? null;
219+}
220+
221+/**
65222 * @enum {number} Where the regex script should be applied
66223 * @readonly
67224 */
public/scripts/extensions/regex/importTarget.html+6 -0
@@ -15,5 +15,11 @@
1515 Scoped Scripts
1616 </span>
1717 </label>
18+ <label for="regex_import_target_preset">
19+ <input type="radio" name="regex_import_target" id="regex_import_target_preset" value="preset" />
20+ <span data-i18n="ext_regex_preset_scripts">
21+ Preset Scripts
22+ </span>
23+ </label>
1824 </div>
1925</div>
public/scripts/extensions/regex/index.js+305 -92
@@ -1,5 +1,5 @@
11import { characters, eventSource, event_types, getCurrentChatId, messageFormatting, reloadCurrentChat, saveSettingsDebounced, this_chid } from '../../../script.js';
22import { extension_settings, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js';
33import { selected_group } from '../../group-chats.js';
44import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';
55import { SlashCommand } from '../../slash-commands/SlashCommand.js';
@@ -8,9 +8,10 @@ import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashComman
88import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
99import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
1010import { download, equalsIgnoreCaseAndAccents, escapeHtml, getFileText, getSortableDelay, isFalseBoolean, isTrueBoolean, regexFromString, setInfoBlock, uuidv4 } from '../../utils.js';
1111import { allowPresetScripts, allowScopedScripts, disallowPresetScripts, disallowScopedScripts, getCurrentPresetAPI, getCurrentPresetName, getRegexScripts, getScriptsByType, isPresetScriptsAllowed, isScopedScriptsAllowed, regex_placement, runRegexScript, saveScriptsByType, SCRIPT_TYPE_UNKNOWN, SCRIPT_TYPES, substitute_find_regex } from './engine.js';
1212import { t } from '../../i18n.js';
1313import { accountStorage } from '../../util/AccountStorage.js';
14+import { getPresetManager } from '../../preset-manager.js';
1415
1516// Re-exports for legacy extensions
1617export { getRegexScripts };
@@ -33,12 +34,14 @@ const sanitizeFileName = name => name.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, '
3334 * @property {boolean} isSelected - Whether the preset is currently selected
3435 * @property {RegexPresetItem[]} global - The list of global preset items
3536 * @property {RegexPresetItem[]} scoped - The list of scoped preset items
37+ * @property {RegexPresetItem[]} preset - The list of preset preset items
3638 */
3739
3840/**
3941 * @typedef {object} RegexPresetState
4042 * @property {string[]} global - List of enabled global regex script IDs
4143 * @property {string[]} scoped - List of enabled scoped regex script IDs
44+ * @property {string[]} preset - List of enabled preset regex script IDs
4245 */
4346
4447class RegexPresetManager {
@@ -70,10 +73,12 @@ class RegexPresetManager {
7073 captureCurrentState() {
7174 const globalScripts = this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.GLOBAL));
7275 const scopedScripts = this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.SCOPED));
76+ const presetScripts = this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.PRESET));
7377
7478 return {
7579 global: globalScripts.map(item => item.id).sort(),
7680 scoped: scopedScripts.map(item => item.id).sort(),
81+ preset: presetScripts.map(item => item.id).sort(),
7782 };
7883 }
7984
@@ -90,13 +95,16 @@ class RegexPresetManager {
9095 const global2 = state2.global || [];
9196 const scoped1 = state1.scoped || [];
9297 const scoped2 = state2.scoped || [];
98+ const preset1 = state1.preset || [];
99+ const preset2 = state2.preset || [];
93100
94101 if (global1.length !== global2.length || scoped1.length !== scoped2.length) {
95102 return true;
96103 }
97104
98105 return !global1.every(id => global2.includes(id)) ||
99106 !scoped1.every(id => scoped2.includes(id)); ||
107+ !preset1.every(id => preset2.includes(id));
100108 }
101109
102110 /**
@@ -365,17 +373,18 @@ class RegexPresetManager {
365373 return;
366374 }
367375
368376 // Apply to both globalpreset andto scopedall lists
369- await this.applyPresetList({
377+ for (const scriptType of Object.values(SCRIPT_TYPES)) {
370- presetList: preset.global,
378+ await this.applyPresetList({
371- targetList: extension_settings.regex,
379+ presetList: {
372- saveFunction: () => saveSettingsDebounced(),
380+ [SCRIPT_TYPES.GLOBAL]: preset.global,
373- });
381+ [SCRIPT_TYPES.SCOPED]: preset.scoped,
374- await this.applyPresetList({
382+ [SCRIPT_TYPES.PRESET]: preset.preset,
375- presetList: preset.scoped,
383+ }[scriptType],
376- targetList: characters[this_chid]?.data?.extensions?.regex_scripts,
384+ targetList: getScriptsByType(scriptType),
377385 saveFunction: (scripts) => writeExtensionFieldsaveScriptsByType(this_chid, 'regex_scripts'scripts, scriptsscriptType),
378386 });
387+ }
379388
380389 // Render the changes to the UI
381390 await loadRegexScripts();
@@ -423,6 +432,7 @@ class RegexPresetManager {
423432 isSelected: false,
424433 global: this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.GLOBAL)),
425434 scoped: this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.SCOPED)),
435+ preset: this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.PRESET)),
426436 };
427437
428438 if (isUpdate) {
@@ -485,15 +495,17 @@ function setToggleAllIcon(allAreChecked) {
485495function setMoveButtonsVisibility() {
486496 const hasGlobalScripts = $('#saved_regex_scripts .regex-script-label:has(.regex_bulk_checkbox:checked)').length > 0;
487497 const hasScopedScripts = $('#saved_scoped_scripts .regex-script-label:has(.regex_bulk_checkbox:checked)').length > 0;
488- $('#bulk_regex_move_to_global').toggle(hasScopedScripts);
498+ const hasPresetScripts = $('#saved_preset_scripts .regex-script-label:has(.regex_bulk_checkbox:checked)').length > 0;
489499 $('#bulk_regex_move_to_scopedbulk_regex_move_to_global').toggle(hasGlobalScriptshasScopedScripts || hasPresetScripts);
500+ $('#bulk_regex_move_to_scoped').toggle(hasGlobalScripts || hasPresetScripts);
501+ $('#bulk_regex_move_to_preset').toggle(hasGlobalScripts || hasScopedScripts);
490502}
491503
492504/**
493505 * Saves a regex script to the extension settings or character data.
494506 * @param {import('../../char-data.js').RegexScriptData} regexScript
495507 * @param {number} existingScriptIndex Index of the existing script
496508 * @param {SCRIPT_TYPES} scriptType global?Type scoped?of the script
497509 * @param {boolean} [saveSettings=true] Whether to save the settings immediately
498510 * @returns {Promise<void>}
499511 */
@@ -529,12 +541,13 @@ async function saveRegexScript(regexScript, existingScriptIndex, scriptType, sav
529541 }
530542
531543 if (scriptType === SCRIPT_TYPES.SCOPED) {
532544 await writeExtensionFieldsaveScriptsByType(this_chid, 'regex_scripts'array, arraySCRIPT_TYPES.SCOPED);
545+ allowScopedScripts(characters?.[this_chid]);
546+ }
533547
534- // Add the character to the allowed list
548+ if (scriptType === SCRIPT_TYPES.PRESET) {
535- if (!extension_settings.character_allowed_regex.includes(characters[this_chid].avatar)) {
549+ await saveScriptsByType(array, SCRIPT_TYPES.PRESET);
536- extension_settings.character_allowed_regex.push(characters[this_chid].avatar);
550+ allowPresetScripts(getCurrentPresetAPI(), getCurrentPresetName());
537- }
538551 }
539552
540553 if (saveSettings) {
@@ -557,7 +570,7 @@ async function saveRegexScript(regexScript, existingScriptIndex, scriptType, sav
557570/**
558571 * Delete a regex script by ID
559572 * @param {string} id ID of the script to delete
560573 * @param {SCRIPT_TYPES} scriptType global?Type scoped?of the script
561574 * @param {boolean} saveSettings Whether to save the settings immediately
562575 * @returns {Promise<void>}
563576 */
@@ -568,8 +581,18 @@ async function deleteRegexScript(id, scriptType, saveSettings = true) {
568581 if (existingScriptIndex !== -1) {
569582 array.splice(existingScriptIndex, 1);
570583
571584 ifswitch (scriptType === SCRIPT_TYPES.SCOPED) {
572- await writeExtensionField(this_chid, 'regex_scripts', array);
585+ case SCRIPT_TYPES.GLOBAL:
586+ // will be handled by saveSettingsDebounced
587+ break;
588+ case SCRIPT_TYPES.SCOPED:
589+ await saveScriptsByType(array, SCRIPT_TYPES.SCOPED);
590+ break;
591+ case SCRIPT_TYPES.PRESET:
592+ await saveScriptsByType(array, SCRIPT_TYPES.PRESET);
593+ break;
594+ default:
595+ break;
573596 }
574597 if (saveSettings) {
575598 saveSettingsDebounced();
@@ -594,7 +617,7 @@ async function moveRegexScript(script, toType, fromType = null, saveSettings = t
594617 if (!Object.values(SCRIPT_TYPES).includes(fromType)) {
595618 fromType = getScriptType(script);
596619 }
597620 if (fromType === toType || fromType === SCRIPT_TYPES.UNKNOWNSCRIPT_TYPE_UNKNOWN || toType === SCRIPT_TYPES.UNKNOWNSCRIPT_TYPE_UNKNOWN) {
598621 return;
599622 }
600623 await deleteRegexScript(script.id, fromType, false);
@@ -604,6 +627,7 @@ async function moveRegexScript(script, toType, fromType = null, saveSettings = t
604627async function loadRegexScripts() {
605628 $('#saved_regex_scripts').empty();
606629 $('#saved_scoped_scripts').empty();
630+ $('#saved_preset_scripts').empty();
607631 setToggleAllIcon(false);
608632
609633 const scriptTemplate = $(await renderExtensionTemplateAsync('regex', 'scriptTemplate'));
@@ -612,7 +636,7 @@ async function loadRegexScripts() {
612636 * Renders a script to the UI.
613637 * @param {string} container Container to render the script to
614638 * @param {import('../../char-data.js').RegexScriptData} script Script data
615639 * @param {SCRIPT_TYPES} scriptType global?Type scoped?of the script
616640 * @param {number} index Index of the script in the array
617641 */
618642 function renderScript(container, script, scriptType, index) {
@@ -625,7 +649,7 @@ async function loadRegexScripts() {
625649 }
626650
627651 scriptHtml.attr('id', script.id);
628652 scriptHtml.find('.regex_script_name').text(script.scriptName).attr('title', script.scriptName);
629653 scriptHtml.find('.disable_regex').prop('checked', script.disabled ?? false)
630654 .on('input', async function () {
631655 script.disabled = !!$(this).prop('checked');
@@ -663,6 +687,16 @@ async function loadRegexScripts() {
663687 }
664688 await moveRegexScript(script, SCRIPT_TYPES.SCOPED, scriptType);
665689 });
690+ scriptHtml.find('.move_to_preset').on('click', async function () {
691+ const confirm = await callGenericPopup(
692+ t`Are you sure you want to move this regex script to preset?`,
693+ POPUP_TYPE.CONFIRM,
694+ );
695+ if (!confirm) {
696+ return;
697+ }
698+ await moveRegexScript(script, SCRIPT_TYPES.PRESET, scriptType);
699+ });
666700 scriptHtml.find('.export_regex').on('click', async function () {
667701 const fileName = `regex-${sanitizeFileName(script.scriptName)}.json`;
668702 const fileData = JSON.stringify(script, null, 4);
@@ -682,15 +716,40 @@ async function loadRegexScripts() {
682716 const allAreChecked = checkboxes.length === checkboxes.filter(':checked').length;
683717 setToggleAllIcon(allAreChecked);
684718 });
719+ scriptHtml.find('input[name="regex_expand"]').on('change', function () {
720+ if (!(this instanceof HTMLInputElement)) {
721+ return;
722+ }
723+
724+ if (!this.checked) {
725+ return;
726+ }
727+
728+ const closeMenuHandler = (e) => {
729+ if (e.target instanceof HTMLElement) {
730+ if (e.target.closest('.regex-script-label')) {
731+ return;
732+ }
733+ this.checked = false;
734+ document.removeEventListener('click', closeMenuHandler);
735+ }
736+ };
737+
738+ // Use setTimeout to avoid closing immediately from the same click
739+ setTimeout(() => {
740+ document.addEventListener('click', closeMenuHandler, { passive: true, once: false });
741+ }, 0);
742+ });
685743
686744 $(container).append(scriptHtml);
687745 }
688746
689747 getScriptsByType(SCRIPT_TYPES.GLOBAL).forEach((script, index) => renderScript('#saved_regex_scripts', script, SCRIPT_TYPES.GLOBAL, index));
690748 getScriptsByType(SCRIPT_TYPES.SCOPED).forEach((script, index) => renderScript('#saved_scoped_scripts', script, SCRIPT_TYPES.SCOPED, index));
749+ getScriptsByType(SCRIPT_TYPES.PRESET).forEach((script, index) => renderScript('#saved_preset_scripts', script, SCRIPT_TYPES.PRESET, index));
691750
692- const isScopedAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar);
751+ $('#regex_scoped_toggle').prop('checked', isScopedScriptsAllowed(characters?.[this_chid]));
693752 $('#regex_scoped_toggleregex_preset_toggle').prop('checked', isScopedAllowedisPresetScriptsAllowed(getCurrentPresetAPI(), getCurrentPresetName()));
694753
695754 setMoveButtonsVisibility();
696755}
@@ -698,7 +757,7 @@ async function loadRegexScripts() {
698757/**
699758 * Opens the regex editor.
700759 * @param {string|boolean} existingId Existing ID
701760 * @param {SCRIPT_TYPES} scriptType global?Type scoped?of the script
702761 * @returns {Promise<void>}
703762 */
704763async function onRegexEditorOpenClick(existingId, scriptType) {
@@ -1006,23 +1065,30 @@ function populateDebuggerRuleList(container) {
10061065 }
10071066
10081067 const globalScriptIds = new Set(getScriptsByType(SCRIPT_TYPES.GLOBAL).map(s => s.id));
1068+ const scopedScriptIds = new Set(getScriptsByType(SCRIPT_TYPES.SCOPED).map(s => s.id));
1069+ const presetScriptIds = new Set(getScriptsByType(SCRIPT_TYPES.PRESET).map(s => s.id));
10091070 const globalScripts = [];
10101071 const scopedScripts = [];
1072+ const presetScripts = [];
10111073
10121074 allScripts.forEach(script => {
10131075 const scriptCopy = structuredClone(script); // Use structuredClone for deep copy
10141076 if (globalScriptIds.has(script.id)) {
10151077 // @ts-ignore
10161078 scriptCopy.type = SCRIPT_TYPES.SCOPEDGLOBAL;
10171079 globalScripts.push(scriptCopy);
1018- } else {
1080+ } else if (scopedScriptIds.has(script.id)) {
10191081 // @ts-ignore
10201082 scriptCopy.type = SCRIPT_TYPES.SCOPED;
10211083 scopedScripts.push(scriptCopy);
1084+ } else if (presetScriptIds.has(script.id)) {
1085+ // @ts-ignore
1086+ scriptCopy.type = SCRIPT_TYPES.PRESET;
1087+ presetScripts.push(scriptCopy);
10221088 }
10231089 });
10241090
10251091 container.data('allScripts', [...globalScripts, ...scopedScripts, ...presetScripts]);
10261092
10271093 const renderRule = (script) => {
10281094 if (!script.id) script.id = uuidv4();
@@ -1040,6 +1106,7 @@ function populateDebuggerRuleList(container) {
10401106 {
10411107 [SCRIPT_TYPES.SCOPED]: t`Scoped`,
10421108 [SCRIPT_TYPES.GLOBAL]: t`Global`,
1109+ [SCRIPT_TYPES.PRESET]: t`Preset`,
10431110 }[script.type],
10441111 );
10451112 ruleElement.find('.rule-enabled').prop('checked', !script.disabled);
@@ -1086,6 +1153,13 @@ function populateDebuggerRuleList(container) {
10861153 scopedScripts.forEach(script => scopedList.append(renderRule(script)));
10871154 rulesContainer.append(scopedList);
10881155 }
1156+
1157+ if (presetScripts.length > 0) {
1158+ rulesContainer.append('<div class="list-header regex-debugger-list-header">' + t`Preset Rules` + '</div>');
1159+ const presetList = $('<ul id="regex_debugger_rules_preset" class="sortable-list"></ul>');
1160+ presetScripts.forEach(script => presetList.append(renderRule(script)));
1161+ rulesContainer.append(presetList);
1162+ }
10891163}
10901164
10911165/**
@@ -1104,12 +1178,15 @@ async function onRegexDebuggerOpenClick() {
11041178 debuggerHtml.find('#regex_debugger_rules_global').sortable({ delay: getSortableDelay() }).disableSelection();
11051179 // @ts-ignore
11061180 debuggerHtml.find('#regex_debugger_rules_scoped').sortable({ delay: getSortableDelay() }).disableSelection();
1181+ // @ts-ignore
1182+ debuggerHtml.find('#regex_debugger_rules_preset').sortable({ delay: getSortableDelay() }).disableSelection();
11071183
11081184 debuggerHtml.find('#regex_debugger_run_test').on('click', function () {
11091185 const allScripts = debuggerHtml.data('allScripts');
11101186 const orderedRuleIds = [
11111187 ...$('#regex_debugger_rules_global').find('li.regex-debugger-rule').map((i, el) => $(el).data('id')).get(),
11121188 ...$('#regex_debugger_rules_scoped').find('li.regex-debugger-rule').map((i, el) => $(el).data('id')).get(),
1189+ ...$('#regex_debugger_rules_preset').find('li.regex-debugger-rule').map((i, el) => $(el).data('id')).get(),
11131190 ];
11141191
11151192 const rawInput = String($('#regex_debugger_raw_input').val());
@@ -1187,11 +1264,13 @@ async function onRegexDebuggerOpenClick() {
11871264 const allKnownScripts = getRegexScripts();
11881265 const newGlobalScripts = $('#regex_debugger_rules_global').children('li').map((_, el) => allKnownScripts.find(s => s.id === $(el).data('id'))).get().filter(Boolean);
11891266 const newScopedScripts = $('#regex_debugger_rules_scoped').children('li').map((_, el) => allKnownScripts.find(s => s.id === $(el).data('id'))).get().filter(Boolean);
1267+ const newPresetScripts = $('#regex_debugger_rules_preset').children('li').map((_, el) => allKnownScripts.find(s => s.id === $(el).data('id'))).get().filter(Boolean);
11901268
11911269 extension_settings.regex = newGlobalScripts;
11921270 if (this_chid !== undefined) {
11931271 await writeExtensionFieldsaveScriptsByType(this_chid, 'regex_scripts'newScopedScripts, newScopedScriptsSCRIPT_TYPES.SCOPED);
11941272 }
1273+ await saveScriptsByType(newPresetScripts, SCRIPT_TYPES.PRESET);
11951274
11961275 saveSettingsDebounced();
11971276 await loadRegexScripts();
@@ -1203,6 +1282,8 @@ async function onRegexDebuggerOpenClick() {
12031282 currentPopupContent.find('#regex_debugger_rules_global').sortable({ delay: getSortableDelay() }).disableSelection();
12041283 // @ts-ignore
12051284 currentPopupContent.find('#regex_debugger_rules_scoped').sortable({ delay: getSortableDelay() }).disableSelection();
1285+ // @ts-ignore
1286+ currentPopupContent.find('#regex_debugger_rules_preset').sortable({ delay: getSortableDelay() }).disableSelection();
12061287 });
12071288
12081289 debuggerHtml.find('#regex_debugger_expand_steps').on('click', function () {
@@ -1328,11 +1409,6 @@ function migrateSettings() {
13281409 }
13291410 });
13301411
1331- if (!extension_settings.character_allowed_regex) {
1332- extension_settings.character_allowed_regex = [];
1333- performSave = true;
1334- }
1335-
13361412 if (performSave) {
13371413 saveSettingsDebounced();
13381414 }
@@ -1418,8 +1494,8 @@ async function toggleRegexCallback(args, scriptName) {
14181494
14191495/**
14201496 * Performs the import of the regex object.
14211497 * @param {ObjectRegexScript} regexScript Input object
14221498 * @param {SCRIPT_TYPES} scriptType global?The scoped?type of script to import as
14231499 */
14241500async function onRegexImportObjectChange(regexScript, scriptType) {
14251501 try {
@@ -1433,8 +1509,18 @@ async function onRegexImportObjectChange(regexScript, scriptType) {
14331509 const array = getScriptsByType(scriptType);
14341510 array.push(regexScript);
14351511
14361512 ifswitch (scriptType === SCRIPT_TYPES.SCOPED) {
1437- await writeExtensionField(this_chid, 'regex_scripts', array);
1513+ case SCRIPT_TYPES.GLOBAL:
1514+ // will be handled by saveSettingsDebounced
1515+ break;
1516+ case SCRIPT_TYPES.SCOPED:
1517+ await saveScriptsByType(array, SCRIPT_TYPES.SCOPED);
1518+ break;
1519+ case SCRIPT_TYPES.PRESET:
1520+ await saveScriptsByType(array, SCRIPT_TYPES.PRESET);
1521+ break;
1522+ default:
1523+ break;
14381524 }
14391525
14401526 saveSettingsDebounced();
@@ -1450,7 +1536,7 @@ async function onRegexImportObjectChange(regexScript, scriptType) {
14501536/**
14511537 * Performs the import of the regex file.
14521538 * @param {File} file Input file
14531539 * @param {SCRIPT_TYPES} scriptType global?The scoped?type of script to import as
14541540 */
14551541async function onRegexImportFileChange(file, scriptType) {
14561542 if (!file) {
@@ -1480,17 +1566,13 @@ async function onRegexImportFileChange(file, scriptType) {
14801566 * @returns {SCRIPT_TYPES} The script type.
14811567 */
14821568function getScriptType(script) {
1483- const scopedScripts = getScriptsByType(SCRIPT_TYPES.SCOPED);
1569+ for (const scriptType of Object.values(SCRIPT_TYPES)) {
1484- if (scopedScripts.some(s => s.id === script.id)) {
1570+ const scripts = getScriptsByType(scriptType);
1485- return SCRIPT_TYPES.SCOPED;
1571+ if (scripts.some(s => s.id === script.id)) {
1486- }
1572+ return scriptType;
1487-
1573+ }
1488- const globalScripts = getScriptsByType(SCRIPT_TYPES.GLOBAL);
1489- if (globalScripts.some(s => s.id === script.id)) {
1490- return SCRIPT_TYPES.GLOBAL;
14911574 }
1492-
1575+ return SCRIPT_TYPE_UNKNOWN;
1493- return SCRIPT_TYPES.UNKNOWN;
14941576}
14951577
14961578function getSelectedScripts() {
@@ -1507,44 +1589,121 @@ function purgeEmbeddedRegexScripts({ character }) {
15071589 if (!avatar) {
15081590 return;
15091591 }
15101592 const checkKey = `AlertRegex_${characters[this_chid].avatar}`;
15111593 if (accountStorage.getItem(checkKey)) {
15121594 accountStorage.removeItem(checkKey);
15131595 }
1514- const index = extension_settings.character_allowed_regex.indexOf(avatar);
1596+ disallowScopedScripts(characters?.[this_chid]);
1515- if (index !== -1) {
1597+}
1516- extension_settings.character_allowed_regex.splice(index, 1);
1598+
1517- saveSettingsDebounced();
1599+function purgePresetEmbeddedRegexScripts({ apiId, name }) {
1600+ const checkKey = `AlertRegex_${apiId}_${name}`;
1601+ if (accountStorage.getItem(checkKey)) {
1602+ accountStorage.removeItem(checkKey);
15181603 }
1604+ disallowPresetScripts(apiId, name);
15191605}
15201606
15211607async function checkCharEmbeddedRegexScripts() {
15221608 const chid = this_chid;
15231609
15241610 if (chid !== undefined && !selected_group) {
15251611 const avatarcharacter = characters[chid]?.avatar;
15261612 const scripts = getScriptsByType(SCRIPT_TYPES.SCOPED);
15271613
15281614 if (Array.isArray(scripts) && scripts.length > 0) {
15291615 if (avatar && !extension_settings.character_allowed_regex.includesisScopedScriptsAllowed(avatarcharacter)) {
15301616 const checkKey = `AlertRegex_${characters[chid]character.avatar}`;
1531-
15321617 if (!accountStorage.getItem(checkKey)) {
15331618 accountStorage.setItem(checkKey, 'true');
15341619 const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {});
15351620 const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { okButton: 'Yes' });
15361621
15371622 if (result) {
15381623 extension_settings.character_allowed_regex.pushallowScopedScripts(avatarcharacter);
1624+ await reloadCurrentChat();
1625+ }
1626+ }
1627+ }
1628+ }
1629+ }
1630+
1631+ await loadRegexScripts();
1632+}
1633+
1634+/**
1635+ * Notify whether to reload current chat when preset is changed
1636+ * @param {string} presetName The name of the preset
1637+ */
1638+function notifyReloadCurrentChat(presetName) {
1639+ toastr.info(
1640+ t`Reload the chat for regex to take effect` + '<br><u>' + t`Click here to reload immediately` + '</u>',
1641+ t`Preset '${presetName}' contains enabled regex scripts`,
1642+ {
1643+ timeOut: 5000,
1644+ escapeHtml: false,
1645+ onclick: reloadCurrentChat,
1646+ });
1647+}
1648+
1649+async function checkPresetEmbeddedRegexScripts() {
1650+ const apiId = getCurrentPresetAPI();
1651+ const name = getCurrentPresetName();
1652+ const scripts = getScriptsByType(SCRIPT_TYPES.PRESET);
1653+
1654+ if (Array.isArray(scripts) && scripts.length > 0) {
1655+ if (!isPresetScriptsAllowed(apiId, name)) {
1656+ const checkKey = `AlertRegex_${apiId}_${name}`;
1657+
1658+ if (!accountStorage.getItem(checkKey)) {
1659+ accountStorage.setItem(checkKey, 'true');
1660+ const template = await renderExtensionTemplateAsync('regex', 'presetEmbeddedScripts', {});
1661+ const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '');
1662+
1663+ if (result) {
1664+ allowPresetScripts(apiId, name);
1665+ if (getCurrentChatId()) {
15391666 await reloadCurrentChat();
1540- saveSettingsDebounced();
15411667 }
15421668 }
15431669 }
1670+ } else if (getCurrentChatId() && scripts.filter(script => !script.disabled).length > 0) {
1671+ notifyReloadCurrentChat(name);
15441672 }
15451673 }
15461674
15471675 await loadRegexScripts();
1676+}
1677+
1678+async function onMainApiChanged({ apiId }) {
1679+ const presetManager = getPresetManager(apiId);
1680+ if (!presetManager) {
1681+ return;
1682+ }
1683+ const presetName = presetManager.getSelectedPresetName();
1684+ const presetScripts = presetManager.readPresetExtensionField({ path: 'regex_scripts' }) ?? [];
1685+ if (getCurrentChatId() &&
1686+ isPresetScriptsAllowed(apiId, presetName) &&
1687+ Array.isArray(presetScripts) &&
1688+ presetScripts.filter(script => !script.disabled).length > 0) {
1689+ notifyReloadCurrentChat(presetName);
1690+ }
1691+
1692+ await loadRegexScripts();
1693+}
1694+
1695+function onPresetRenamed({ apiId, oldName, newName }) {
1696+ const oldCheckKey = `AlertRegex_${apiId}_${oldName}`;
1697+ const checkKey = `AlertRegex_${apiId}_${newName}`;
1698+ const value = accountStorage.getItem(oldCheckKey);
1699+ if (value) {
1700+ accountStorage.setItem(checkKey, value);
1701+ accountStorage.removeItem(oldCheckKey);
1702+ }
1703+ if (isPresetScriptsAllowed(apiId, oldName)) {
1704+ disallowPresetScripts(apiId, oldName);
1705+ allowPresetScripts(apiId, newName);
1706+ }
15481707}
15491708
15501709// Workaround for loading in sequence with other extensions
@@ -1584,11 +1743,15 @@ jQuery(async () => {
15841743
15851744 onRegexEditorOpenClick(false, SCRIPT_TYPES.SCOPED);
15861745 });
1746+ $('#open_preset_editor').on('click', function () {
1747+ onRegexEditorOpenClick(false, SCRIPT_TYPES.PRESET);
1748+ });
15871749 $('#import_regex_file').on('change', async function () {
15881750 let target = SCRIPT_TYPES.GLOBAL;
15891751 const template = $(await renderExtensionTemplateAsync('regex', 'importTarget'));
15901752 template.find('#regex_import_target_global').on('input', () => (target = SCRIPT_TYPES.GLOBAL));
15911753 template.find('#regex_import_target_scoped').on('input', () => (target = SCRIPT_TYPES.SCOPED));
1754+ template.find('#regex_import_target_preset').on('input', () => (target = SCRIPT_TYPES.PRESET));
15921755
15931756 await callGenericPopup(template, POPUP_TYPE.TEXT);
15941757
@@ -1617,30 +1780,47 @@ jQuery(async () => {
16171780 });
16181781
16191782 $('#bulk_enable_regex').on('click', async function () {
1620- const scripts = getSelectedScripts().filter(script => script.disabled);
1783+ await bulkToggleRegexScripts(true);
1621- if (scripts.length === 0) {
1622- toastr.warning(t`No regex scripts selected for enabling.`);
1623- return;
1624- }
1625- for (const script of scripts) {
1626- script.disabled = false;
1627- }
1628- saveSettingsDebounced();
1629- await loadRegexScripts();
16301784 });
16311785
16321786 $('#bulk_disable_regex').on('click', async function () {
1633- const scripts = getSelectedScripts().filter(script => !script.disabled);
1787+ await bulkToggleRegexScripts(false);
1788+ });
1789+
1790+ /**
1791+ * Bulk enable or disable regex scripts
1792+ * @param {boolean} newState New state to set (true = enable, false = disable)
1793+ * @returns {Promise<void>}
1794+ */
1795+ async function bulkToggleRegexScripts(newState) {
1796+ const scripts = getSelectedScripts().filter(script => script.disabled === newState);
16341797 if (scripts.length === 0) {
1635- toastr.warning(t`No regex scripts selected for disabling.`);
1798+ toastr.warning(newState
1799+ ? t`No regex scripts selected for enabling.`
1800+ : t`No regex scripts selected for disabling.`,
1801+ );
16361802 return;
16371803 }
1804+ const scriptTypesToSave = new Set();
16381805 for (const script of scripts) {
1639- script.disabled = true;
1806+ const scriptType = getScriptType(script);
1807+ scriptTypesToSave.add(scriptType);
1808+ script.disabled = !newState;
16401809 }
1810+ for (const scriptType of scriptTypesToSave) {
1811+ const scriptsOfType = getScriptsByType(scriptType);
1812+ await saveScriptsByType(scriptsOfType, scriptType);
1813+ }
1814+
16411815 saveSettingsDebounced();
16421816 await loadRegexScripts();
1643- });
1817+
1818+ // Reload the current chat to undo previous markdown
1819+ const currentChatId = getCurrentChatId();
1820+ if (currentChatId) {
1821+ await reloadCurrentChat();
1822+ }
1823+ }
16441824
16451825 /**
16461826 * Bulk move regex scripts to the specified type
@@ -1656,6 +1836,7 @@ jQuery(async () => {
16561836 await moveRegexScript(script, toType, getScriptType(script), false);
16571837 }
16581838
1839+ saveSettingsDebounced();
16591840 await loadRegexScripts();
16601841
16611842 // Reload the current chat to undo previous markdown
@@ -1681,6 +1862,14 @@ jQuery(async () => {
16811862 await bulkMoveRegexScript(SCRIPT_TYPES.SCOPED);
16821863 });
16831864
1865+ $('#bulk_regex_move_to_preset').on('click', async function () {
1866+ const confirm = await callGenericPopup(t`Are you sure you want to move the selected regex scripts to preset?`, POPUP_TYPE.CONFIRM);
1867+ if (!confirm) {
1868+ return;
1869+ }
1870+ await bulkMoveRegexScript(SCRIPT_TYPES.PRESET);
1871+ });
1872+
16841873 $('#bulk_delete_regex').on('click', async function () {
16851874 const scripts = getSelectedScripts();
16861875 if (scripts.length === 0) {
@@ -1714,14 +1903,19 @@ jQuery(async () => {
17141903 let sortableDatas = [
17151904 {
17161905 selector: '#saved_regex_scripts',
17171906 setter: xscripts => extension_settings.regex =saveScriptsByType(scripts, xSCRIPT_TYPES.GLOBAL),
17181907 getter: () => getScriptsByType(SCRIPT_TYPES.GLOBAL),
17191908 },
17201909 {
17211910 selector: '#saved_scoped_scripts',
17221911 setter: xscripts => writeExtensionFieldsaveScriptsByType(this_chid, 'regex_scripts'scripts, xSCRIPT_TYPES.SCOPED),
17231912 getter: () => getScriptsByType(SCRIPT_TYPES.SCOPED),
17241913 },
1914+ {
1915+ selector: '#saved_preset_scripts',
1916+ setter: scripts => saveScriptsByType(scripts, SCRIPT_TYPES.PRESET),
1917+ getter: () => getScriptsByType(SCRIPT_TYPES.PRESET),
1918+ },
17251919 ];
17261920 for (const { selector, setter, getter } of sortableDatas) {
17271921 // @ts-ignore
@@ -1760,17 +1954,26 @@ jQuery(async () => {
17601954 }
17611955
17621956 const isEnable = !!$(this).prop('checked');
17631957 const avatarcharacter = characters[this_chid].avatar;
17641958
17651959 if (isEnable) {
1766- if (!extension_settings.character_allowed_regex.includes(avatar)) {
1960+ allowScopedScripts(character);
1767- extension_settings.character_allowed_regex.push(avatar);
1768- }
17691961 } else {
1770- const index = extension_settings.character_allowed_regex.indexOf(avatar);
1962+ disallowScopedScripts(character);
1771- if (index !== -1) {
1963+ }
1772- extension_settings.character_allowed_regex.splice(index, 1);
1964+
1773- }
1965+ saveSettingsDebounced();
1966+ reloadCurrentChat();
1967+ });
1968+
1969+ $('#regex_preset_toggle').on('input', function () {
1970+ const isEnable = !!$(this).prop('checked');
1971+ const name = getCurrentPresetName();
1972+
1973+ if (isEnable) {
1974+ allowPresetScripts(getCurrentPresetAPI(), name);
1975+ } else {
1976+ disallowPresetScripts(getCurrentPresetAPI(), name);
17741977 }
17751978
17761979 saveSettingsDebounced();
@@ -1806,6 +2009,12 @@ jQuery(async () => {
18062009 color: enumTypes.name,
18072010 icon: 'S',
18082011 };
2012+ case SCRIPT_TYPES.PRESET:
2013+ return {
2014+ typename: 'preset',
2015+ color: enumTypes.name,
2016+ icon: 'P',
2017+ };
18092018 default:
18102019 return {
18112020 typename: 'Unknown',
@@ -1895,8 +2104,12 @@ jQuery(async () => {
18952104 `,
18962105 }));
18972106
2107+ eventSource.on(event_types.MAIN_API_CHANGED, onMainApiChanged);
18982108 eventSource.on(event_types.CHAT_CHANGED, checkCharEmbeddedRegexScripts);
18992109 eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts);
2110+ eventSource.on(event_types.PRESET_RENAMED_BEFORE, onPresetRenamed);
2111+ eventSource.on(event_types.PRESET_CHANGED, checkPresetEmbeddedRegexScripts);
2112+ eventSource.on(event_types.PRESET_DELETED, purgePresetEmbeddedRegexScripts);
19002113
19012114 presetManager.setupEventListeners();
19022115 presetManager.registerSlashCommands();
public/scripts/extensions/regex/presetEmbeddedScripts.html+5 -0
@@ -0,0 +1,5 @@
1+<div>
2+ <h3 data-i18n="This preset has embedded regex script(s).">This preset has embedded regex script(s).</h3>
3+ <h3 data-i18n="Would you like to allow using them?">Would you like to allow using them?</h3>
4+ <div class="m-b-1" data-i18n="If you want to do it later, select 'Regex' from the extensions menu.">If you want to do it later, select "Regex" from the extensions menu.</div>
5+</div>
public/scripts/extensions/regex/scriptTemplate.html+20 -11
@@ -1,25 +1,34 @@
11<div class="regex-script-label flex-container flexnowrap">
22 <input type="checkbox" class="regex_bulk_checkbox" />
33 <span class="drag-handle menu-handle">&#9776;</span>
44 <div class="regex_script_name flexGrowflex1 overflow-hidden"></div>
55 <div class="flex-container flexnowrap">
66 <label class="checkbox flex-container margin-r5" for="regex_disable">
77 <input type="checkbox" name="regex_disable" class="disable_regex" />
88 <span class="regex-toggle-on fa-solid fa-toggle-on" data-i18n="[title]ext_regex_disable_script" title="Disable script"></span>
99 <span class="regex-toggle-off fa-solid fa-toggle-off" data-i18n="[title]ext_regex_enable_script" title="Enable script"></span>
1010 </label>
11+ <label class="menu_button regex_script_expand" title="Show more options" data-i18n="[title]Show more options">
12+ <input type="checkbox" name="regex_expand" />
13+ <span class="fa-solid fa-ellipsis"></span>
14+ </label>
15+ <div class="flex-container regex_script_buttons">
16+ <div class="move_to_global menu_button" data-i18n="[title]ext_regex_move_to_global" title="Move to global scripts">
17+ <i class="fa-solid fa-globe"></i>
18+ </div>
19+ <div class="move_to_scoped menu_button" data-i18n="[title]ext_regex_move_to_scoped" title="Move to scoped scripts">
20+ <i class="fa-solid fa-address-card"></i>
21+ </div>
22+ <div class="move_to_preset menu_button" data-i18n="[title]ext_regex_move_to_preset" title="Move to preset scripts">
23+ <i class="fa-solid fa-sliders"></i>
24+ </div>
25+ <div class="export_regex menu_button" data-i18n="[title]ext_regex_export_script" title="Export script">
26+ <i class="fa-solid fa-file-export"></i>
27+ </div>
28+ </div>
1129 <div class="edit_existing_regex menu_button" data-i18n="[title]ext_regex_edit_script" title="Edit script">
1230 <i class="fa-solid fa-pencil"></i>
1331 </div>
14- <div class="move_to_global menu_button" data-i18n="[title]ext_regex_move_to_global" title="Move to global scripts">
15- <i class="fa-solid fa-globe"></i>
16- </div>
17- <div class="move_to_scoped menu_button" data-i18n="[title]ext_regex_move_to_scoped" title="Move to scoped scripts">
18- <i class="fa-solid fa-address-card"></i>
19- </div>
20- <div class="export_regex menu_button" data-i18n="[title]ext_regex_export_script" title="Export script">
21- <i class="fa-solid fa-file-export"></i>
22- </div>
2332 <div class="delete_regex menu_button" data-i18n="[title]ext_regex_delete_script" title="Delete script">
2433 <i class="fa-solid fa-trash"></i>
2534 </div>
public/scripts/extensions/regex/style.css+29 -25
@@ -24,39 +24,33 @@
2424 text-align: center;
2525}
2626
2727#scoped_scripts_block {,
28+#preset_scripts_block {
2829 opacity: 1;
2930 transition: opacity var(--animation-duration-2x) ease-in-out;
3031}
3132
3233#scoped_scripts_block .move_to_scoped {,
34+#global_scripts_block .move_to_global,
35+#preset_scripts_block .move_to_preset {
3336 display: none;
3437}
3538
36-#global_scripts_block .move_to_global {
39+#scoped_scripts_block:not(:has(#regex_scoped_toggle:checked)),
37- display: none;
40+#preset_scripts_block:not(:has(#regex_preset_toggle:checked)) {
38-}
39-
40-#scoped_scripts_block:not(:has(#regex_scoped_toggle:checked)) {
4141 opacity: 0.5;
4242}
4343
4444.enable_scoped:checked~.regex-toggle-on {,
45+.enable_scoped:not(:checked)~.regex-toggle-off {
4546 display: block;
4647}
4748
4849.enable_scoped:checked~.regex-toggle-off {,
49- display: none;
50-}
51-
5250.enable_scoped:not(:checked)~.regex-toggle-on {
5351 display: none;
5452}
5553
56-.enable_scoped:not(:checked)~.regex-toggle-off {
57- display: block;
58-}
59-
6054.regex-script-label {
6155 align-items: baseline;
6256 border: 1px solid var(--SmartThemeBorderColor);
@@ -92,19 +86,13 @@ input.enable_scoped {
9286 cursor: pointer;
9387}
9488
9589.disable_regex:checked~.regex-toggle-off {on,
96- display: block;
97-}
98-
99-.disable_regex:checked~.regex-toggle-on {
100- display: none;
101-}
102-
10390.disable_regex:not(:checked)~.regex-toggle-off {
10491 display: none;
10592}
10693
10794.disable_regex:not(:checked)~.regex-toggle-on {,
95+.disable_regex:checked~.regex-toggle-off {
10896 display: block;
10997}
11098
@@ -155,3 +143,19 @@ input.enable_scoped {
155143.regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_checkbox {
156144 display: inline-grid;
157145}
146+
147+@supports not selector(:has(*)) {
148+ .regex-script-label label.regex_script_expand {
149+ display: none;
150+ }
151+
152+ .regex-script-label .regex_script_buttons {
153+ display: flex;
154+ }
155+}
156+
157+.regex-script-label label.regex_script_expand input[name="regex_expand"],
158+.regex-script-label:has(input[name="regex_expand"]:checked) label.regex_script_expand,
159+.regex-script-label:not(:has(input[name="regex_expand"]:checked)) .regex_script_buttons {
160+ display: none;
161+}
public/scripts/preset-manager.js+12 -7
@@ -81,6 +81,9 @@ function autoSelectPreset() {
8181 * @returns {PresetManager} Preset manager
8282 */
8383export function getPresetManager(apiId = '') {
84+ if (apiId === 'koboldhorde') {
85+ apiId = 'kobold';
86+ }
8487 if (!apiId) {
8588 apiId = main_api == 'koboldhorde' ? 'kobold' : main_api;
8689 }
@@ -814,7 +817,7 @@ class PresetManager {
814817 * Reads a preset extension field from the preset.
815818 * @param {object} options
816819 * @param {string} [options.name] Name of the preset. If not provided, uses the currently selected preset name.
817820 * @param {string} options.path Path to the preset extension field, e.g. 'myextension.data'. If empty, reads the entire extensions object.
818821 * @return {any} The value of the preset extension field, or null if not found.
819822 */
820823 readPresetExtensionField({ name, path }) {
@@ -825,7 +828,7 @@ class PresetManager {
825828 // Read from settings if the selected preset is the same as the provided name
826829 if (settings && selectedName === presetName) {
827830 const settingsExtensions = ensurePlainObject(settings.extensions || {});
828831 return path ? lodash.get(settingsExtensions, path, null) : settingsExtensions;
829832 }
830833
831834 // Otherwise, read from the preset by name
@@ -835,7 +838,7 @@ class PresetManager {
835838 }
836839
837840 const presetExtensions = ensurePlainObject(preset.extensions || {});
838841 const value = path ? lodash.get(presetExtensions, path, null) : presetExtensions;
839842 return value;
840843 }
841844
@@ -843,7 +846,7 @@ class PresetManager {
843846 * Writes a value to a preset extension field.
844847 * @param {object} options
845848 * @param {string} [options.name] Name of the preset. If not provided, uses the currently selected preset name.
846849 * @param {string} options.path Path to the preset extension field, e.g. 'myextension.data'. If empty, writes to the root of the extensions object.
847850 * @param {any} options.value Value to write to the preset extension field.
848851 * @return {Promise<void>} Resolves when the preset is saved.
849852 */
@@ -856,7 +859,7 @@ class PresetManager {
856859 if (settings && selectedName === presetName) {
857860 // Set the value at the specified path
858861 settings.extensions = ensurePlainObject(settings.extensions || {});
859862 path ? lodash.set(settings.extensions, path, value) : (settings.extensions = value);
860863 await saveSettings();
861864 }
862865
@@ -868,7 +871,7 @@ class PresetManager {
868871
869872 // Set the value at the specified path
870873 preset.extensions = ensurePlainObject(preset.extensions || {});
871874 path ? lodash.set(preset.extensions, path, value) : (preset.extensions = value);
872875
873876 // Save the updated preset
874877 await this.savePreset(presetName, preset, { skipUpdate: true });
@@ -1033,8 +1036,10 @@ export async function initPresetManager() {
10331036 return;
10341037 }
10351038
1039+ await eventSource.emit(event_types.PRESET_RENAMED_BEFORE, { apiId: apiId, oldName: oldName, newName: newName });
1040+ const extensions = presetManager.readPresetExtensionField({ name: oldName, path: '' });
10361041 await presetManager.renamePreset(newName);
1037-
1042+ await presetManager.writePresetExtensionField({ name: newName, path: '', value: extensions });
10381043 await eventSource.emit(event_types.PRESET_RENAMED, { apiId: apiId, oldName: oldName, newName: newName });
10391044
10401045 if (apiId === 'openai') {