feat: Regex Presets (#4468) * Work in progress * feat: save confirmation * Fix eslint, add typedef * Add slash command and compatibility with profiles * Move UI label up * Null safety * Fix duplicate block title * i18n: zh-CN translation * Adjust debugger styles * Move presets block down * Do not generate id twice for new entries * Add missing translations * Simplify settings migration logic * Fix type comment * Add missing keys --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

57e363cae608e9c59cfbb1648a071a40b8349fd4

Tosd <65720409+Tosd0@users.noreply.github.com>

Signed
7 files changed, +575 -60Ignore whitespace
public/locales/zh-cn.json+69 -46
@@ -379,7 +379,7 @@
379 "Make sure you run it with": "确保您在运行时加上",379 "Make sure you run it with": "确保您在运行时加上",
380 "flag": "标志",380 "flag": "标志",
381 "Custom model (optional)": "自定义模型(可选)",381 "Custom model (optional)": "自定义模型(可选)",
382 "Featherless Model Selection": "Featherless Model Selection",382 "Featherless Model Selection": "Featherless 模型选择",
383 "Search...": "搜索...",383 "Search...": "搜索...",
384 "Search": "搜索",384 "Search": "搜索",
385 "Date Asc": "日期顺序",385 "Date Asc": "日期顺序",
@@ -458,7 +458,7 @@
458 "Context Size": "上下文长度",458 "Context Size": "上下文长度",
459 "Group by vendors": "按厂商分组",459 "Group by vendors": "按厂商分组",
460 "Group by vendors Description": "将 OpenAI 模型放在一组,将 Anthropic 模型放在另一组,等等。可以与排序结合。",460 "Group by vendors Description": "将 OpenAI 模型放在一组,将 Anthropic 模型放在另一组,等等。可以与排序结合。",
461 "To use instruct formatting, switch to OpenRouter under Text Completion API.": "要使用指导格式,请在文本补全 API 下切换到 OpenRouter。",461 "To use instruct formatting, switch to OpenRouter under Text Completion API.": "要使用指导格式,请在 文字补全API 下切换到 OpenRouter。",
462 "AI21 API Key": "AI21 API 密钥",462 "AI21 API Key": "AI21 API 密钥",
463 "AI21 Model": "AI21 模型",463 "AI21 Model": "AI21 模型",
464 "Google AI Studio API Key": "Google AI Studio API 密钥",464 "Google AI Studio API Key": "Google AI Studio API 密钥",
@@ -470,13 +470,13 @@
470 "(Express mode)": "(快速模式)",470 "(Express mode)": "(快速模式)",
471 "API Key": "API 密钥",471 "API Key": "API 密钥",
472 "Project ID": "项目ID:",472 "Project ID": "项目ID:",
473 "Project ID is required when selecting regions other than the default (us-central1). You can find this in a model 404 error message.": "仅当选择非默认区域(us-central1)时才需要`项目ID`。\n 您可以在模型 404 错误消息中找到它。",473 "Project ID is required when selecting regions other than the default (us-central1). You can find this in a model 404 error message.": "仅当选择非默认区域(us-central1)时才需要`项目ID`。\n 您可以在模型 404 错误消息中找到它。",
474 "Service Account Configuration": "服务帐户配置",474 "Service Account Configuration": "服务帐户配置",
475 "Service Account JSON Content": "服务帐户 JSON 内容:",475 "Service Account JSON Content": "服务帐户 JSON 内容:",
476 "For privacy reasons, your Service Account JSON content will be hidden after you click 'Validate JSON'.": "出于隐私原因,您的服务帐户 JSON 内容将在您点击“验证 JSON”后隐藏。",476 "For privacy reasons, your Service Account JSON content will be hidden after you click 'Validate JSON'.": "出于隐私考虑,你的服务账号 JSON 内容将在点击“验证JSON”后隐藏。",
477 "Validate JSON": "验证JSON",477 "Validate JSON": "验证JSON",
478 "Region": "区域:",478 "Region": "地区:",
479 "View available regions and models": "查看可用区域和模型",479 "View available regions and models": "查看可用地区和模型",
480 "MistralAI API Key": "MistralAI API 密钥",480 "MistralAI API Key": "MistralAI API 密钥",
481 "MistralAI Model": "MistralAI 模型",481 "MistralAI Model": "MistralAI 模型",
482 "Groq API Key": "Groq API 密钥",482 "Groq API Key": "Groq API 密钥",
@@ -580,7 +580,7 @@
580 "Skip Example Dialogues Formatting": "跳过示例对话格式化",580 "Skip Example Dialogues Formatting": "跳过示例对话格式化",
581 "Include Names": "包括名称",581 "Include Names": "包括名称",
582 "Never": "永不",582 "Never": "永不",
583 "Groups and Past Personas": "群聊和过去的用户角色",583 "Groups and Past Personas": "群聊和过去的用户设定",
584 "Always": "永远",584 "Always": "永远",
585 "Instruct Sequences": "指令序列",585 "Instruct Sequences": "指令序列",
586 "Story String Sequences": "故事字符串序列",586 "Story String Sequences": "故事字符串序列",
@@ -635,7 +635,7 @@
635 "Replace Macro in Stop Strings": "替换自定义停止字符串中的宏",635 "Replace Macro in Stop Strings": "替换自定义停止字符串中的宏",
636 "Token Padding": "词符填充",636 "Token Padding": "词符填充",
637 "Reasoning": "推理",637 "Reasoning": "推理",
638 "reasoning_auto_parse": "自动从主要内容中解析位于推理前缀/后缀之间的推理块。两个字段都必须定义且非空。",638 "reasoning_auto_parse": "自动从消息内容中提取由推理块前后缀包裹的推理块。前后缀都必须设置且不为空。",
639 "Auto-Parse": "自动解析",639 "Auto-Parse": "自动解析",
640 "reasoning_auto_expand": "自动展开推理内容块。",640 "reasoning_auto_expand": "自动展开推理内容块。",
641 "Auto-Expand": "自动展开",641 "Auto-Expand": "自动展开",
@@ -643,7 +643,7 @@
643 "Show Hidden": "显示隐藏内容",643 "Show Hidden": "显示隐藏内容",
644 "reasoning_add_to_prompts": "将已有的推理块添加到提示词。若需新增一个推理块,请使用消息编辑菜单。",644 "reasoning_add_to_prompts": "将已有的推理块添加到提示词。若需新增一个推理块,请使用消息编辑菜单。",
645 "Add to Prompts": "添加到提示词",645 "Add to Prompts": "添加到提示词",
646 "reasoning_max_additions": "每个提示词最多可添加的推理块数量,从最后一条消息开始计算。",646 "reasoning_max_additions": "从最后一条消息开始,最多有多少个思维链可以被加入提示词。",
647 "Max": "最大值",647 "Max": "最大值",
648 "Reasoning Formatting": "推理内容格式化",648 "Reasoning Formatting": "推理内容格式化",
649 "Select your current Reasoning Template": "选择你当前的推理模板",649 "Select your current Reasoning Template": "选择你当前的推理模板",
@@ -708,8 +708,8 @@
708 "Apply current sorting as Order": "应用当前排序作为顺序",708 "Apply current sorting as Order": "应用当前排序作为顺序",
709 "Priority": "优先级",709 "Priority": "优先级",
710 "Custom": "自定义",710 "Custom": "自定义",
711 "Title A-Z": "标题 A-Z",711 "Title A-Z": "标题 A 到 Z",
712 "Title Z-A": "标题 Z-A",712 "Title Z-A": "标题 Z 到 A",
713 "Tokens ↗": "词符 ↗",713 "Tokens ↗": "词符 ↗",
714 "Tokens ↘": "词符 ↘",714 "Tokens ↘": "词符 ↘",
715 "Depth ↗": "深度 ↗",715 "Depth ↗": "深度 ↗",
@@ -956,8 +956,7 @@
956 "stscript_parser_flag_replace_getvar_label": "防止 {{getvar::}} {{getglobalvar::}} 宏具有自动评估的文字宏类值。\n例如,“{{newline}}”保留为文字字符串“{{newline}}”\n\n(这是通过在内部用范围变量替换 {{getvar::}} {{getglobalvar::}} 宏来实现的。)",956 "stscript_parser_flag_replace_getvar_label": "防止 {{getvar::}} {{getglobalvar::}} 宏具有自动评估的文字宏类值。\n例如,“{{newline}}”保留为文字字符串“{{newline}}”\n\n(这是通过在内部用范围变量替换 {{getvar::}} {{getglobalvar::}} 宏来实现的。)",
957 "REPLACE_GETVAR": "替换GETVAR",957 "REPLACE_GETVAR": "替换GETVAR",
958 "Change Background Image": "更改背景图片",958 "Change Background Image": "更改背景图片",
959 "Background Image": "背景图片",959 "Backgrounds": "背景",
960 "Filter": "搜索",
961 "Background Fitting": "背景图片尺寸",960 "Background Fitting": "背景图片尺寸",
962 "Classic": "经典",961 "Classic": "经典",
963 "Cover": "填充",962 "Cover": "填充",
@@ -966,6 +965,7 @@
966 "Center": "居中",965 "Center": "居中",
967 "Automatically select a background based on the chat context": "根据聊天上下文自动选择背景",966 "Automatically select a background based on the chat context": "根据聊天上下文自动选择背景",
968 "Auto-select": "自动选择",967 "Auto-select": "自动选择",
968 "Add Background": "添加背景",
969 "System Backgrounds": "系统背景",969 "System Backgrounds": "系统背景",
970 "Chat Backgrounds": "聊天背景",970 "Chat Backgrounds": "聊天背景",
971 "bg_chat_hint_1": "使用生成的聊天背景",971 "bg_chat_hint_1": "使用生成的聊天背景",
@@ -973,7 +973,7 @@
973 "Extensions": "扩展",973 "Extensions": "扩展",
974 "Notify on extension updates": "在扩展更新时通知",974 "Notify on extension updates": "在扩展更新时通知",
975 "Manage extensions": "管理扩展",975 "Manage extensions": "管理扩展",
976 "Import Extension From Git Repo": "从Git存储库导入扩展",976 "Import Extension From Git Repo": "从 Git 仓库导入扩展",
977 "Install extension": "安装扩展",977 "Install extension": "安装扩展",
978 "(DEPRECATED)": "(已弃用)",978 "(DEPRECATED)": "(已弃用)",
979 "Extras API:": "扩展API:",979 "Extras API:": "扩展API:",
@@ -985,7 +985,7 @@
985 "Usage Stats": "使用统计",985 "Usage Stats": "使用统计",
986 "Backup your personas to a file": "将用户设定备份到文件中",986 "Backup your personas to a file": "将用户设定备份到文件中",
987 "Backup": "备份",987 "Backup": "备份",
988 "Restore your personas from a file": "从文件中恢复用户角色",988 "Restore your personas from a file": "从文件中恢复用户设定",
989 "Restore": "恢复",989 "Restore": "恢复",
990 "Create a dummy persona": "创建空白用户设定",990 "Create a dummy persona": "创建空白用户设定",
991 "Create": "创建",991 "Create": "创建",
@@ -1117,7 +1117,7 @@
1117 "Bulk select all characters": "批量选择所有角色",1117 "Bulk select all characters": "批量选择所有角色",
1118 "Bulk delete characters": "批量删除角色",1118 "Bulk delete characters": "批量删除角色",
1119 "Persona is locked to the current chat": "用户设定已被绑定到当前聊天",1119 "Persona is locked to the current chat": "用户设定已被绑定到当前聊天",
1120 "Persona is locked to the current character": "用户角色已被绑定到当前角色",1120 "Persona is locked to the current character": "用户设定已被绑定到当前角色",
1121 "popup-button-save": "保存",1121 "popup-button-save": "保存",
1122 "popup-button-yes": "是",1122 "popup-button-yes": "是",
1123 "popup-button-no": "否",1123 "popup-button-no": "否",
@@ -1163,10 +1163,10 @@
1163 "Chat History": "聊天记录",1163 "Chat History": "聊天记录",
1164 "Import Chat": "导入聊天",1164 "Import Chat": "导入聊天",
1165 "Copy to system backgrounds": "复制到系统背景",1165 "Copy to system backgrounds": "复制到系统背景",
1166 "Rename background": "重命名背景",
1167 "Lock": "锁定",1166 "Lock": "锁定",
1168 "Unlock": "解锁",1167 "Unlock": "解锁",
1169 "Delete background": "删除背景",1168 "Rename Background": "重命名背景",
1169 "Delete Background": "删除背景",
1170 "Select a World Info file for": "选择一个世界书文件给",1170 "Select a World Info file for": "选择一个世界书文件给",
1171 "Primary Lorebook": "主要知识书",1171 "Primary Lorebook": "主要知识书",
1172 "A selected World Info will be bound to this character as its own Lorebook.": "所选的世界信息将会于该角色绑定,作为该角色自己的知识书",1172 "A selected World Info will be bound to this character as its own Lorebook.": "所选的世界信息将会于该角色绑定,作为该角色自己的知识书",
@@ -1202,14 +1202,15 @@
1202 "Use global": "使用全局",1202 "Use global": "使用全局",
1203 "Yes": "是",1203 "Yes": "是",
1204 "No": "否",1204 "No": "否",
1205 "Whole Words": "Whole Words",1205 "Whole Words": "完整单词",
1206 "Group Scoring": "Group Scoring",1206 "Group Scoring": "组评分",
1207 "Can be used to automatically activate Quick Replies": "可用于自动激活快速回复",1207 "Can be used to automatically activate Quick Replies": "可用于自动激活快速回复",
1208 "Automation ID": "自动化ID",1208 "Automation ID": "自动化ID",
1209 "( None )": "(没有任何)",1209 "( None )": "(没有任何)",
1210 "delay_until_recursion_level": "Defines delay levels for recursive scans.\r\rInitially, only the first level (smallest number) will match.\rOnce no matches are found, the next level becomes eligible for matching.\rThis repeats until all levels are checked.\r\rTied to the \"Delay until recursion\" setting.",1210 "delay_until_recursion_level": "定义递归扫描的延迟级别。\n最初,只有第一级(最小数字)会匹配。\n一旦未找到匹配项,下一个级别将变为匹配的候选项。\n这将重复,直到检查所有级别。\n与“延迟到递归”设置相关联。",
1211 "Recursion Level": "递归等级",1211 "Recursion Level": "递归等级",
1212 "Content": "内容",1212 "Content": "内容",
1213 "This entry will not be recursively activated by other entries.": "此条目不会被其他条目递归激活。",
1213 "Non-recursable": "不可递归(不会被其他条目激活)",1214 "Non-recursable": "不可递归(不会被其他条目激活)",
1214 "This entry will not activate other entries recursively.": "此条目不会被其他条目激活。",1215 "This entry will not activate other entries recursively.": "此条目不会被其他条目激活。",
1215 "Prevent further recursion": "防止进一步递归",1216 "Prevent further recursion": "防止进一步递归",
@@ -1290,7 +1291,7 @@
1290 "prompt_manager_depth": "深度",1291 "prompt_manager_depth": "深度",
1291 "0 = after the last message, 1 = before the last message, etc.": "“0”为在最后一条消息之后,“1”为在最后一条消息之前,等等。",1292 "0 = after the last message, 1 = before the last message, etc.": "“0”为在最后一条消息之后,“1”为在最后一条消息之前,等等。",
1292 "prompt_manager_order": "排序",1293 "prompt_manager_order": "排序",
1293 "prompt_manager_order_note": "来自其他来源(世界信息、作者注释等)的提示注入的默认顺序始终为 100。",1294 "prompt_manager_order_note": "来自其他来源(世界信息、作者注释等)的提示注入的默认顺序为 100。",
1294 "Ordered from low/top to high/bottom, and at same order: Assistant, User, System.": "从低/顶到高/底排序,并按相同顺序:助手、用户、系统。",1295 "Ordered from low/top to high/bottom, and at same order: Assistant, User, System.": "从低/顶到高/底排序,并按相同顺序:助手、用户、系统。",
1295 "The content of this prompt is pulled from elsewhere and cannot be edited here.": "此提示词的内容是从其他地方提取的,无法在此处进行编辑。",1296 "The content of this prompt is pulled from elsewhere and cannot be edited here.": "此提示词的内容是从其他地方提取的,无法在此处进行编辑。",
1296 "Prompt": "提示词",1297 "Prompt": "提示词",
@@ -1344,8 +1345,8 @@
1344 "onboarding_import": "导入",1345 "onboarding_import": "导入",
1345 "from supported sources or view": "来自受支持的来源或查看",1346 "from supported sources or view": "来自受支持的来源或查看",
1346 "Sample characters": "示例角色",1347 "Sample characters": "示例角色",
1347 "Your Persona": "您的用户角色",1348 "Your Persona": "您的用户设定",
1348 "Before you get started, you must select a persona name.": "在开始之前,您必须选择一个用户设定名称。",1349 "Before you get started, you must select a persona name.": "在开始之前,您必须起一个用户名。",
1349 "welcome_message_part_8": "您可随时通过",1350 "welcome_message_part_8": "您可随时通过",
1350 "welcome_message_part_9": "图标来更改此设置。",1351 "welcome_message_part_9": "图标来更改此设置。",
1351 "Persona Name:": "用户设定名称:",1352 "Persona Name:": "用户设定名称:",
@@ -1486,6 +1487,8 @@
1486 "Model": "模型",1487 "Model": "模型",
1487 "currently_selected": "[当前选定]",1488 "currently_selected": "[当前选定]",
1488 "currently_loaded": "[当前正在加载]",1489 "currently_loaded": "[当前正在加载]",
1490 "Custom Model Tag": "自定义模型标签",
1491 "(for [Custom model] option)": "(自定义模型选项用)",
1489 "Allow reverse proxy": "允许反向代理",1492 "Allow reverse proxy": "允许反向代理",
1490 "Hint:": "提示:",1493 "Hint:": "提示:",
1491 "Set your API keys and endpoints in the 'API Connections' tab first.": "首先在“API 连接”选项卡中设置您的 API 密钥和端点。",1494 "Set your API keys and endpoints in the 'API Connections' tab first.": "首先在“API 连接”选项卡中设置您的 API 密钥和端点。",
@@ -1523,7 +1526,7 @@
1523 "Classifier API": "分类器 API",1526 "Classifier API": "分类器 API",
1524 "Select the API for classifying expressions.": "选择用于对表达式进行分类的API。",1527 "Select the API for classifying expressions.": "选择用于对表达式进行分类的API。",
1525 "Main API": "当前连接的 API",1528 "Main API": "当前连接的 API",
1526 "WebLLM Extension": "WebLLM Extension",1529 "WebLLM Extension": "WebLLM 扩展程序",
1527 "When using LLM or WebLLM classifier, only show and use expressions that have sprites assigned to them.": "When using LLM or WebLLM classifier, only show and use expressions that have sprites assigned to them.",1530 "When using LLM or WebLLM classifier, only show and use expressions that have sprites assigned to them.": "When using LLM or WebLLM classifier, only show and use expressions that have sprites assigned to them.",
1528 "Filter expressions for available sprites": "Filter expressions for available sprites",1531 "Filter expressions for available sprites": "Filter expressions for available sprites",
1529 "LLM Prompt": "大语言模型提示词",1532 "LLM Prompt": "大语言模型提示词",
@@ -1556,29 +1559,29 @@
1556 "ext_sum_with": "总结如下:",1559 "ext_sum_with": "总结如下:",
1557 "ext_sum_main_api": "主要 API",1560 "ext_sum_main_api": "主要 API",
1558 "ext_sum_webllm": "WebLLM 扩展",1561 "ext_sum_webllm": "WebLLM 扩展",
1559 "ext_sum_current_summary": "当前摘要:",1562 "ext_sum_current_summary": "当前总结:",
1560 "ext_sum_restore_tip": "恢复先前的摘要;重复使用以清除此聊天的摘要状态",1563 "ext_sum_restore_tip": "恢复先前的总结;重复使用以清除此聊天的总结状态",
1561 "ext_sum_restore_previous": "恢复上一个",1564 "ext_sum_restore_previous": "恢复上一个",
1562 "ext_sum_memory_placeholder": "摘要将在这里生成...",1565 "ext_sum_memory_placeholder": "总结将在这里生成...",
1563 "ext_sum_force_tip": "立即触发摘要更新。",1566 "ext_sum_force_tip": "立即触发总结。",
1564 "ext_sum_force_text": "现在总结",1567 "ext_sum_force_text": "立即总结",
1565 "Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API).": "禁用自动摘要更新。暂停时,摘要保持原样。您仍然可以通过按“立即汇总”按钮(仅适用于主 API)强制更新。",1568 "Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API).": "禁用自动总结。暂停时,总结保持原样。您仍然可以通过按“立即总结”按钮(仅适用于主 API)强制更新。",
1566 "ext_sum_pause": "暂停",1569 "ext_sum_pause": "暂停",
1567 "Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN.": "从要总结的文本中省略世界信息和作者注释。仅在使用主 API 时有效。附加 API 始终省略世界书/作者注。",1570 "Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN.": "从要总结的文本中省略世界信息和作者注释。仅在使用主 API 时有效。附加 API 始终省略世界书/作者注。",
1568 "ext_sum_no_wi_an": "无世界书/作者注",1571 "ext_sum_no_wi_an": "无世界书/作者注",
1569 "ext_sum_settings_tip": "编辑摘要提示词、插入位置等。",1572 "ext_sum_settings_tip": "编辑总结提示词、插入位置等。",
1570 "ext_sum_settings": "摘要设置",1573 "ext_sum_settings": "总结设置",
1571 "ext_sum_prompt_builder": "提示词生成器",1574 "ext_sum_prompt_builder": "提示词生成器",
1572 "ext_sum_prompt_builder_1_desc": "扩展将使用尚未汇总的消息构建自己的提示词。阻止聊天,直到生成摘要为止。",1575 "ext_sum_prompt_builder_1_desc": "将使用尚未被总结的消息构建提示词,在总结生成前暂停聊天。",
1573 "ext_sum_prompt_builder_1": "原始,阻塞",1576 "ext_sum_prompt_builder_1": "原始,阻塞",
1574 "ext_sum_prompt_builder_2_desc": "扩展将使用尚未汇总的消息构建自己的提示词。在生成摘要时不会阻止聊天。并非所有后端都支持此模式。",1577 "ext_sum_prompt_builder_2_desc": "将使用尚未被总结的消息构建提示词。在生成总结时不会暂停聊天。并非所有后端都支持此模式。",
1575 "ext_sum_prompt_builder_2": "原始,非阻塞",1578 "ext_sum_prompt_builder_2": "原始,非阻塞",
1576 "ext_sum_prompt_builder_3_desc": "扩展将使用常规主提示词生成器并将摘要请求添加为其作为最后的系统消息。",1579 "ext_sum_prompt_builder_3_desc": "将使用常规主提示词生成器,并将总结请求设为系统消息添加到提示词最后。",
1577 "ext_sum_prompt_builder_3": "经典,阻塞",1580 "ext_sum_prompt_builder_3": "经典,阻塞",
1578 "Summary Prompt": "摘要提示词",1581 "Summary Prompt": "总结提示词",
1579 "ext_sum_restore_default_prompt_tip": "恢复默认提示词",1582 "ext_sum_restore_default_prompt_tip": "恢复默认提示词",
1580 "ext_sum_prompt_placeholder": "该提示词将被发送给 AI,以请求生成摘要。{{words}} 将解析为“字数”参数。",1583 "ext_sum_prompt_placeholder": "该提示词将被发送给 AI,以请求生成总结。{{words}} 将解析为“字数”参数。",
1581 "ext_sum_target_length_1": "目标摘要长度",1584 "ext_sum_target_length_1": "目标总结长度",
1582 "ext_sum_target_length_2": "(",1585 "ext_sum_target_length_2": "(",
1583 "ext_sum_target_length_3": "字)",1586 "ext_sum_target_length_3": "字)",
1584 "ext_sum_api_response_length_1": "API 响应长度",1587 "ext_sum_api_response_length_1": "API 响应长度",
@@ -1598,10 +1601,10 @@
1598 "ext_sum_injection_template": "插入模板",1601 "ext_sum_injection_template": "插入模板",
1599 "ext_sum_memory_template_placeholder": "{{summary}} 将解析当前摘要内容。",1602 "ext_sum_memory_template_placeholder": "{{summary}} 将解析当前摘要内容。",
1600 "ext_sum_injection_position": "插入位置",1603 "ext_sum_injection_position": "插入位置",
1601 "ext_sum_include_wi_scan_desc": "在 WI 扫描中包括最新摘要。",1604 "ext_sum_include_wi_scan_desc": "在世界信息扫描中加入最新总结。",
1602 "ext_sum_include_wi_scan": "纳入世界信息扫描",1605 "ext_sum_include_wi_scan": "纳入世界信息扫描",
1603 "None (not injected)": "无(未注入)",1606 "None (not injected)": "无(未注入)",
1604 "ext_sum_injection_position_none": "摘要不会被注入到提示中。您仍然可以通过 {{summary}} 宏访问它。",1607 "ext_sum_injection_position_none": "总结不会被注入到提示中。您仍然可以通过 {{summary}} 宏访问它。",
1605 "How many messages before the current end of the chat.": "当前聊天结束前还有多少条消息。",1608 "How many messages before the current end of the chat.": "当前聊天结束前还有多少条消息。",
1606 "Labels and Message": "标签和信息",1609 "Labels and Message": "标签和信息",
1607 "Label": "标签",1610 "Label": "标签",
@@ -1643,13 +1646,32 @@
1643 "macro for manual injection)": "宏用于手动注入)",1646 "macro for manual injection)": "宏用于手动注入)",
1644 "Color": "颜色",1647 "Color": "颜色",
1645 "Only apply color as accent": "仅应用颜色作为强调",1648 "Only apply color as accent": "仅应用颜色作为强调",
1649 "ext_regex_new_global_script_desc": "新增「全局」正规表达式",
1650 "ext_regex_new_scoped_script_desc": "新增「局部」正规表达式",
1651 "ext_regex_debugger_active_rules": "激活的规则",
1652 "ext_regex_debugger_save_order": "保存此顺序",
1653 "ext_regex_debugger_testing_area": "测试区域",
1654 "ext_regex_debugger_raw_input": "原始输入",
1655 "ext_regex_debugger_run_test": "运行测试",
1656 "ext_regex_debugger_display_replace": "替换",
1657 "ext_regex_debugger_display_highlight": "高亮",
1658 "ext_regex_debugger_render_text": "渲染为文本",
1659 "ext_regex_debugger_render_message": "渲染为消息",
1660 "ext_regex_debugger_step_by_step": "逐步转换",
1661 "ext_regex_debugger_final_output": "最终输出",
1646 "ext_regex_title": "正则",1662 "ext_regex_title": "正则",
1647 "ext_regex_new_global_script_desc": "新的全局正则表达式脚本",1663 "ext_regex_presets": "正则预设",
1664 "ext_regex_presets_desc": "可以轻松保存并切换多组正则开关状态。",
1665 "ext_regex_preset_create": "创建新预设",
1666 "ext_regex_preset_update": "更新已有预设",
1667 "ext_regex_preset_apply": "重新应用当前预设",
1668 "ext_regex_preset_delete": "删除当前预设",
1648 "ext_regex_new_global_script": "新建全局正则",1669 "ext_regex_new_global_script": "新建全局正则",
1649 "ext_regex_new_scoped_script_desc": "新的作用域正则表达式脚本",
1650 "ext_regex_new_scoped_script": "新建局部正则",1670 "ext_regex_new_scoped_script": "新建局部正则",
1651 "ext_regex_import_script": "导入正则",1671 "ext_regex_import_script": "导入正则",
1652 "ext_regex_bulk_edit": "批量编辑",1672 "ext_regex_bulk_edit": "批量编辑",
1673 "ext_regex_debugger_desc": "高级正则调试工具",
1674 "ext_regex_debugger": "调试工具",
1653 "Export": "导出",1675 "Export": "导出",
1654 "ext_regex_global_scripts": "全局正则脚本",1676 "ext_regex_global_scripts": "全局正则脚本",
1655 "ext_regex_global_scripts_desc": "影响所有角色,保存在本地设定中",1677 "ext_regex_global_scripts_desc": "影响所有角色,保存在本地设定中",
@@ -1766,6 +1788,7 @@
1766 "Enhance": "提高",1788 "Enhance": "提高",
1767 "You can find your API key in the Stability AI dashboard.": "您可以在 Stability AI 仪表板中找到您的 API 密钥。",1789 "You can find your API key in the Stability AI dashboard.": "您可以在 Stability AI 仪表板中找到您的 API 密钥。",
1768 "Style Preset": "风格预设",1790 "Style Preset": "风格预设",
1791 "Whether to perform upsampling on the prompt. If active, automatically modifies the prompt for more creative generation.": "是否对提示词使用提示词增强(Upsampling)。若开启,则会自动修改提示词,使回复更有创造力。",
1769 "Prompt Upsampling": "提示词增强(Upsampling)",1792 "Prompt Upsampling": "提示词增强(Upsampling)",
1770 "Sampling method": "采样方法",1793 "Sampling method": "采样方法",
1771 "Scheduler": "调度器",1794 "Scheduler": "调度器",
@@ -2105,8 +2128,8 @@
2105 "help_macros_12": "您当前的角色描述",2128 "help_macros_12": "您当前的角色描述",
2106 "help_macros_13": "角色对话示例",2129 "help_macros_13": "角色对话示例",
2107 "help_macros_14": "未格式化的对话示例",2130 "help_macros_14": "未格式化的对话示例",
2108 "help_macros_summary": "“Summarize”扩展生成的最新聊天摘要(如果有)。",2131 "help_macros_summary": "“总结”扩展程序生成的最新聊天总结(如果有)。",
2109 "help_macros_15": "您当前的用户角色名称",2132 "help_macros_15": "您当前的用户设定名称",
2110 "help_macros_16": "角色的名字",2133 "help_macros_16": "角色的名字",
2111 "help_macros_17": "角色的版本号",2134 "help_macros_17": "角色的版本号",
2112 "help_macros_charDepthPrompt": "角色的 @ 深度注释",2135 "help_macros_charDepthPrompt": "角色的 @ 深度注释",
@@ -2263,7 +2286,7 @@
2263 "Reset Settings": "重置设置",2286 "Reset Settings": "重置设置",
2264 "Wipe all user data and reset your account to factory settings.": "删除所有用户数据并将您的账号重置为默认设置。",2287 "Wipe all user data and reset your account to factory settings.": "删除所有用户数据并将您的账号重置为默认设置。",
2265 "Reset Everything": "重置一切",2288 "Reset Everything": "重置一切",
2266 "This will delete all your settings and data. There will be no undo button. Make sure you have a backup before proceeding.": "这将删除您的所有设置和数据。没有撤销按钮。在继续之前请确保您已有备份。",2289 "This will delete all your settings and data. There will be no undo button. Make sure you have a backup before proceeding.": "这将删除您所有的设置和数据,不可撤销。请确保您已备份数据。",
2267 "Account reset code has been posted to the server console.": "账户重置代码已发布到服务器控制台。",2290 "Account reset code has been posted to the server console.": "账户重置代码已发布到服务器控制台。",
2268 "Reset Code:": "重置代码:",2291 "Reset Code:": "重置代码:",
2269 "Want to update?": "获取最新版本",2292 "Want to update?": "获取最新版本",
public/scripts/extensions.js+2 -0
@@ -188,6 +188,8 @@ export const extension_settings = {
188 dice: {},188 dice: {},
189 /** @type {import('./char-data.js').RegexScriptData[]} */189 /** @type {import('./char-data.js').RegexScriptData[]} */
190 regex: [],190 regex: [],
191 /** @type {import('./extensions/regex/index.js').RegexPreset[]} */
192 regex_presets: [],
191 character_allowed_regex: [],193 character_allowed_regex: [],
192 tts: {},194 tts: {},
193 sd: {195 sd: {
public/scripts/extensions/connection-manager/index.js+11 -0
@@ -43,6 +43,7 @@ const CC_COMMANDS = [
43 'reasoning-template',43 'reasoning-template',
44 'prompt-post-processing',44 'prompt-post-processing',
45 'secret-id',45 'secret-id',
46 'regex-preset',
46];47];
4748
48const TC_COMMANDS = [49const TC_COMMANDS = [
@@ -60,6 +61,7 @@ const TC_COMMANDS = [
60 'start-reply-with',61 'start-reply-with',
61 'reasoning-template',62 'reasoning-template',
62 'secret-id',63 'secret-id',
64 'regex-preset',
63];65];
6466
65const FANCY_NAMES = {67const FANCY_NAMES = {
@@ -79,6 +81,7 @@ const FANCY_NAMES = {
79 'reasoning-template': 'Reasoning Template',81 'reasoning-template': 'Reasoning Template',
80 'prompt-post-processing': 'Prompt Post-Processing',82 'prompt-post-processing': 'Prompt Post-Processing',
81 'secret-id': 'Secret',83 'secret-id': 'Secret',
84 'regex-preset': 'Regex Preset',
82};85};
8386
84/**87/**
@@ -357,6 +360,14 @@ function makeFancyProfile(profile) {
357 }360 }
358 }361 }
359362
363 if (key === 'regex-preset') {
364 const label = extension_settings.regex_presets?.find(p => p.id === profile[key])?.name;
365 if (label) {
366 acc[value] = label;
367 return acc;
368 }
369 }
370
360 acc[value] = profile[key];371 acc[value] = profile[key];
361 return acc;372 return acc;
362 }, {});373 }, {});
public/scripts/extensions/regex/debugger.css+8 -0
@@ -104,6 +104,7 @@
104 background-color: var(--black30a);104 background-color: var(--black30a);
105 white-space: pre-wrap;105 white-space: pre-wrap;
106 word-break: break-word;106 word-break: break-word;
107 text-align: left;
107}108}
108109
109.step-header {110.step-header {
@@ -117,6 +118,7 @@
117 padding: 5px;118 padding: 5px;
118 background-color: var(--black30a);119 background-color: var(--black30a);
119 border-radius: 5px;120 border-radius: 5px;
121 text-align: left;
120}122}
121123
122/* Classes to replace inline styles */124/* Classes to replace inline styles */
@@ -253,3 +255,9 @@ mark.red_hl {
253 padding-right: 20px;255 padding-right: 20px;
254 margin-top: 5px;256 margin-top: 5px;
255}257}
258
259.regex-popup-content {
260 white-space: pre-wrap;
261 word-break: break-all;
262 text-align: left;
263}
public/scripts/extensions/regex/dropdown.html+16 -0
@@ -53,6 +53,22 @@
53 </div>53 </div>
54 </div>54 </div>
55 <hr />55 <hr />
56 <div id="regex_presets_block">
57 <div class="flex-container alignItemsBaseline">
58 <strong class="flex1" data-i18n="ext_regex_presets">Regex Presets</strong>
59 </div>
60 <small data-i18n="ext_regex_presets_desc">
61 Save and switch between groups of enabled regex scripts.
62 </small>
63 <div class="flex-container marginTop5">
64 <select id="regex_presets" class="text_pole flex1"></select>
65 <div id="regex_preset_create" class="menu_button fa-solid fa-file-circle-plus" data-i18n="[title]ext_regex_preset_create" title="Create a new regex preset"></div>
66 <div id="regex_preset_update" class="menu_button fa-solid fa-save" data-i18n="[title]ext_regex_preset_update" title="Update existing regex preset"></div>
67 <div id="regex_preset_apply" class="menu_button fa-solid fa-recycle" data-i18n="[title]ext_regex_preset_apply" title="Re-apply current preset"></div>
68 <div id="regex_preset_delete" class="menu_button fa-solid fa-trash" data-i18n="[title]ext_regex_preset_delete" title="Delete current preset"></div>
69 </div>
70 </div>
71 <hr />
56 <div id="global_scripts_block" class="padding5">72 <div id="global_scripts_block" class="padding5">
57 <div>73 <div>
58 <strong data-i18n="ext_regex_global_scripts">Global Scripts</strong>74 <strong data-i18n="ext_regex_global_scripts">Global Scripts</strong>
public/scripts/extensions/regex/index.js+461 -6
@@ -1,7 +1,7 @@
1import { characters, eventSource, event_types, getCurrentChatId, messageFormatting, reloadCurrentChat, saveSettingsDebounced, this_chid } from '../../../script.js';1import { characters, eventSource, event_types, getCurrentChatId, messageFormatting, reloadCurrentChat, saveSettingsDebounced, this_chid } from '../../../script.js';
2import { extension_settings, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js';2import { extension_settings, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js';
3import { selected_group } from '../../group-chats.js';3import { selected_group } from '../../group-chats.js';
4import { callGenericPopup, POPUP_TYPE } from '../../popup.js';4import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';
5import { SlashCommand } from '../../slash-commands/SlashCommand.js';5import { SlashCommand } from '../../slash-commands/SlashCommand.js';
6import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';6import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
7import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';7import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
@@ -19,6 +19,452 @@ const sanitizeFileName = name => name.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, '
19 */19 */
2020
21/**21/**
22 * @typedef {object} RegexPresetItem
23 * @property {string} id - UUID of the regex script
24 */
25
26/**
27 * @typedef {object} RegexPreset
28 * @property {string} id - UUID of the preset
29 * @property {string} name - Name of the preset
30 * @property {boolean} isSelected - Whether the preset is currently selected
31 * @property {RegexPresetItem[]} global - The list of global preset items
32 * @property {RegexPresetItem[]} scoped - The list of scoped preset items
33 */
34
35/**
36 * @typedef {object} RegexPresetState
37 * @property {string[]} global - List of enabled global regex script IDs
38 * @property {string[]} scoped - List of enabled scoped regex script IDs
39 */
40
41class RegexPresetManager {
42 /** @type {HTMLSelectElement} */
43 presetSelect = null;
44
45 /** @type {HTMLElement} */
46 presetCreateButton = null;
47
48 /** @type {HTMLElement} */
49 presetUpdateButton = null;
50
51 /** @type {HTMLElement} */
52 presetApplyButton = null;
53
54 /** @type {HTMLElement} */
55 presetDeleteButton = null;
56
57 /** @type {string|null} */
58 currentPresetId = null;
59
60 /** @type {RegexPresetState|null} */
61 lastKnownState = null;
62
63 /**
64 * Captures the current state of enabled regex scripts for change detection.
65 * @returns {RegexPresetState} The current state object
66 */
67 captureCurrentState() {
68 const globalScripts = this.regexListToPresetItems(extension_settings.regex) || [];
69 const scopedScripts = this.regexListToPresetItems(characters[this_chid]?.data?.extensions?.regex_scripts) || [];
70
71 return {
72 global: globalScripts.map(item => item.id).sort(),
73 scoped: scopedScripts.map(item => item.id).sort(),
74 };
75 }
76
77 /**
78 * Compares two state objects to detect changes.
79 * @param {RegexPresetState} state1 First state object
80 * @param {RegexPresetState} state2 Second state object
81 * @returns {boolean} True if states are different
82 */
83 hasStateChanged(state1, state2) {
84 if (!state1 || !state2) return false;
85
86 const global1 = state1.global || [];
87 const global2 = state2.global || [];
88 const scoped1 = state1.scoped || [];
89 const scoped2 = state2.scoped || [];
90
91 if (global1.length !== global2.length || scoped1.length !== scoped2.length) {
92 return true;
93 }
94
95 return !global1.every(id => global2.includes(id)) ||
96 !scoped1.every(id => scoped2.includes(id));
97 }
98
99 /**
100 * Updates the stored state after a preset is applied or saved.
101 * @param {string} presetId - The current preset ID
102 */
103 updateStoredState(presetId) {
104 this.currentPresetId = presetId;
105 this.lastKnownState = this.captureCurrentState();
106 }
107
108 /**
109 * Checks if there are unsaved changes and shows a confirmation dialog.
110 * @returns {Promise<boolean>} True if user wants to proceed without saving
111 */
112 async checkUnsavedChanges() {
113 if (!this.currentPresetId || !this.lastKnownState) {
114 return true; // No current preset or state to compare
115 }
116
117 const currentState = this.captureCurrentState();
118 if (!this.hasStateChanged(this.lastKnownState, currentState)) {
119 return true; // No changes detected
120 }
121
122 const currentPreset = extension_settings.regex_presets.find(p => p.id === this.currentPresetId);
123 const presetName = currentPreset ? currentPreset.name : t`Unknown Preset`;
124
125 const choice = await Popup.show.confirm(
126 t`You have unsaved changes to the "${presetName}" preset.`,
127 t`Do you want to save them before switching?`,
128 {
129 okButton: t`Save Changes`,
130 cancelButton: t`Discard Changes`,
131 },
132 );
133
134 if (choice) {
135 // User chose to save changes
136 await this.savePreset(this.currentPresetId, true);
137 this.renderPresetList();
138 return true;
139 }
140
141 // User chose to discard changes
142 return true;
143 }
144
145 /**
146 * Sets up event listeners for the preset management UI.
147 * @returns {void}
148 */
149 setupEventListeners() {
150 this.presetSelect = /** @type {HTMLSelectElement} */ (document.getElementById('regex_presets'));
151 if (!this.presetSelect) {
152 console.error('RegexPresetManager: Could not find preset select element in the DOM.');
153 return;
154 }
155
156 this.presetSelect.addEventListener('change', async (event) => {
157 const selectedPresetId = this.presetSelect.value;
158 const fromSlashCommand = event instanceof CustomEvent && event?.detail?.fromSlashCommand === true;
159
160 // Check for unsaved changes before switching
161 if (!fromSlashCommand) {
162 const canProceed = await this.checkUnsavedChanges();
163 if (!canProceed) {
164 // Revert the selection
165 event.preventDefault();
166 const currentPreset = extension_settings.regex_presets.find(p => p.id === this.currentPresetId);
167 if (currentPreset) {
168 this.presetSelect.value = currentPreset.id;
169 }
170 return;
171 }
172 }
173
174 await this.applyPreset(selectedPresetId);
175 extension_settings.regex_presets.forEach(p => { p.isSelected = p.id === selectedPresetId; });
176 saveSettingsDebounced();
177 this.updateStoredState(selectedPresetId);
178 });
179
180 this.presetCreateButton = document.getElementById('regex_preset_create');
181 if (!this.presetCreateButton) {
182 console.error('RegexPresetManager: Could not find preset create button in the DOM.');
183 return;
184 }
185
186 this.presetCreateButton.addEventListener('click', async () => {
187 const newId = uuidv4();
188 await this.savePreset(newId, false);
189 this.renderPresetList();
190 this.updateStoredState(newId);
191 });
192
193 this.presetUpdateButton = document.getElementById('regex_preset_update');
194 if (!this.presetUpdateButton) {
195 console.error('RegexPresetManager: Could not find preset update button in the DOM.');
196 return;
197 }
198
199 this.presetUpdateButton.addEventListener('click', async () => {
200 const selectedPresetId = this.presetSelect.value;
201 await this.savePreset(selectedPresetId, true);
202 this.renderPresetList();
203 this.updateStoredState(selectedPresetId);
204 });
205
206 this.presetApplyButton = document.getElementById('regex_preset_apply');
207 if (!this.presetApplyButton) {
208 console.error('RegexPresetManager: Could not find preset apply button in the DOM.');
209 return;
210 }
211
212 this.presetApplyButton.addEventListener('click', async () => {
213 const selectedPresetId = this.presetSelect.value;
214 await this.applyPreset(selectedPresetId);
215 this.updateStoredState(selectedPresetId);
216 });
217
218 this.presetDeleteButton = document.getElementById('regex_preset_delete');
219 if (!this.presetDeleteButton) {
220 console.error('RegexPresetManager: Could not find preset delete button in the DOM.');
221 return;
222 }
223
224 this.presetDeleteButton.addEventListener('click', async () => {
225 const selectedPresetId = this.presetSelect.value;
226 await this.deletePreset(selectedPresetId);
227 this.renderPresetList();
228
229 const newSelectedPresetId = extension_settings.regex_presets.find(p => p.isSelected)?.id;
230 if (newSelectedPresetId) {
231 await this.applyPreset(newSelectedPresetId);
232 this.presetSelect.value = newSelectedPresetId;
233 this.updateStoredState(newSelectedPresetId);
234 } else {
235 this.currentPresetId = null;
236 this.lastKnownState = null;
237 }
238 });
239
240 this.renderPresetList();
241
242 // Initialize the stored state with the currently selected preset
243 const selectedPreset = extension_settings.regex_presets?.find(p => p.isSelected);
244 if (selectedPreset) {
245 this.updateStoredState(selectedPreset.id);
246 }
247 }
248
249 /**
250 * Registers slash commands related to regex presets.
251 * @returns {void}
252 */
253 registerSlashCommands() {
254 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
255 name: 'regex-preset',
256 helpString: t`Selects a regex preset by name or ID. Gets the current regex preset ID if no argument is provided.`,
257 callback: (args, name) => {
258 if (!this.presetSelect) {
259 return '';
260 }
261
262 name = String(name ?? '').trim();
263
264 if (name) {
265 const quiet = isTrueBoolean(args?.quiet?.toString());
266 const foundId = extension_settings.regex_presets.find(p => equalsIgnoreCaseAndAccents(p.id, name) || equalsIgnoreCaseAndAccents(p.name, name))?.id;
267
268 if (foundId) {
269 this.presetSelect.value = foundId;
270 this.presetSelect.dispatchEvent(new CustomEvent('change', { detail: { fromSlashCommand: true } }));
271 return foundId;
272 }
273
274 !quiet && toastr.warning(`Regex preset "${name}" not found`);
275 return '';
276 }
277
278 return this.presetSelect.value;
279 },
280 returns: 'current preset ID',
281 namedArgumentList: [
282 SlashCommandNamedArgument.fromProps({
283 name: 'quiet',
284 description: 'Suppress the toast message on preset change',
285 typeList: [ARGUMENT_TYPE.BOOLEAN],
286 defaultValue: 'false',
287 enumList: commonEnumProviders.boolean('trueFalse')(),
288 }),
289 ],
290 unnamedArgumentList: [
291 SlashCommandArgument.fromProps({
292 description: 'regex preset name or ID',
293 typeList: [ARGUMENT_TYPE.STRING],
294 enumProvider: () => extension_settings.regex_presets.map(x => new SlashCommandEnumValue(x.id, x.name, enumTypes.enum, enumIcons.preset)),
295 }),
296 ],
297 }));
298 }
299
300 /**
301 * Renders the list of regex presets in the UI.
302 * @returns {void}
303 */
304 renderPresetList() {
305 if (!this.presetSelect) {
306 return;
307 }
308
309 this.presetSelect.innerHTML = '';
310
311 if (!Array.isArray(extension_settings.regex_presets) || extension_settings.regex_presets.length === 0) {
312 const fallbackOption = new Option(t`[No presets saved]`, '', true, true);
313 this.presetSelect.appendChild(fallbackOption);
314 this.presetSelect.disabled = true;
315 return;
316 }
317
318 extension_settings.regex_presets.forEach(preset => {
319 const option = new Option(preset.name, preset.id, preset.isSelected, preset.isSelected);
320 this.presetSelect.appendChild(option);
321 });
322
323 this.presetSelect.disabled = false;
324 }
325
326 /**
327 * Applies a preset list to a target list of scripts.
328 * @param {Object} params The parameters object
329 * @param {RegexPresetItem[]} params.presetList The list of preset items
330 * @param {RegexScript[]} params.targetList The list of target scripts to modify
331 * @param {(targetList: RegexScript[]) => Promise<any>} params.saveFunction Function to save the modified list
332 */
333 async applyPresetList({ presetList, targetList, saveFunction }) {
334 if (!Array.isArray(targetList) || !Array.isArray(presetList)) {
335 return;
336 }
337
338 // Only enable scripts that are in the preset
339 targetList.forEach((script => {
340 script.disabled = !presetList.some(p => p.id === script.id);
341 }));
342
343 // First sort by the order in the preset, then the original order
344 targetList.sort((a, b) => {
345 const aIndex = presetList.findIndex(p => p.id === a.id);
346 const bIndex = presetList.findIndex(p => p.id === b.id);
347 return aIndex - bIndex || targetList.indexOf(a) - targetList.indexOf(b);
348 });
349
350 await saveFunction(targetList);
351 }
352
353 /**
354 * Applies a regex preset to the current context.
355 * @param {string} presetId - The ID of the preset to apply
356 * @returns {Promise<void>}
357 */
358 async applyPreset(presetId) {
359 const preset = extension_settings.regex_presets.find(p => p.id === presetId);
360 if (!preset) {
361 toastr.error(t`Could not find the selected preset.`);
362 return;
363 }
364
365 // Apply to both global and scoped lists
366 await this.applyPresetList({
367 presetList: preset.global,
368 targetList: extension_settings.regex,
369 saveFunction: () => saveSettingsDebounced(),
370 });
371 await this.applyPresetList({
372 presetList: preset.scoped,
373 targetList: characters[this_chid]?.data?.extensions?.regex_scripts,
374 saveFunction: (scripts) => writeExtensionField(this_chid, 'regex_scripts', scripts),
375 });
376
377 // Render the changes to the UI
378 await loadRegexScripts();
379 // Apply the changes to the current chat
380 await reloadCurrentChat();
381 }
382
383 /**
384 * Converts a list of regex scripts to preset items.
385 * @param {RegexScript[]} list The list of regex scripts
386 * @returns {RegexPresetItem[] | null} The list of preset items, or null if the input is invalid
387 */
388 regexListToPresetItems(list) {
389 if (!Array.isArray(list)) {
390 return null;
391 }
392
393 return list.filter(x => !x.disabled).map(s => ({ id: s.id }));
394 }
395
396 /**
397 * Saves a regex preset.
398 * @param {string} presetId - The ID of the preset
399 * @param {boolean} isUpdate - Whether this is an update operation
400 * @returns {Promise<void>}
401 */
402 async savePreset(presetId, isUpdate) {
403 const existingPreset = isUpdate ? extension_settings.regex_presets.find(p => p.id === presetId) : null;
404
405 if (isUpdate && !existingPreset) {
406 toastr.error(t`Could not find the preset to update.`);
407 return;
408 }
409
410 const name = isUpdate ? existingPreset.name : await Popup.show.input(t`Enter a name for the new regex preset:`, '');
411 const id = isUpdate ? existingPreset.id : presetId;
412
413 if (!name || !name.trim().length) {
414 return;
415 }
416
417 const preset = {
418 id: id,
419 name: name,
420 isSelected: false,
421 global: this.regexListToPresetItems(extension_settings.regex),
422 scoped: this.regexListToPresetItems(characters[this_chid]?.data?.extensions?.regex_scripts),
423 };
424
425 if (isUpdate) {
426 Object.assign(existingPreset, preset);
427 } else {
428 extension_settings.regex_presets.push(preset);
429 }
430
431 extension_settings.regex_presets.forEach(p => { p.isSelected = p.id === id; });
432 saveSettingsDebounced();
433
434 toastr.success(isUpdate ? t`Regex preset updated` : t`Regex preset saved`);
435 }
436
437 /**
438 * Deletes a regex preset.
439 * @param {string} presetId - The ID of the preset to delete
440 * @returns {Promise<void>}
441 */
442 async deletePreset(presetId) {
443 const presetIndex = extension_settings.regex_presets.findIndex(p => p.id === presetId);
444 if (presetIndex === -1) {
445 toastr.error(t`Could not find the preset to delete.`);
446 return;
447 }
448
449 const presetName = extension_settings.regex_presets[presetIndex].name;
450 const confirm = await Popup.show.confirm(t`Are you sure you want to delete this regex preset?`, presetName);
451 if (!confirm) {
452 return;
453 }
454
455 extension_settings.regex_presets.splice(presetIndex, 1);
456
457 // Select the first preset if any exist
458 extension_settings.regex_presets.forEach((p, i) => { p.isSelected = i === 0; });
459 saveSettingsDebounced();
460
461 toastr.success(t`Regex preset deleted`);
462 }
463}
464
465const presetManager = new RegexPresetManager();
466
467/**
22 * Retrieves the list of regex scripts by combining the scripts from the extension settings and the character data468 * Retrieves the list of regex scripts by combining the scripts from the extension settings and the character data
23 *469 *
24 * @return {RegexScript[]} An array of regex scripts, where each script is an object containing the necessary information.470 * @return {RegexScript[]} An array of regex scripts, where each script is an object containing the necessary information.
@@ -105,7 +551,7 @@ async function deleteRegexScript({ id, isScoped }) {
105 const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];551 const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
106552
107 const existingScriptIndex = array.findIndex((script) => script.id === id);553 const existingScriptIndex = array.findIndex((script) => script.id === id);
108 if (!existingScriptIndex || existingScriptIndex !== -1) {554 if (existingScriptIndex !== -1) {
109 array.splice(existingScriptIndex, 1);555 array.splice(existingScriptIndex, 1);
110556
111 if (isScoped) {557 if (isScoped) {
@@ -763,8 +1209,8 @@ async function onRegexDebuggerOpenClick() {
7631209
764 debuggerHtml.find('#regex_debugger_expand_final').on('click', function () {1210 debuggerHtml.find('#regex_debugger_expand_final').on('click', function () {
765 const content = $('#regex_debugger_final_output').html();1211 const content = $('#regex_debugger_final_output').html();
766 const popupContent = $('<div style="height: 70vh; overflow-y: auto;"></div>').html(content);1212 const popupContent = $('<div class="regex-popup-content"></div>').html(content);
767 callGenericPopup(popupContent, POPUP_TYPE.TEXT, 'Final Output', { wide: true, allowVerticalScrolling: true });1213 callGenericPopup(popupContent, POPUP_TYPE.TEXT, 'Final Output', { wide: true, large: true, allowVerticalScrolling: true });
768 });1214 });
7691215
770 await callGenericPopup(debuggerHtml.children(), POPUP_TYPE.TEXT, '', { wide: true, allowVerticalScrolling: true });1216 await callGenericPopup(debuggerHtml.children(), POPUP_TYPE.TEXT, '', { wide: true, allowVerticalScrolling: true });
@@ -1033,8 +1479,12 @@ async function checkEmbeddedRegexScripts() {
1033// Workaround for loading in sequence with other extensions1479// Workaround for loading in sequence with other extensions
1034// NOTE: Always puts extension at the top of the list, but this is fine since it's static1480// NOTE: Always puts extension at the top of the list, but this is fine since it's static
1035jQuery(async () => {1481jQuery(async () => {
1036 if (extension_settings.regex) {1482 if (!Array.isArray(extension_settings.regex)) {
1037 migrateSettings();1483 extension_settings.regex = [];
1484 }
1485
1486 if (!Array.isArray(extension_settings.regex_presets)) {
1487 extension_settings.regex_presets = [];
1038 }1488 }
10391489
1040 // Manually disable the extension since static imports auto-import the JS file1490 // Manually disable the extension since static imports auto-import the JS file
@@ -1042,6 +1492,8 @@ jQuery(async () => {
1042 return;1492 return;
1043 }1493 }
10441494
1495 migrateSettings();
1496
1045 const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown'));1497 const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown'));
1046 $('#regex_container').append(settingsHtml);1498 $('#regex_container').append(settingsHtml);
1047 $('#open_regex_editor').on('click', function () {1499 $('#open_regex_editor').on('click', function () {
@@ -1300,4 +1752,7 @@ jQuery(async () => {
13001752
1301 eventSource.on(event_types.CHAT_CHANGED, checkEmbeddedRegexScripts);1753 eventSource.on(event_types.CHAT_CHANGED, checkEmbeddedRegexScripts);
1302 eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts);1754 eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts);
1755
1756 presetManager.setupEventListeners();
1757 presetManager.registerSlashCommands();
1303});1758});
public/scripts/extensions/regex/style.css+8 -8
@@ -41,19 +41,19 @@
41 opacity: 0.5;41 opacity: 0.5;
42}42}
4343
44.enable_scoped:checked ~ .regex-toggle-on {44.enable_scoped:checked~.regex-toggle-on {
45 display: block;45 display: block;
46}46}
4747
48.enable_scoped:checked ~ .regex-toggle-off {48.enable_scoped:checked~.regex-toggle-off {
49 display: none;49 display: none;
50}50}
5151
52.enable_scoped:not(:checked) ~ .regex-toggle-on {52.enable_scoped:not(:checked)~.regex-toggle-on {
53 display: none;53 display: none;
54}54}
5555
56.enable_scoped:not(:checked) ~ .regex-toggle-off {56.enable_scoped:not(:checked)~.regex-toggle-off {
57 display: block;57 display: block;
58}58}
5959
@@ -92,19 +92,19 @@ input.enable_scoped {
92 cursor: pointer;92 cursor: pointer;
93}93}
9494
95.disable_regex:checked ~ .regex-toggle-off {95.disable_regex:checked~.regex-toggle-off {
96 display: block;96 display: block;
97}97}
9898
99.disable_regex:checked ~ .regex-toggle-on {99.disable_regex:checked~.regex-toggle-on {
100 display: none;100 display: none;
101}101}
102102
103.disable_regex:not(:checked) ~ .regex-toggle-off {103.disable_regex:not(:checked)~.regex-toggle-off {
104 display: none;104 display: none;
105}105}
106106
107.disable_regex:not(:checked) ~ .regex-toggle-on {107.disable_regex:not(:checked)~.regex-toggle-on {
108 display: block;108 display: block;
109}109}
110110