From 6bc9535040bb8f5cab64a4c7c0bfb86dcbc5acb7 Mon Sep 17 00:00:00 2001 From: kingbri Date: Sun, 2 Jul 2023 23:14:23 -0400 Subject: [PATCH 01/11] Popups: Allow substitution of primary button This allows for more flexible popups with options rather than implementing a brand new popup just to change button text. Signed-off-by: kingbri --- public/script.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/script.js b/public/script.js index ff21505ee..332bf7c43 100644 --- a/public/script.js +++ b/public/script.js @@ -5330,7 +5330,7 @@ function onScenarioOverrideRemoveClick() { $(this).closest('.scenario_override').find('.chat_scenario').val('').trigger('input'); } -function callPopup(text, type, inputValue = '') { +function callPopup(text, type, inputValue = '', okButton) { if (type) { popup_type = type; } @@ -5338,30 +5338,30 @@ function callPopup(text, type, inputValue = '') { $("#dialogue_popup_cancel").css("display", "inline-block"); switch (popup_type) { case "avatarToCrop": - $("#dialogue_popup_ok").text("Accept"); + $("#dialogue_popup_ok").text(okButton ?? "Accept"); break; case "text": case "alternate_greeting": case "char_not_selected": - $("#dialogue_popup_ok").text("Ok"); + $("#dialogue_popup_ok").text(okButton ?? "Ok"); $("#dialogue_popup_cancel").css("display", "none"); break; case "new_chat": case "confirm": - $("#dialogue_popup_ok").text("Yes"); + $("#dialogue_popup_ok").text(okButton ?? "Yes"); break; case "del_group": case "rename_chat": case "del_chat": default: - $("#dialogue_popup_ok").text("Delete"); + $("#dialogue_popup_ok").text(okButton ?? "Delete"); } $("#dialogue_popup_input").val(inputValue); if (popup_type == 'input') { $("#dialogue_popup_input").css("display", "block"); - $("#dialogue_popup_ok").text("Save"); + $("#dialogue_popup_ok").text(okButton ?? "Save"); } else { $("#dialogue_popup_input").css("display", "none"); From ef7aa3941baa1465b8aa95a4cb312e112230c0e9 Mon Sep 17 00:00:00 2001 From: kingbri Date: Mon, 3 Jul 2023 21:25:35 -0400 Subject: [PATCH 02/11] Extensions: Add regex engine Regex is a method that is commonly used to find and replace parts of a string using a single pattern. Add support for using regex in SillyTavern which allows users to dynamically change various aspects of the chatting experience. Users are able to choose where a given regex script should apply (both invasive and non-invasive options!). Invasive options alter chat history while non-invasive alters markdown display for the entire chat. A new variable called {{match}} is added in regex scripts which substitutes in the found match from the original find regex script. There is a lot more that can be added to this extension, but for now, this is enough. Signed-off-by: kingbri --- public/script.js | 72 +++++++- public/scripts/extensions.js | 1 + public/scripts/extensions/regex/dropdown.html | 17 ++ public/scripts/extensions/regex/editor.html | 84 ++++++++++ public/scripts/extensions/regex/engine.js | 58 +++++++ public/scripts/extensions/regex/index.js | 155 ++++++++++++++++++ public/scripts/extensions/regex/manifest.json | 11 ++ .../extensions/regex/scriptTemplate.html | 12 ++ public/scripts/extensions/regex/style.css | 37 +++++ public/scripts/slash-commands.js | 24 ++- public/style.css | 4 + 11 files changed, 470 insertions(+), 5 deletions(-) create mode 100644 public/scripts/extensions/regex/dropdown.html create mode 100644 public/scripts/extensions/regex/editor.html create mode 100644 public/scripts/extensions/regex/engine.js create mode 100644 public/scripts/extensions/regex/index.js create mode 100644 public/scripts/extensions/regex/manifest.json create mode 100644 public/scripts/extensions/regex/scriptTemplate.html create mode 100644 public/scripts/extensions/regex/style.css diff --git a/public/script.js b/public/script.js index 332bf7c43..f3cfde278 100644 --- a/public/script.js +++ b/public/script.js @@ -161,6 +161,8 @@ import { context_settings, loadContextTemplatesFromSettings } from "./scripts/co import { markdownExclusionExt } from "./scripts/showdown-exclusion.js"; import { NOTE_MODULE_NAME, metadata_keys, setFloatingPrompt, shouldWIAddPrompt } from "./scripts/extensions/floating-prompt/index.js"; import { deviceInfo } from "./scripts/RossAscends-mods.js"; +import { runRegexScript } from "./scripts/extensions/regex/engine.js"; +import { REGEX_PLACEMENT } from "./scripts/extensions/regex/index.js"; //exporting functions and vars for mods export { @@ -488,6 +490,7 @@ export const event_types = { IMPERSONATE_READY: 'impersonate_ready', CHAT_CHANGED: 'chat_id_changed', GENERATION_STOPPED: 'generation_stopped', + SETTINGS_LOADED: 'settings_loaded', SETTINGS_UPDATED: 'settings_updated', GROUP_UPDATED: 'group_updated', MOVABLE_PANELS_RESET: 'movable_panels_reset', @@ -1108,6 +1111,15 @@ function messageFormatting(mes, ch_name, isSystem, isUser) { mes = mes.replaceAll(substituteParams(power_user.user_prompt_bias), ""); } + extension_settings.regex.forEach((script) => { + if (script.placement.includes(REGEX_PLACEMENT.mdDisplay)) { + const regexResult = runRegexScript(script, mes); + if (regexResult) { + mes = regexResult; + } + } + }); + if (power_user.auto_fix_generated_markdown) { mes = fixMarkdown(mes); } @@ -2865,6 +2877,15 @@ export function replaceBiasMarkup(str) { } export async function sendMessageAsUser(textareaText, messageBias) { + extension_settings.regex.forEach((script) => { + if (script.placement.includes(REGEX_PLACEMENT.userInput)) { + const regexResult = runRegexScript(script, textareaText); + if (regexResult) { + textareaText = regexResult; + } + } + }); + chat[chat.length] = {}; chat[chat.length - 1]['name'] = name1; chat[chat.length - 1]['is_user'] = true; @@ -3445,11 +3466,28 @@ function extractMessageFromData(data) { } function cleanUpMessage(getMessage, isImpersonate, displayIncompleteSentences = false) { - // Append the user bias first before trimming anything else - if (power_user.user_prompt_bias && power_user.user_prompt_bias.length !== 0) { + // Add the prompt bias before anything else + if ( + power_user.user_prompt_bias && + !isImpersonate && + power_user.user_prompt_bias.length !== 0 + ) { getMessage = substituteParams(power_user.user_prompt_bias) + getMessage; } + // Regex uses vars, so add before formatting + extension_settings.regex.forEach((script) => { + if ( + (script.placement.includes(REGEX_PLACEMENT.aiOutput) && !isImpersonate) || + (script.placement.includes(REGEX_PLACEMENT.userInput) && isImpersonate) + ) { + const regexResult = runRegexScript(script, getMessage); + if (regexResult) { + getMessage = regexResult; + } + } + }); + if (!displayIncompleteSentences && power_user.trim_sentences) { getMessage = end_trim_to_sentence(getMessage, power_user.include_newline); } @@ -4771,6 +4809,8 @@ async function getSettings(type) { } if (!is_checked_colab) isColab(); + + eventSource.emit(event_types.SETTINGS_LOADED); } function selectKoboldGuiPreset() { @@ -4859,13 +4899,26 @@ function setCharacterBlockHeight() { function updateMessage(div) { const mesBlock = div.closest(".mes_block"); let text = mesBlock.find(".edit_textarea").val(); + const mes = chat[this_edit_mes_id]; + + extension_settings.regex.forEach((script) => { + if (script.runOnEdit && ( + (script.placement.includes(REGEX_PLACEMENT.aiOutput) && mes.is_name) || + (script.placement.includes(REGEX_PLACEMENT.userInput) && mes.is_user) || + (script.placement.includes(REGEX_PLACEMENT.system) && mes.extra?.type === "narrator") + )) { + const regexResult = runRegexScript(script, text); + if (regexResult) { + text = regexResult; + } + } + }); if (power_user.trim_spaces) { text = text.trim(); } const bias = extractMessageBias(text); - const mes = chat[this_edit_mes_id]; mes["mes"] = text; if (mes["swipe_id"] !== undefined) { mes["swipes"][mes["swipe_id"]] = text; @@ -5959,9 +6012,11 @@ async function createOrEditCharacter(e) { success: async function (html) { if (chat.length === 1 && !selected_group) { var this_ch_mes = default_ch_mes; + if ($("#firstmessage_textarea").val() != "") { this_ch_mes = $("#firstmessage_textarea").val(); } + if ( this_ch_mes != $.trim( @@ -5972,6 +6027,17 @@ async function createOrEditCharacter(e) { .text() ) ) { + // MARK - kingbri: Regex on character greeting message + // May need to be placed somewhere else + extension_settings.regex.forEach((script) => { + if (script.placement.includes(REGEX_PLACEMENT.aiOutput)) { + const regexResult = runRegexScript(script, this_ch_mes); + if (regexResult) { + this_ch_mes = regexResult + } + } + }); + clearChat(); chat.length = 0; chat[0] = {}; diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index d4c1ea947..662a599fc 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -56,6 +56,7 @@ const extension_settings = { caption: {}, expressions: {}, dice: {}, + regex: [], tts: {}, sd: {}, chromadb: {}, diff --git a/public/scripts/extensions/regex/dropdown.html b/public/scripts/extensions/regex/dropdown.html new file mode 100644 index 000000000..7a488321e --- /dev/null +++ b/public/scripts/extensions/regex/dropdown.html @@ -0,0 +1,17 @@ +
+
+
+ Regex +
+
+
+ +
+ +
+
+
+
diff --git a/public/scripts/extensions/regex/editor.html b/public/scripts/extensions/regex/editor.html new file mode 100644 index 000000000..7c81ed717 --- /dev/null +++ b/public/scripts/extensions/regex/editor.html @@ -0,0 +1,84 @@ +
+
+

Regex Editor

+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ + +
+
+
diff --git a/public/scripts/extensions/regex/engine.js b/public/scripts/extensions/regex/engine.js new file mode 100644 index 000000000..74c197fa6 --- /dev/null +++ b/public/scripts/extensions/regex/engine.js @@ -0,0 +1,58 @@ +import { substituteParams } from "../../../script.js"; +export { + runRegexScript +} + +// From: https://github.com/IonicaBizau/regex-parser.js/blob/master/lib/index.js +function regexFromString(input) { + // Parse input + var m = input.match(/(\/?)(.+)\1([a-z]*)/i); + + // Invalid flags + if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) { + return RegExp(input); + } + + // Create the regular expression + return new RegExp(m[2], m[3]); +} + +// Runs the provided regex script on the given string +function runRegexScript(regexScript, rawString) { + if (!!(regexScript.disabled)) { + return; + } + + let match; + let newString; + const findRegex = regexFromString(regexScript.findRegex); + while ((match = findRegex.exec(rawString)) !== null) { + const fencedMatch = match[0]; + const capturedMatch = match[1]; + + // TODO: Use substrings for replacement. But not necessary at this time. + // A substring is from match.index to match.index + match[0].length or fencedMatch.length + const subReplaceString = substituteRegexParams(regexScript.replaceString, { regexMatch: capturedMatch ?? fencedMatch }); + if (!newString) { + newString = rawString.replace(fencedMatch, subReplaceString); + } else { + newString = newString.replace(fencedMatch, subReplaceString); + } + + // If the regex isn't global, break out of the loop + if (!findRegex.flags.includes('g')) { + break; + } + } + + return newString; +} + +// Substitutes parameters +function substituteRegexParams(rawString, { regexMatch }) { + let finalString = rawString; + finalString = finalString.replace("{{match}}", regexMatch); + finalString = substituteParams(finalString); + + return finalString; +} diff --git a/public/scripts/extensions/regex/index.js b/public/scripts/extensions/regex/index.js new file mode 100644 index 000000000..24317e025 --- /dev/null +++ b/public/scripts/extensions/regex/index.js @@ -0,0 +1,155 @@ +import { callPopup, eventSource, event_types, saveSettingsDebounced } from "../../../script.js"; +import { extension_settings } from "../../extensions.js"; +import { uuidv4 } from "../../utils.js"; +export { REGEX_PLACEMENT } + +const REGEX_PLACEMENT = { + mdDisplay: 0, + userInput: 1, + aiOutput: 2, + system: 3, + sendas: 4 +} + +async function saveRegexScript(regexScript, existingScriptIndex) { + // If the script index is undefined or already exists + // Don't fire this if editing an existing script + if (existingScriptIndex === -1) { + if (!regexScript.scriptName) { + toastr.error(`Could not save regex: The script name was undefined or empty!`); + return; + } + + if (extension_settings.regex.find((e) => e.scriptName === regexScript.scriptName)) { + toastr.error(`Could not save regex: The name ${regexScript.scriptName} already exists.`); + return; + } + } + + if (regexScript.placement.length === 0) { + toastr.error(`Could not save regex: One placement checkbox must be selected!`); + return; + } + + if (existingScriptIndex !== -1) { + extension_settings.regex[existingScriptIndex] = regexScript; + } else { + extension_settings.regex.push(regexScript); + } + + saveSettingsDebounced(); + await loadRegexScripts(); +} + +async function deleteRegexScript({ existingId }) { + let scriptName = $(`#${existingId}`).find('.regex_script_name').text(); + + const existingScriptIndex = extension_settings.regex.findIndex((script) => script.scriptName === scriptName); + if (!existingScriptIndex || existingScriptIndex !== -1) { + extension_settings.regex.splice(existingScriptIndex, 1); + + saveSettingsDebounced(); + await loadRegexScripts(); + } +} + +async function loadRegexScripts() { + $("#saved_regex_scripts").empty(); + + const scriptTemplate = $(await $.get("scripts/extensions/regex/scriptTemplate.html")); + + extension_settings.regex.forEach((script) => { + // Have to clone here + const scriptHtml = scriptTemplate.clone(); + scriptHtml.attr('id', uuidv4()); + scriptHtml.find('.regex_script_name').text(script.scriptName); + scriptHtml.find('.edit_existing_regex').on('click', async function() { + await onRegexEditorOpenClick(scriptHtml.attr("id")); + }); + scriptHtml.find('.delete_regex').on('click', async function() { + await deleteRegexScript({ existingId: scriptHtml.attr("id") }); + }); + + $("#saved_regex_scripts").append(scriptHtml); + }); +} + +async function onRegexEditorOpenClick(existingId) { + const editorHtml = $(await $.get("scripts/extensions/regex/editor.html")); + + // 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); + if (existingScriptIndex !== -1) { + const existingScript = extension_settings.regex[existingScriptIndex]; + editorHtml.find(`.regex_script_name`).val(existingScript.scriptName); + editorHtml.find(`.find_regex`).val(existingScript.findRegex); + editorHtml.find(`.regex_replace_string`).val(existingScript.replaceString); + editorHtml + .find(`input[name="disabled"]`) + .prop("checked", existingScript.disabled); + editorHtml + .find(`input[name="run_on_edit"]`) + .prop("checked", existingScript.runOnEdit); + + existingScript.placement.forEach((element) => { + editorHtml + .find(`input[name="replace_position"][value="${element}"]`) + .prop("checked", true); + }); + } + } else { + editorHtml + .find(`input[name="run_on_edit"]`) + .prop("checked", true); + + editorHtml + .find(`input[name="replace_position"][value="0"]`) + .prop("checked", true); + } + + const popupResult = await callPopup(editorHtml, "confirm", undefined, "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(), + placement: + editorHtml + .find(`input[name="replace_position"]`) + .filter(":checked") + .map(function() { return parseInt($(this).val()) }) + .get() + .filter((e) => e !== NaN) ?? [], + disabled: + editorHtml + .find(`input[name="disabled"]`) + .prop("checked"), + runOnEdit: + editorHtml + .find(`input[name="run_on_edit"]`) + .prop("checked") + }; + + saveRegexScript(newRegexScript, existingScriptIndex); + } +} + +function hookToEvents() { + eventSource.on(event_types.SETTINGS_LOADED, async function () { + await loadRegexScripts(); + }); +} + +jQuery(async () => { + const settingsHtml = await $.get("scripts/extensions/regex/dropdown.html"); + $("#extensions_settings2").append(settingsHtml); + $("#open_regex_editor").on("click", function() { + onRegexEditorOpenClick(false); + }); + + // Listen to event source after 1ms + setTimeout(() => hookToEvents(), 1) +}); diff --git a/public/scripts/extensions/regex/manifest.json b/public/scripts/extensions/regex/manifest.json new file mode 100644 index 000000000..d6ac3056f --- /dev/null +++ b/public/scripts/extensions/regex/manifest.json @@ -0,0 +1,11 @@ +{ + "display_name": "Regex", + "loading_order": 12, + "requires": [], + "optional": [], + "js": "index.js", + "css": "style.css", + "author": "kingbri", + "version": "1.0.0", + "homePage": "https://github.com/SillyTavern/SillyTavern" +} diff --git a/public/scripts/extensions/regex/scriptTemplate.html b/public/scripts/extensions/regex/scriptTemplate.html new file mode 100644 index 000000000..761b94763 --- /dev/null +++ b/public/scripts/extensions/regex/scriptTemplate.html @@ -0,0 +1,12 @@ +
+ +
+
+ + +
+
diff --git a/public/scripts/extensions/regex/style.css b/public/scripts/extensions/regex/style.css new file mode 100644 index 000000000..890c8266d --- /dev/null +++ b/public/scripts/extensions/regex/style.css @@ -0,0 +1,37 @@ +.align-start { + align-items: start; +} + +.regex_settings .menu_button { + width: fit-content; + display: flex; + gap: 10px; + flex-direction: row; +} + +.align-center { + align-items: center; +} + +.regex-script-container { + margin-top: 10px; + margin-bottom: 10px; +} + +.regex-script-label { + align-items: center; + border: 1px solid rgba(128, 128, 128, 0.5); + border-radius: 10px; + padding: 0 5px; + margin-top: 1px; + margin-bottom: 1px; + flex-wrap: nowrap; +} + +.align-self-center { + align-self: center; +} + +.flex-grow { + flex-grow: 1; +} diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index fd3d2123a..f9b2956db 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -22,6 +22,9 @@ import { } from "../script.js"; import { humanizedDateTime } from "./RossAscends-mods.js"; import { resetSelectedGroup } from "./group-chats.js"; +import { extension_settings } from "./extensions.js"; +import { runRegexScript } from "./extensions/regex/engine.js"; +import { REGEX_PLACEMENT } from "./extensions/regex/index.js"; import { chat_styles, power_user } from "./power-user.js"; export { executeSlashCommands, @@ -218,14 +221,22 @@ async function sendMessageAs(_, text) { } const parts = text.split('\n'); - if (parts.length <= 1) { toastr.warning('Both character name and message are required. Separate them with a new line.'); return; } const name = parts.shift().trim(); - const mesText = parts.join('\n').trim(); + let mesText = parts.join('\n').trim(); + extension_settings.regex.forEach((script) => { + if (script.placement.includes(REGEX_PLACEMENT.sendas)) { + const regexResult = runRegexScript(script, mesText); + if (regexResult) { + mesText = regexResult; + } + } + }); + // Messages that do nothing but set bias will be hidden from the context const bias = extractMessageBias(mesText); const isSystem = replaceBiasMarkup(mesText).trim().length === 0; @@ -268,6 +279,15 @@ async function sendNarratorMessage(_, text) { return; } + extension_settings.regex.forEach((script) => { + if (script.placement.includes(REGEX_PLACEMENT.system)) { + const regexResult = runRegexScript(script, text); + if (regexResult) { + text = regexResult; + } + } + }); + const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT; // Messages that do nothing but set bias will be hidden from the context const bias = extractMessageBias(text); diff --git a/public/style.css b/public/style.css index 98081553e..8bb0c4dab 100644 --- a/public/style.css +++ b/public/style.css @@ -4060,6 +4060,10 @@ toolcool-color-picker { justify-content: space-around; } +.justifyContentFlexStart { + justify-content: flex-start; +} + .justifyContentFlexEnd { justify-content: flex-end; } From ee6a6603a3b5a7c5e37f096888fa623b2f3eef64 Mon Sep 17 00:00:00 2001 From: kingbri Date: Tue, 4 Jul 2023 02:00:03 -0400 Subject: [PATCH 03/11] Regex: Add trimStrings option Sometimes the matched regex string needs to be pruned before replacement. Add a method for the user to provide strings which globally trims a regex match before any replacement is done. Example without trim: input - regex - /<([^>]*)>/g output - With trim: input - regex - /<([^>]*)>/g trim - ["{{char}}'s thoughts: "] output - Signed-off-by: kingbri --- public/scripts/extensions/regex/editor.html | 21 ++++++++++++---- public/scripts/extensions/regex/engine.js | 27 ++++++++++++++++++--- public/scripts/extensions/regex/index.js | 6 +++-- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/public/scripts/extensions/regex/editor.html b/public/scripts/extensions/regex/editor.html index 7c81ed717..dbf7b553a 100644 --- a/public/scripts/extensions/regex/editor.html +++ b/public/scripts/extensions/regex/editor.html @@ -8,7 +8,7 @@ Script Name
- +
@@ -16,19 +16,30 @@ Find Regex
- +
-
+
+ +
+
diff --git a/public/scripts/extensions/regex/engine.js b/public/scripts/extensions/regex/engine.js index 74c197fa6..e2296f610 100644 --- a/public/scripts/extensions/regex/engine.js +++ b/public/scripts/extensions/regex/engine.js @@ -30,9 +30,19 @@ function runRegexScript(regexScript, rawString) { const fencedMatch = match[0]; const capturedMatch = match[1]; + let trimCapturedMatch; + let trimFencedMatch; + if (capturedMatch) { + const tempTrimCapture = filterString(capturedMatch, regexScript.trimStrings); + trimFencedMatch = fencedMatch.replaceAll(capturedMatch, tempTrimCapture); + trimCapturedMatch = tempTrimCapture; + } else { + trimFencedMatch = filterString(fencedMatch, regexScript.trimStrings); + } + // TODO: Use substrings for replacement. But not necessary at this time. // A substring is from match.index to match.index + match[0].length or fencedMatch.length - const subReplaceString = substituteRegexParams(regexScript.replaceString, { regexMatch: capturedMatch ?? fencedMatch }); + const subReplaceString = substituteRegexParams(regexScript.replaceString, trimCapturedMatch ?? trimFencedMatch); if (!newString) { newString = rawString.replace(fencedMatch, subReplaceString); } else { @@ -48,8 +58,19 @@ function runRegexScript(regexScript, rawString) { return newString; } -// Substitutes parameters -function substituteRegexParams(rawString, { regexMatch }) { +// Filters anything to trim from the regex match +function filterString(rawString, trimStrings) { + let finalString = rawString; + trimStrings.forEach((trimString) => { + const subTrimString = substituteParams(trimString); + finalString = finalString.replaceAll(subTrimString, ""); + }); + + return finalString; +} + +// Substitutes regex-specific and normal parameters +function substituteRegexParams(rawString, regexMatch) { let finalString = rawString; finalString = finalString.replace("{{match}}", regexMatch); finalString = substituteParams(finalString); diff --git a/public/scripts/extensions/regex/index.js b/public/scripts/extensions/regex/index.js index 24317e025..e7bb56b11 100644 --- a/public/scripts/extensions/regex/index.js +++ b/public/scripts/extensions/regex/index.js @@ -87,9 +87,10 @@ async function onRegexEditorOpenClick(existingId) { editorHtml.find(`.regex_script_name`).val(existingScript.scriptName); editorHtml.find(`.find_regex`).val(existingScript.findRegex); editorHtml.find(`.regex_replace_string`).val(existingScript.replaceString); + editorHtml.find(`.regex_trim_strings`).val(existingScript.trimStrings?.join("\n") || []); editorHtml .find(`input[name="disabled"]`) - .prop("checked", existingScript.disabled); + .prop("checked", existingScript.disabled ?? false); editorHtml .find(`input[name="run_on_edit"]`) .prop("checked", existingScript.runOnEdit); @@ -116,13 +117,14 @@ async function onRegexEditorOpenClick(existingId) { scriptName: editorHtml.find(".regex_script_name").val(), findRegex: editorHtml.find(".find_regex").val(), replaceString: editorHtml.find(".regex_replace_string").val(), + trimStrings: editorHtml.find(".regex_trim_strings").val().split("\n").filter((e) => e.length !== 0) || [], placement: editorHtml .find(`input[name="replace_position"]`) .filter(":checked") .map(function() { return parseInt($(this).val()) }) .get() - .filter((e) => e !== NaN) ?? [], + .filter((e) => e !== NaN) || [], disabled: editorHtml .find(`input[name="disabled"]`) From b362dba7264d4e9e3bbfae2cc2a7255a46bc2271 Mon Sep 17 00:00:00 2001 From: kingbri Date: Tue, 4 Jul 2023 11:52:28 -0400 Subject: [PATCH 04/11] Regex: Fix edit message hook The only way to distinguish between a user and AI is if the is_user property is changed. Signed-off-by: kingbri --- public/script.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/public/script.js b/public/script.js index f3cfde278..ff63fe40c 100644 --- a/public/script.js +++ b/public/script.js @@ -4900,11 +4900,10 @@ function updateMessage(div) { const mesBlock = div.closest(".mes_block"); let text = mesBlock.find(".edit_textarea").val(); const mes = chat[this_edit_mes_id]; - extension_settings.regex.forEach((script) => { if (script.runOnEdit && ( - (script.placement.includes(REGEX_PLACEMENT.aiOutput) && mes.is_name) || - (script.placement.includes(REGEX_PLACEMENT.userInput) && mes.is_user) || + (script.placement.includes(REGEX_PLACEMENT.aiOutput) && (mes.is_name && !mes.is_user)) || + (script.placement.includes(REGEX_PLACEMENT.userInput) && (mes.is_name && mes.is_user)) || (script.placement.includes(REGEX_PLACEMENT.system) && mes.extra?.type === "narrator") )) { const regexResult = runRegexScript(script, text); From 0f8d07053e22722235413fe096b3619664cc830b Mon Sep 17 00:00:00 2001 From: kingbri Date: Tue, 4 Jul 2023 15:29:29 -0400 Subject: [PATCH 05/11] Regex: Don't wrap scriptTemplate elements Fix some UI for flex element wrapping. Text overflow for a script name is now truncated if it exceeds one line of text. Also fix how extension settings are laid out to prevent unnecessary flex resizing. Signed-off-by: kingbri --- public/index.html | 4 ++-- public/scripts/extensions/regex/scriptTemplate.html | 7 +++---- public/scripts/extensions/regex/style.css | 11 ++++++++++- public/style.css | 4 ++++ 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/public/index.html b/public/index.html index 9c2a469ee..88298fbdd 100644 --- a/public/index.html +++ b/public/index.html @@ -2424,9 +2424,9 @@ -
+
-
+
diff --git a/public/scripts/extensions/regex/scriptTemplate.html b/public/scripts/extensions/regex/scriptTemplate.html index 761b94763..995c37f91 100644 --- a/public/scripts/extensions/regex/scriptTemplate.html +++ b/public/scripts/extensions/regex/scriptTemplate.html @@ -1,7 +1,6 @@ -
- -
-
+
+
+
diff --git a/public/scripts/extensions/regex/style.css b/public/scripts/extensions/regex/style.css index 890c8266d..c7b5182bb 100644 --- a/public/scripts/extensions/regex/style.css +++ b/public/scripts/extensions/regex/style.css @@ -25,7 +25,6 @@ padding: 0 5px; margin-top: 1px; margin-bottom: 1px; - flex-wrap: nowrap; } .align-self-center { @@ -35,3 +34,13 @@ .flex-grow { flex-grow: 1; } + +.flex-nowrap { + flex-wrap: nowrap; +} + +.overflow-hidden { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} diff --git a/public/style.css b/public/style.css index 8bb0c4dab..476eab0b1 100644 --- a/public/style.css +++ b/public/style.css @@ -4091,6 +4091,10 @@ toolcool-color-picker { width: 100%; } +.wide50p { + width: 50%; +} + .wide50px { width: 50px; } From e6eae0aad122d54a42a8e236bf24b35a47deb1c9 Mon Sep 17 00:00:00 2001 From: kingbri Date: Tue, 4 Jul 2023 16:27:12 -0400 Subject: [PATCH 06/11] Regex: Fix script duplication on edit Editing a script can bypass the unique naming system by just renaming the script to another one. This change ensures that no two script names can be the same by checking if the existing index and found indices differ. If something goes wrong, it would be better to use a filter and a map and then check the index array length/includes. FindIndex is used here for efficiency's sake since each array index is unique. Signed-off-by: kingbri --- public/scripts/extensions/regex/index.js | 26 +++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/public/scripts/extensions/regex/index.js b/public/scripts/extensions/regex/index.js index e7bb56b11..81d349793 100644 --- a/public/scripts/extensions/regex/index.js +++ b/public/scripts/extensions/regex/index.js @@ -12,22 +12,38 @@ const REGEX_PLACEMENT = { } async function saveRegexScript(regexScript, existingScriptIndex) { - // If the script index is undefined or already exists - // Don't fire this if editing an existing script + // If not editing if (existingScriptIndex === -1) { + // Is the script name undefined? if (!regexScript.scriptName) { - toastr.error(`Could not save regex: The script name was undefined or empty!`); + toastr.error(`Could not save regex script: The script name was undefined or empty!`); return; } + // Does the script name already exist? if (extension_settings.regex.find((e) => e.scriptName === regexScript.scriptName)) { - toastr.error(`Could not save regex: The name ${regexScript.scriptName} already exists.`); + 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.error(`Could not save regex script: A find regex is required!`); + return; + } + + // Is there someplace to place results? if (regexScript.placement.length === 0) { - toastr.error(`Could not save regex: One placement checkbox must be selected!`); + toastr.error(`Could not save regex script: One placement checkbox must be selected!`); return; } From 7c0222a15bbae33cd61616630144e9659637efdd Mon Sep 17 00:00:00 2001 From: kingbri Date: Tue, 4 Jul 2023 20:58:18 -0400 Subject: [PATCH 07/11] Regex: Migrate CSS styling Universal styling has been moved to the main styles.css. Signed-off-by: kingbri --- public/scripts/extensions/regex/editor.html | 4 +-- .../extensions/regex/scriptTemplate.html | 6 ++--- public/scripts/extensions/regex/style.css | 26 ------------------- public/style.css | 10 +++++++ 4 files changed, 15 insertions(+), 31 deletions(-) diff --git a/public/scripts/extensions/regex/editor.html b/public/scripts/extensions/regex/editor.html index dbf7b553a..e19d11777 100644 --- a/public/scripts/extensions/regex/editor.html +++ b/public/scripts/extensions/regex/editor.html @@ -46,7 +46,7 @@
-