mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	Merge branch 'SillyTavern:staging' into staging
This commit is contained in:
		@@ -1,14 +1,15 @@
 | 
			
		||||
import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHeaders, saveSettingsDebounced, substituteParams } from '../../../script.js';
 | 
			
		||||
import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHeaders, online_status, saveSettingsDebounced, substituteParams } from '../../../script.js';
 | 
			
		||||
import { dragElement, isMobile } from '../../RossAscends-mods.js';
 | 
			
		||||
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
 | 
			
		||||
import { loadMovingUIState, power_user } from '../../power-user.js';
 | 
			
		||||
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence } from '../../utils.js';
 | 
			
		||||
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition } from '../../utils.js';
 | 
			
		||||
import { hideMutedSprites } from '../../group-chats.js';
 | 
			
		||||
import { isJsonSchemaSupported } from '../../textgen-settings.js';
 | 
			
		||||
import { debounce_timeout } from '../../constants.js';
 | 
			
		||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
 | 
			
		||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
 | 
			
		||||
import { ARGUMENT_TYPE, SlashCommandArgument } from '../../slash-commands/SlashCommandArgument.js';
 | 
			
		||||
import { isFunctionCallingSupported } from '../../openai.js';
 | 
			
		||||
export { MODULE_NAME };
 | 
			
		||||
 | 
			
		||||
const MODULE_NAME = 'expressions';
 | 
			
		||||
@@ -16,6 +17,7 @@ const UPDATE_INTERVAL = 2000;
 | 
			
		||||
const STREAMING_UPDATE_INTERVAL = 10000;
 | 
			
		||||
const TALKINGCHECK_UPDATE_INTERVAL = 500;
 | 
			
		||||
const DEFAULT_FALLBACK_EXPRESSION = 'joy';
 | 
			
		||||
const FUNCTION_NAME = 'set_emotion';
 | 
			
		||||
const DEFAULT_LLM_PROMPT = 'Pause your roleplay. Classify the emotion of the last message. Output just one word, e.g. "joy" or "anger". Choose only one of the following labels: {{labels}}';
 | 
			
		||||
const DEFAULT_EXPRESSIONS = [
 | 
			
		||||
    'talkinghead',
 | 
			
		||||
@@ -1001,6 +1003,10 @@ async function getLlmPrompt(labels) {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isFunctionCallingSupported()) {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const labelsString = labels.map(x => `"${x}"`).join(', ');
 | 
			
		||||
    const prompt = substituteParams(String(extension_settings.expressions.llmPrompt))
 | 
			
		||||
        .replace(/{{labels}}/gi, labelsString);
 | 
			
		||||
@@ -1014,11 +1020,16 @@ async function getLlmPrompt(labels) {
 | 
			
		||||
 * @returns {string} The parsed emotion or the fallback expression.
 | 
			
		||||
 */
 | 
			
		||||
function parseLlmResponse(emotionResponse, labels) {
 | 
			
		||||
    const fallbackExpression = getFallbackExpression();
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        const parsedEmotion = JSON.parse(emotionResponse);
 | 
			
		||||
        return parsedEmotion?.emotion ?? fallbackExpression;
 | 
			
		||||
        const response = parsedEmotion?.emotion?.trim()?.toLowerCase();
 | 
			
		||||
 | 
			
		||||
        if (!response || !labels.includes(response)) {
 | 
			
		||||
            console.debug(`Parsed emotion response: ${response} not in labels: ${labels}`);
 | 
			
		||||
            throw new Error('Emotion not in labels');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return response;
 | 
			
		||||
    } catch {
 | 
			
		||||
        const fuse = new Fuse(labels, { includeScore: true });
 | 
			
		||||
        console.debug('Using fuzzy search in labels:', labels);
 | 
			
		||||
@@ -1032,6 +1043,41 @@ function parseLlmResponse(emotionResponse, labels) {
 | 
			
		||||
    throw new Error('Could not parse emotion response ' + emotionResponse);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Registers the function tool for the LLM API.
 | 
			
		||||
 * @param {FunctionToolRegister} args Function tool register arguments.
 | 
			
		||||
 */
 | 
			
		||||
function onFunctionToolRegister(args) {
 | 
			
		||||
    if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isFunctionCallingSupported()) {
 | 
			
		||||
        // Only trigger on quiet mode
 | 
			
		||||
        if (args.type !== 'quiet') {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const emotions = DEFAULT_EXPRESSIONS.filter((e) => e != 'talkinghead');
 | 
			
		||||
        const jsonSchema = {
 | 
			
		||||
            $schema: 'http://json-schema.org/draft-04/schema#',
 | 
			
		||||
            type: 'object',
 | 
			
		||||
            properties: {
 | 
			
		||||
                emotion: {
 | 
			
		||||
                    type: 'string',
 | 
			
		||||
                    enum: emotions,
 | 
			
		||||
                    description: `One of the following: ${JSON.stringify(emotions)}`,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            required: [
 | 
			
		||||
                'emotion',
 | 
			
		||||
            ],
 | 
			
		||||
        };
 | 
			
		||||
        args.registerFunctionTool(
 | 
			
		||||
            FUNCTION_NAME,
 | 
			
		||||
            substituteParams('Sets the label that best describes the current emotional state of {{char}}. Only select one of the enumerated values.'),
 | 
			
		||||
            jsonSchema,
 | 
			
		||||
            true,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onTextGenSettingsReady(args) {
 | 
			
		||||
    // Only call if inside an API call
 | 
			
		||||
    if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) {
 | 
			
		||||
@@ -1087,11 +1133,27 @@ async function getExpressionLabel(text) {
 | 
			
		||||
            } break;
 | 
			
		||||
            // Using LLM
 | 
			
		||||
            case EXPRESSION_API.llm: {
 | 
			
		||||
                try {
 | 
			
		||||
                    await waitUntilCondition(() => online_status !== 'no_connection', 3000, 250);
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    console.warn('No LLM connection. Using fallback expression', error);
 | 
			
		||||
                    return getFallbackExpression();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const expressionsList = await getExpressionsList();
 | 
			
		||||
                const prompt = await getLlmPrompt(expressionsList);
 | 
			
		||||
                let functionResult = null;
 | 
			
		||||
                eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);
 | 
			
		||||
                eventSource.once(event_types.LLM_FUNCTION_TOOL_REGISTER, onFunctionToolRegister);
 | 
			
		||||
                eventSource.once(event_types.LLM_FUNCTION_TOOL_CALL, (/** @type {FunctionToolCall} */ args) => {
 | 
			
		||||
                    if (args.name !== FUNCTION_NAME) {
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    functionResult = args?.arguments;
 | 
			
		||||
                });
 | 
			
		||||
                const emotionResponse = await generateQuietPrompt(prompt, false, false);
 | 
			
		||||
                return parseLlmResponse(emotionResponse, expressionsList);
 | 
			
		||||
                return parseLlmResponse(functionResult || emotionResponse, expressionsList);
 | 
			
		||||
            }
 | 
			
		||||
            // Extras
 | 
			
		||||
            default: {
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@
 | 
			
		||||
                        <i class="fa-solid fa-clock-rotate-left fa-sm"></i>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </label>
 | 
			
		||||
                <small>Will be used if the API doesn't support JSON schemas.</small>
 | 
			
		||||
                <small>Will be used if the API doesn't support JSON schemas or function calling.</small>
 | 
			
		||||
                <textarea id="expression_llm_prompt" type="text" class="text_pole textarea_compact" rows="2" placeholder="Use {{labels}} special macro."></textarea>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="expression_fallback_block m-b-1 m-t-1">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,52 @@
 | 
			
		||||
<div class="regex_settings">
 | 
			
		||||
    <div class="inline-drawer">
 | 
			
		||||
        <div class="inline-drawer-toggle inline-drawer-header">
 | 
			
		||||
            <b>Regex</b>
 | 
			
		||||
            <b data-i18n="ext_regex_title">
 | 
			
		||||
                Regex
 | 
			
		||||
            </b>
 | 
			
		||||
            <div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="inline-drawer-content">
 | 
			
		||||
            <div class="flex-container">
 | 
			
		||||
                <div id="open_regex_editor" class="menu_button">
 | 
			
		||||
                <div id="open_regex_editor" class="menu_button menu_button_icon" title="New global regex script">
 | 
			
		||||
                    <i class="fa-solid fa-pen-to-square"></i>
 | 
			
		||||
                    <span data-i18n="ext_regex_open_editor">Open Editor</span>
 | 
			
		||||
                    <small data-i18n="ext_regex_new_global_script">+ Global</small>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div id="import_regex" class="menu_button">
 | 
			
		||||
                <div id="open_scoped_editor" class="menu_button menu_button_icon" title="New scoped regex script">
 | 
			
		||||
                    <i class="fa-solid fa-address-card"></i>
 | 
			
		||||
                    <small data-i18n="ext_regex_new_scoped_script">+ Scoped</small>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div id="import_regex" class="menu_button menu_button_icon">
 | 
			
		||||
                    <i class="fa-solid fa-file-import"></i>
 | 
			
		||||
                    <span data-i18n="ext_regex_import_script">Import Script</span>
 | 
			
		||||
                    <small data-i18n="ext_regex_import_script">Import</small>
 | 
			
		||||
                </div>
 | 
			
		||||
                <input type="file" id="import_regex_file" hidden accept="*.json" multiple />
 | 
			
		||||
            </div>
 | 
			
		||||
            <hr />
 | 
			
		||||
            <label data-i18n="ext_regex_saved_scripts">Saved Scripts</label>
 | 
			
		||||
            <div id="saved_regex_scripts" class="flex-container regex-script-container flexFlowColumn"></div>
 | 
			
		||||
            <div id="global_scripts_block" class="padding5">
 | 
			
		||||
                <div>
 | 
			
		||||
                    <strong data-i18n="ext_regex_global_scripts">Global Scripts</strong>
 | 
			
		||||
                </div>
 | 
			
		||||
                <small data-i18n="ext_regex_global_scripts_desc">
 | 
			
		||||
                    Available for all characters. Saved to local settings.
 | 
			
		||||
                </small>
 | 
			
		||||
                <div id="saved_regex_scripts" class="flex-container regex-script-container flexFlowColumn"></div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <hr />
 | 
			
		||||
            <div id="scoped_scripts_block" class="padding5">
 | 
			
		||||
                <div class="flex-container alignItemsBaseline">
 | 
			
		||||
                    <strong class="flex1" data-i18n="ext_regex_scoped_scripts">Scoped Scripts</strong>
 | 
			
		||||
                    <label id="toggle_scoped_regex" class="checkbox flex-container" for="regex_scoped_toggle">
 | 
			
		||||
                        <input type="checkbox" id="regex_scoped_toggle" class="enable_scoped" />
 | 
			
		||||
                        <span class="regex-toggle-on fa-solid fa-toggle-on fa-lg" title="Disallow using scoped regex"></span>
 | 
			
		||||
                        <span class="regex-toggle-off fa-solid fa-toggle-off fa-lg" title="Allow using scoped regex"></span>
 | 
			
		||||
                    </label>
 | 
			
		||||
                </div>
 | 
			
		||||
                <small data-i18n="ext_regex_scoped_scripts_desc">
 | 
			
		||||
                    Only available for this character. Saved to the card data.
 | 
			
		||||
                </small>
 | 
			
		||||
                <div id="saved_scoped_scripts" class="flex-container regex-script-container flexFlowColumn"></div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -56,7 +56,7 @@
 | 
			
		||||
                <div>
 | 
			
		||||
                    <textarea
 | 
			
		||||
                        class="regex_replace_string text_pole wide100p textarea_compact"
 | 
			
		||||
						data-i18n="[placeholder]ext_regex_replace_string_placeholder"
 | 
			
		||||
                        data-i18n="[placeholder]ext_regex_replace_string_placeholder"
 | 
			
		||||
                        placeholder="Use {{match}} to include the matched text from the Find Regex or $1, $2, etc. for capture groups."
 | 
			
		||||
                        rows="2"
 | 
			
		||||
                    ></textarea>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								public/scripts/extensions/regex/embeddedScripts.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								public/scripts/extensions/regex/embeddedScripts.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
<div>
 | 
			
		||||
    <h3>This character has embedded regex script(s).</h3>
 | 
			
		||||
    <h3>Would you like to allow using them?</h3>
 | 
			
		||||
    <div class="m-b-1">If you want to do it later, select "Regex" from the extensions menu.</div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { substituteParams } from '../../../script.js';
 | 
			
		||||
import { characters, substituteParams, this_chid } from '../../../script.js';
 | 
			
		||||
import { extension_settings } from '../../extensions.js';
 | 
			
		||||
import { regexFromString } from '../../utils.js';
 | 
			
		||||
export {
 | 
			
		||||
@@ -22,6 +22,22 @@ const regex_placement = {
 | 
			
		||||
    WORLD_INFO: 5,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function getScopedRegex() {
 | 
			
		||||
    const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar);
 | 
			
		||||
 | 
			
		||||
    if (!isAllowed) {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const scripts = characters[this_chid]?.data?.extensions?.regex_scripts;
 | 
			
		||||
 | 
			
		||||
    if (!Array.isArray(scripts)) {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return scripts;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Parent function to fetch a regexed version of a raw string
 | 
			
		||||
 * @param {string} rawString The raw string to be regexed
 | 
			
		||||
@@ -42,7 +58,8 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
 | 
			
		||||
        return finalString;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    extension_settings.regex.forEach((script) => {
 | 
			
		||||
    const allRegex = [...(extension_settings.regex ?? []), ...(getScopedRegex() ?? [])];
 | 
			
		||||
    allRegex.forEach((script) => {
 | 
			
		||||
        if (
 | 
			
		||||
            // Script applies to Markdown and input is Markdown
 | 
			
		||||
            (script.markdownOnly && isMarkdown) ||
 | 
			
		||||
@@ -95,7 +112,7 @@ function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Run replacement. Currently does not support the Overlay strategy
 | 
			
		||||
    newString = rawString.replace(findRegex, function(match) {
 | 
			
		||||
    newString = rawString.replace(findRegex, function (match) {
 | 
			
		||||
        const args = [...arguments];
 | 
			
		||||
        const replaceString = regexScript.replaceString.replace(/{{match}}/gi, '$0');
 | 
			
		||||
        const replaceWithGroups = replaceString.replaceAll(/\$(\d+)/g, (_, num) => {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								public/scripts/extensions/regex/importTarget.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								public/scripts/extensions/regex/importTarget.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
<div>
 | 
			
		||||
    <h3 data-i18n="ext_regex_import_target">
 | 
			
		||||
        Import To:
 | 
			
		||||
    </h3>
 | 
			
		||||
    <div class="flex-container flexFlowColumn wide100p padding10 justifyLeft">
 | 
			
		||||
        <label for="regex_import_target_global">
 | 
			
		||||
            <input type="radio" name="regex_import_target" id="regex_import_target_global" value="global" checked />
 | 
			
		||||
            <span data-i18n="ext_regex_global_scripts">
 | 
			
		||||
                Global Scripts
 | 
			
		||||
            </span>
 | 
			
		||||
        </label>
 | 
			
		||||
        <label for="regex_import_target_scoped">
 | 
			
		||||
            <input type="radio" name="regex_import_target" id="regex_import_target_scoped" value="scoped" />
 | 
			
		||||
            <span data-i18n="ext_regex_scoped_scripts">
 | 
			
		||||
                Scoped Scripts
 | 
			
		||||
            </span>
 | 
			
		||||
        </label>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import { callPopup, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced } from '../../../script.js';
 | 
			
		||||
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
 | 
			
		||||
import { callPopup, characters, eventSource, event_types, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced, this_chid } from '../../../script.js';
 | 
			
		||||
import { extension_settings, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js';
 | 
			
		||||
import { selected_group } from '../../group-chats.js';
 | 
			
		||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
 | 
			
		||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
 | 
			
		||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
 | 
			
		||||
@@ -7,8 +8,21 @@ import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js'
 | 
			
		||||
import { resolveVariable } from '../../variables.js';
 | 
			
		||||
import { regex_placement, runRegexScript } from './engine.js';
 | 
			
		||||
 | 
			
		||||
async function saveRegexScript(regexScript, existingScriptIndex) {
 | 
			
		||||
/**
 | 
			
		||||
 * Saves a regex script to the extension settings or character data.
 | 
			
		||||
 * @param {import('../../char-data.js').RegexScriptData} regexScript
 | 
			
		||||
 * @param {number} existingScriptIndex Index of the existing script
 | 
			
		||||
 * @param {boolean} isScoped Is the script scoped to a character?
 | 
			
		||||
 * @returns {Promise<void>}
 | 
			
		||||
 */
 | 
			
		||||
async function saveRegexScript(regexScript, existingScriptIndex, isScoped) {
 | 
			
		||||
    // If not editing
 | 
			
		||||
    const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
 | 
			
		||||
 | 
			
		||||
    // Assign a UUID if it doesn't exist
 | 
			
		||||
    if (!regexScript.id) {
 | 
			
		||||
        regexScript.id = uuidv4();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Is the script name undefined or empty?
 | 
			
		||||
    if (!regexScript.scriptName) {
 | 
			
		||||
@@ -16,22 +30,6 @@ async function saveRegexScript(regexScript, existingScriptIndex) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (existingScriptIndex === -1) {
 | 
			
		||||
        // Does the script name already exist?
 | 
			
		||||
        if (extension_settings.regex.find((e) => e.scriptName === regexScript.scriptName)) {
 | 
			
		||||
            toastr.error(`Could not save regex script: A script with name ${regexScript.scriptName} already exists.`);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        // Does the script name already exist somewhere else?
 | 
			
		||||
        // (If this fails, make it a .filter().map() to index array)
 | 
			
		||||
        const foundIndex = extension_settings.regex.findIndex((e) => e.scriptName === regexScript.scriptName);
 | 
			
		||||
        if (foundIndex !== existingScriptIndex && foundIndex !== -1) {
 | 
			
		||||
            toastr.error(`Could not save regex script: A script with name ${regexScript.scriptName} already exists.`);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Is a find regex present?
 | 
			
		||||
    if (regexScript.findRegex.length === 0) {
 | 
			
		||||
        toastr.warning('This regex script will not work, but was saved anyway: A find regex isn\'t present.');
 | 
			
		||||
@@ -43,9 +41,18 @@ async function saveRegexScript(regexScript, existingScriptIndex) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (existingScriptIndex !== -1) {
 | 
			
		||||
        extension_settings.regex[existingScriptIndex] = regexScript;
 | 
			
		||||
        array[existingScriptIndex] = regexScript;
 | 
			
		||||
    } else {
 | 
			
		||||
        extension_settings.regex.push(regexScript);
 | 
			
		||||
        array.push(regexScript);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isScoped) {
 | 
			
		||||
        await writeExtensionField(this_chid, 'regex_scripts', array);
 | 
			
		||||
 | 
			
		||||
        // Add the character to the allowed list
 | 
			
		||||
        if (!extension_settings.character_allowed_regex.includes(characters[this_chid].avatar)) {
 | 
			
		||||
            extension_settings.character_allowed_regex.push(characters[this_chid].avatar);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    saveSettingsDebounced();
 | 
			
		||||
@@ -58,12 +65,16 @@ async function saveRegexScript(regexScript, existingScriptIndex) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function deleteRegexScript({ existingId }) {
 | 
			
		||||
    let scriptName = $(`#${existingId}`).find('.regex_script_name').text();
 | 
			
		||||
async function deleteRegexScript({ id, isScoped }) {
 | 
			
		||||
    const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
 | 
			
		||||
 | 
			
		||||
    const existingScriptIndex = extension_settings.regex.findIndex((script) => script.scriptName === scriptName);
 | 
			
		||||
    const existingScriptIndex = array.findIndex((script) => script.id === id);
 | 
			
		||||
    if (!existingScriptIndex || existingScriptIndex !== -1) {
 | 
			
		||||
        extension_settings.regex.splice(existingScriptIndex, 1);
 | 
			
		||||
        array.splice(existingScriptIndex, 1);
 | 
			
		||||
 | 
			
		||||
        if (isScoped) {
 | 
			
		||||
            await writeExtensionField(this_chid, 'regex_scripts', array);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        saveSettingsDebounced();
 | 
			
		||||
        await loadRegexScripts();
 | 
			
		||||
@@ -72,19 +83,32 @@ async function deleteRegexScript({ existingId }) {
 | 
			
		||||
 | 
			
		||||
async function loadRegexScripts() {
 | 
			
		||||
    $('#saved_regex_scripts').empty();
 | 
			
		||||
    $('#saved_scoped_scripts').empty();
 | 
			
		||||
 | 
			
		||||
    const scriptTemplate = $(await renderExtensionTemplateAsync('regex', 'scriptTemplate'));
 | 
			
		||||
 | 
			
		||||
    extension_settings.regex.forEach((script) => {
 | 
			
		||||
    /**
 | 
			
		||||
     * Renders a script to the UI.
 | 
			
		||||
     * @param {string} container Container to render the script to
 | 
			
		||||
     * @param {import('../../char-data.js').RegexScriptData} script Script data
 | 
			
		||||
     * @param {boolean} isScoped Script is scoped to a character
 | 
			
		||||
     * @param {number} index Index of the script in the array
 | 
			
		||||
     */
 | 
			
		||||
    function renderScript(container, script, isScoped, index) {
 | 
			
		||||
        // Have to clone here
 | 
			
		||||
        const scriptHtml = scriptTemplate.clone();
 | 
			
		||||
        scriptHtml.attr('id', uuidv4());
 | 
			
		||||
        const save = () => saveRegexScript(script, index, isScoped);
 | 
			
		||||
 | 
			
		||||
        if (!script.id) {
 | 
			
		||||
            script.id = uuidv4();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        scriptHtml.attr('id', script.id);
 | 
			
		||||
        scriptHtml.find('.regex_script_name').text(script.scriptName);
 | 
			
		||||
        scriptHtml.find('.disable_regex').prop('checked', script.disabled ?? false)
 | 
			
		||||
            .on('input', function () {
 | 
			
		||||
            .on('input', async function () {
 | 
			
		||||
                script.disabled = !!$(this).prop('checked');
 | 
			
		||||
                reloadCurrentChat();
 | 
			
		||||
                saveSettingsDebounced();
 | 
			
		||||
                await save();
 | 
			
		||||
            });
 | 
			
		||||
        scriptHtml.find('.regex-toggle-on').on('click', function () {
 | 
			
		||||
            scriptHtml.find('.disable_regex').prop('checked', true).trigger('input');
 | 
			
		||||
@@ -93,7 +117,37 @@ async function loadRegexScripts() {
 | 
			
		||||
            scriptHtml.find('.disable_regex').prop('checked', false).trigger('input');
 | 
			
		||||
        });
 | 
			
		||||
        scriptHtml.find('.edit_existing_regex').on('click', async function () {
 | 
			
		||||
            await onRegexEditorOpenClick(scriptHtml.attr('id'));
 | 
			
		||||
            await onRegexEditorOpenClick(scriptHtml.attr('id'), isScoped);
 | 
			
		||||
        });
 | 
			
		||||
        scriptHtml.find('.move_to_global').on('click', async function () {
 | 
			
		||||
            const confirm = await callPopup('Are you sure you want to move this regex script to global?', 'confirm');
 | 
			
		||||
 | 
			
		||||
            if (!confirm) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await deleteRegexScript({ id: script.id, isScoped: true });
 | 
			
		||||
            await saveRegexScript(script, -1, false);
 | 
			
		||||
        });
 | 
			
		||||
        scriptHtml.find('.move_to_scoped').on('click', async function () {
 | 
			
		||||
            if (this_chid === undefined) {
 | 
			
		||||
                toastr.error('No character selected.');
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (selected_group) {
 | 
			
		||||
                toastr.error('Cannot edit scoped scripts in group chats.');
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const confirm = await callPopup('Are you sure you want to move this regex script to scoped?', 'confirm');
 | 
			
		||||
 | 
			
		||||
            if (!confirm) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await deleteRegexScript({ id: script.id, isScoped: false });
 | 
			
		||||
            await saveRegexScript(script, -1, true);
 | 
			
		||||
        });
 | 
			
		||||
        scriptHtml.find('.export_regex').on('click', async function () {
 | 
			
		||||
            const fileName = `${script.scriptName.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, '_').toLowerCase()}.json`;
 | 
			
		||||
@@ -107,23 +161,36 @@ async function loadRegexScripts() {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await deleteRegexScript({ existingId: scriptHtml.attr('id') });
 | 
			
		||||
            await deleteRegexScript({ id: script.id, isScoped });
 | 
			
		||||
            await reloadCurrentChat();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $('#saved_regex_scripts').append(scriptHtml);
 | 
			
		||||
    });
 | 
			
		||||
        $(container).append(scriptHtml);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    extension_settings?.regex?.forEach((script, index, array) => renderScript('#saved_regex_scripts', script, false, index, array));
 | 
			
		||||
    characters[this_chid]?.data?.extensions?.regex_scripts?.forEach((script, index, array) => renderScript('#saved_scoped_scripts', script, true, index, array));
 | 
			
		||||
 | 
			
		||||
    const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar);
 | 
			
		||||
    $('#regex_scoped_toggle').prop('checked', isAllowed);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function onRegexEditorOpenClick(existingId) {
 | 
			
		||||
/**
 | 
			
		||||
 * Opens the regex editor.
 | 
			
		||||
 * @param {string|boolean} existingId Existing ID
 | 
			
		||||
 * @param {boolean} isScoped Is the script scoped to a character?
 | 
			
		||||
 * @returns {Promise<void>}
 | 
			
		||||
 */
 | 
			
		||||
async function onRegexEditorOpenClick(existingId, isScoped) {
 | 
			
		||||
    const editorHtml = $(await renderExtensionTemplateAsync('regex', 'editor'));
 | 
			
		||||
    const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
 | 
			
		||||
 | 
			
		||||
    // If an ID exists, fill in all the values
 | 
			
		||||
    let existingScriptIndex = -1;
 | 
			
		||||
    if (existingId) {
 | 
			
		||||
        const existingScriptName = $(`#${existingId}`).find('.regex_script_name').text();
 | 
			
		||||
        existingScriptIndex = extension_settings.regex.findIndex((script) => script.scriptName === existingScriptName);
 | 
			
		||||
        existingScriptIndex = array.findIndex((script) => script.id === existingId);
 | 
			
		||||
        if (existingScriptIndex !== -1) {
 | 
			
		||||
            const existingScript = extension_settings.regex[existingScriptIndex];
 | 
			
		||||
            const existingScript = array[existingScriptIndex];
 | 
			
		||||
            if (existingScript.scriptName) {
 | 
			
		||||
                editorHtml.find('.regex_script_name').val(existingScript.scriptName);
 | 
			
		||||
            } else {
 | 
			
		||||
@@ -173,6 +240,7 @@ async function onRegexEditorOpenClick(existingId) {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const testScript = {
 | 
			
		||||
            id: uuidv4(),
 | 
			
		||||
            scriptName: editorHtml.find('.regex_script_name').val(),
 | 
			
		||||
            findRegex: editorHtml.find('.find_regex').val(),
 | 
			
		||||
            replaceString: editorHtml.find('.regex_replace_string').val(),
 | 
			
		||||
@@ -189,9 +257,10 @@ async function onRegexEditorOpenClick(existingId) {
 | 
			
		||||
    const popupResult = await callPopup(editorHtml, 'confirm', undefined, { okButton: 'Save' });
 | 
			
		||||
    if (popupResult) {
 | 
			
		||||
        const newRegexScript = {
 | 
			
		||||
            scriptName: editorHtml.find('.regex_script_name').val(),
 | 
			
		||||
            findRegex: editorHtml.find('.find_regex').val(),
 | 
			
		||||
            replaceString: editorHtml.find('.regex_replace_string').val(),
 | 
			
		||||
            id: existingId ? String(existingId) : uuidv4(),
 | 
			
		||||
            scriptName: String(editorHtml.find('.regex_script_name').val()),
 | 
			
		||||
            findRegex: String(editorHtml.find('.find_regex').val()),
 | 
			
		||||
            replaceString: String(editorHtml.find('.regex_replace_string').val()),
 | 
			
		||||
            trimStrings: editorHtml.find('.regex_trim_strings').val().split('\n').filter((e) => e.length !== 0) || [],
 | 
			
		||||
            placement:
 | 
			
		||||
                editorHtml
 | 
			
		||||
@@ -209,7 +278,7 @@ async function onRegexEditorOpenClick(existingId) {
 | 
			
		||||
            maxDepth: parseInt(String(editorHtml.find('input[name="max_depth"]').val())),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        saveRegexScript(newRegexScript, existingScriptIndex);
 | 
			
		||||
        saveRegexScript(newRegexScript, existingScriptIndex, isScoped);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -220,6 +289,11 @@ function migrateSettings() {
 | 
			
		||||
 | 
			
		||||
    // Current: If MD Display is present in placement, remove it and add new placements/MD option
 | 
			
		||||
    extension_settings.regex.forEach((script) => {
 | 
			
		||||
        if (!script.id) {
 | 
			
		||||
            script.id = uuidv4();
 | 
			
		||||
            performSave = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (script.placement.includes(regex_placement.MD_DISPLAY)) {
 | 
			
		||||
            script.placement = script.placement.length === 1 ?
 | 
			
		||||
                Object.values(regex_placement).filter((e) => e !== regex_placement.MD_DISPLAY) :
 | 
			
		||||
@@ -242,6 +316,11 @@ function migrateSettings() {
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!extension_settings.character_allowed_regex) {
 | 
			
		||||
        extension_settings.character_allowed_regex = [];
 | 
			
		||||
        performSave = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (performSave) {
 | 
			
		||||
        saveSettingsDebounced();
 | 
			
		||||
    }
 | 
			
		||||
@@ -260,8 +339,9 @@ function runRegexCallback(args, value) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const scriptName = String(resolveVariable(args.name));
 | 
			
		||||
    const scripts = [...(extension_settings.regex ?? []), ...(characters[this_chid]?.data?.extensions?.regex_scripts ?? [])];
 | 
			
		||||
 | 
			
		||||
    for (const script of extension_settings.regex) {
 | 
			
		||||
    for (const script of scripts) {
 | 
			
		||||
        if (String(script.scriptName).toLowerCase() === String(scriptName).toLowerCase()) {
 | 
			
		||||
            if (script.disabled) {
 | 
			
		||||
                toastr.warning(`Regex script "${scriptName}" is disabled.`);
 | 
			
		||||
@@ -280,8 +360,9 @@ function runRegexCallback(args, value) {
 | 
			
		||||
/**
 | 
			
		||||
 * Performs the import of the regex file.
 | 
			
		||||
 * @param {File} file Input file
 | 
			
		||||
 * @param {boolean} isScoped Is the script scoped to a character?
 | 
			
		||||
 */
 | 
			
		||||
async function onRegexImportFileChange(file) {
 | 
			
		||||
async function onRegexImportFileChange(file, isScoped) {
 | 
			
		||||
    if (!file) {
 | 
			
		||||
        toastr.error('No file provided.');
 | 
			
		||||
        return;
 | 
			
		||||
@@ -294,7 +375,15 @@ async function onRegexImportFileChange(file) {
 | 
			
		||||
            throw new Error('No script name provided.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        extension_settings.regex.push(regexScript);
 | 
			
		||||
        // Assign a new UUID
 | 
			
		||||
        regexScript.id = uuidv4();
 | 
			
		||||
 | 
			
		||||
        const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
 | 
			
		||||
        array.push(regexScript);
 | 
			
		||||
 | 
			
		||||
        if (isScoped) {
 | 
			
		||||
            await writeExtensionField(this_chid, 'regex_scripts', array);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        saveSettingsDebounced();
 | 
			
		||||
        await loadRegexScripts();
 | 
			
		||||
@@ -306,6 +395,47 @@ async function onRegexImportFileChange(file) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function purgeEmbeddedRegexScripts( { character }){
 | 
			
		||||
    const avatar = character?.avatar;
 | 
			
		||||
 | 
			
		||||
    if (avatar && extension_settings.character_allowed_regex?.includes(avatar)) {
 | 
			
		||||
        const index = extension_settings.character_allowed_regex.indexOf(avatar);
 | 
			
		||||
        if (index !== -1) {
 | 
			
		||||
            extension_settings.character_allowed_regex.splice(index, 1);
 | 
			
		||||
            saveSettingsDebounced();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function checkEmbeddedRegexScripts() {
 | 
			
		||||
    const chid = this_chid;
 | 
			
		||||
 | 
			
		||||
    if (chid !== undefined && !selected_group) {
 | 
			
		||||
        const avatar = characters[chid]?.avatar;
 | 
			
		||||
        const scripts = characters[chid]?.data?.extensions?.regex_scripts;
 | 
			
		||||
 | 
			
		||||
        if (Array.isArray(scripts) && scripts.length > 0) {
 | 
			
		||||
            if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) {
 | 
			
		||||
                const checkKey = `AlertRegex_${characters[chid].avatar}`;
 | 
			
		||||
 | 
			
		||||
                if (!localStorage.getItem(checkKey)) {
 | 
			
		||||
                    localStorage.setItem(checkKey, 'true');
 | 
			
		||||
                    const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {});
 | 
			
		||||
                    const result = await callPopup(template, 'confirm', '', { okButton: 'Yes' });
 | 
			
		||||
 | 
			
		||||
                    if (result) {
 | 
			
		||||
                        extension_settings.character_allowed_regex.push(avatar);
 | 
			
		||||
                        await reloadCurrentChat();
 | 
			
		||||
                        saveSettingsDebounced();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    loadRegexScripts();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Workaround for loading in sequence with other extensions
 | 
			
		||||
// NOTE: Always puts extension at the top of the list, but this is fine since it's static
 | 
			
		||||
jQuery(async () => {
 | 
			
		||||
@@ -321,12 +451,32 @@ jQuery(async () => {
 | 
			
		||||
    const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown'));
 | 
			
		||||
    $('#extensions_settings2').append(settingsHtml);
 | 
			
		||||
    $('#open_regex_editor').on('click', function () {
 | 
			
		||||
        onRegexEditorOpenClick(false);
 | 
			
		||||
        onRegexEditorOpenClick(false, false);
 | 
			
		||||
    });
 | 
			
		||||
    $('#open_scoped_editor').on('click', function () {
 | 
			
		||||
        if (this_chid === undefined) {
 | 
			
		||||
            toastr.error('No character selected.');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (selected_group) {
 | 
			
		||||
            toastr.error('Cannot edit scoped scripts in group chats.');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        onRegexEditorOpenClick(false, true);
 | 
			
		||||
    });
 | 
			
		||||
    $('#import_regex_file').on('change', async function () {
 | 
			
		||||
        let target = 'global';
 | 
			
		||||
        const template = $(await renderExtensionTemplateAsync('regex', 'importTarget'));
 | 
			
		||||
        template.find('#regex_import_target_global').on('input', () => target = 'global');
 | 
			
		||||
        template.find('#regex_import_target_scoped').on('input', () => target = 'scoped');
 | 
			
		||||
 | 
			
		||||
        await callPopup(template, 'text');
 | 
			
		||||
 | 
			
		||||
        const inputElement = this instanceof HTMLInputElement && this;
 | 
			
		||||
        for (const file of inputElement.files) {
 | 
			
		||||
            await onRegexImportFileChange(file);
 | 
			
		||||
            await onRegexImportFileChange(file, target === 'scoped');
 | 
			
		||||
        }
 | 
			
		||||
        inputElement.value = '';
 | 
			
		||||
    });
 | 
			
		||||
@@ -334,30 +484,75 @@ jQuery(async () => {
 | 
			
		||||
        $('#import_regex_file').trigger('click');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('#saved_regex_scripts').sortable({
 | 
			
		||||
        delay: getSortableDelay(),
 | 
			
		||||
        stop: function () {
 | 
			
		||||
            let newScripts = [];
 | 
			
		||||
            $('#saved_regex_scripts').children().each(function () {
 | 
			
		||||
                const scriptName = $(this).find('.regex_script_name').text();
 | 
			
		||||
                const existingScript = extension_settings.regex.find((e) => e.scriptName === scriptName);
 | 
			
		||||
                if (existingScript) {
 | 
			
		||||
                    newScripts.push(existingScript);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            extension_settings.regex = newScripts;
 | 
			
		||||
            saveSettingsDebounced();
 | 
			
		||||
 | 
			
		||||
            console.debug('Regex scripts reordered');
 | 
			
		||||
            // TODO: Maybe reload regex scripts after move
 | 
			
		||||
    let sortableDatas = [
 | 
			
		||||
        {
 | 
			
		||||
            selector: '#saved_regex_scripts',
 | 
			
		||||
            setter: x => extension_settings.regex = x,
 | 
			
		||||
            getter: () => extension_settings.regex ?? [],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            selector: '#saved_scoped_scripts',
 | 
			
		||||
            setter: x => writeExtensionField(this_chid, 'regex_scripts', x),
 | 
			
		||||
            getter: () => characters[this_chid]?.data?.extensions?.regex_scripts ?? [],
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
    for (const { selector, setter, getter } of sortableDatas) {
 | 
			
		||||
        $(selector).sortable({
 | 
			
		||||
            delay: getSortableDelay(),
 | 
			
		||||
            stop: async function () {
 | 
			
		||||
                const oldScripts = getter();
 | 
			
		||||
                const newScripts = [];
 | 
			
		||||
                $(selector).children().each(function () {
 | 
			
		||||
                    const id = $(this).attr('id');
 | 
			
		||||
                    const existingScript = oldScripts.find((e) => e.id === id);
 | 
			
		||||
                    if (existingScript) {
 | 
			
		||||
                        newScripts.push(existingScript);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                await setter(newScripts);
 | 
			
		||||
                saveSettingsDebounced();
 | 
			
		||||
 | 
			
		||||
                console.debug(`Regex scripts in ${selector} reordered`);
 | 
			
		||||
                await loadRegexScripts();
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $('#regex_scoped_toggle').on('input', function () {
 | 
			
		||||
        if (this_chid === undefined) {
 | 
			
		||||
            toastr.error('No character selected.');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (selected_group) {
 | 
			
		||||
            toastr.error('Cannot edit scoped scripts in group chats.');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const isEnable = !!$(this).prop('checked');
 | 
			
		||||
        const avatar = characters[this_chid].avatar;
 | 
			
		||||
 | 
			
		||||
        if (isEnable) {
 | 
			
		||||
            if (!extension_settings.character_allowed_regex.includes(avatar)) {
 | 
			
		||||
                extension_settings.character_allowed_regex.push(avatar);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            const index = extension_settings.character_allowed_regex.indexOf(avatar);
 | 
			
		||||
            if (index !== -1) {
 | 
			
		||||
                extension_settings.character_allowed_regex.splice(index, 1);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        saveSettingsDebounced();
 | 
			
		||||
        reloadCurrentChat();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await loadRegexScripts();
 | 
			
		||||
    $('#saved_regex_scripts').sortable('enable');
 | 
			
		||||
 | 
			
		||||
    SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'regex',
 | 
			
		||||
    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
 | 
			
		||||
        name: 'regex',
 | 
			
		||||
        callback: runRegexCallback,
 | 
			
		||||
        returns: 'replaced text',
 | 
			
		||||
        namedArgumentList: [
 | 
			
		||||
@@ -373,4 +568,6 @@ jQuery(async () => {
 | 
			
		||||
        helpString: 'Runs a Regex extension script by name on the provided string. The script must be enabled.',
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    eventSource.on(event_types.CHAT_CHANGED, checkEmbeddedRegexScripts);
 | 
			
		||||
    eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts);
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,12 @@
 | 
			
		||||
        <div class="edit_existing_regex menu_button" data-i18n="[title]ext_regex_edit_script" title="Edit script">
 | 
			
		||||
            <i class="fa-solid fa-pencil"></i>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="move_to_global menu_button" data-i18n="[title]ext_regex_move_to_global" title="Move to global scripts">
 | 
			
		||||
            <i class="fa-solid fa-arrow-up"></i>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="move_to_scoped menu_button" data-i18n="[title]ext_regex_move_to_scoped" title="Move to scoped scripts">
 | 
			
		||||
            <i class="fa-solid fa-arrow-down"></i>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="export_regex menu_button" data-i18n="[title]ext_regex_export_script" title="Export script">
 | 
			
		||||
            <i class="fa-solid fa-file-export"></i>
 | 
			
		||||
        </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,47 @@
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.regex-script-container:empty::after {
 | 
			
		||||
    content: "No scripts found";
 | 
			
		||||
    font-size: 0.95em;
 | 
			
		||||
    opacity: 0.7;
 | 
			
		||||
    display: block;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#scoped_scripts_block {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    transition: opacity 0.2s ease-in-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#scoped_scripts_block .move_to_scoped {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#global_scripts_block .move_to_global {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#scoped_scripts_block:not(:has(#regex_scoped_toggle:checked)) {
 | 
			
		||||
    opacity: 0.5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.enable_scoped:checked ~ .regex-toggle-on {
 | 
			
		||||
    display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.enable_scoped:checked ~ .regex-toggle-off {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.enable_scoped:not(:checked) ~ .regex-toggle-on {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.enable_scoped:not(:checked) ~ .regex-toggle-off {
 | 
			
		||||
    display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.regex-script-label {
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    border: 1px solid var(--SmartThemeBorderColor);
 | 
			
		||||
@@ -23,7 +64,13 @@
 | 
			
		||||
    margin-bottom: 1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input.disable_regex {
 | 
			
		||||
.regex-script-label:has(.disable_regex:checked) .regex_script_name {
 | 
			
		||||
    text-decoration: line-through;
 | 
			
		||||
    filter: grayscale(0.5);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input.disable_regex,
 | 
			
		||||
input.enable_scoped {
 | 
			
		||||
    display: none !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -31,6 +78,12 @@ input.disable_regex {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    opacity: 0.5;
 | 
			
		||||
    filter: grayscale(0.5);
 | 
			
		||||
    transition: opacity 0.2s ease-in-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.regex-toggle-off:hover {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    filter: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.regex-toggle-on {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user