diff --git a/public/index.html b/public/index.html index 52656b56d..c7e0311d3 100644 --- a/public/index.html +++ b/public/index.html @@ -2428,9 +2428,9 @@ -
+
-
+
diff --git a/public/script.js b/public/script.js index 1f41409a5..ede347718 100644 --- a/public/script.js +++ b/public/script.js @@ -165,6 +165,7 @@ 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 { getRegexedString, regex_placement } from "./scripts/extensions/regex/engine.js"; //exporting functions and vars for mods export { @@ -241,6 +242,26 @@ export { // API OBJECT FOR EXTERNAL WIRING window["SillyTavern"] = {}; +// Event source init +export const event_types = { + EXTRAS_CONNECTED: 'extras_connected', + MESSAGE_SWIPED: 'message_swiped', + MESSAGE_SENT: 'message_sent', + MESSAGE_RECEIVED: 'message_received', + MESSAGE_EDITED: 'message_edited', + MESSAGE_DELETED: 'message_deleted', + IMPERSONATE_READY: 'impersonate_ready', + CHAT_CHANGED: 'chat_id_changed', + GENERATION_STOPPED: 'generation_stopped', + EXTENSIONS_FIRST_LOAD: 'extensions_first_load', + SETTINGS_LOADED: 'settings_loaded', + SETTINGS_UPDATED: 'settings_updated', + GROUP_UPDATED: 'group_updated', + MOVABLE_PANELS_RESET: 'movable_panels_reset', +} + +export const eventSource = new EventEmitter(); + const gpt3 = new GPT3BrowserTokenizer({ type: 'gpt3' }); hljs.addPlugin({ "before:highlightElement": ({ el }) => { el.textContent = el.innerText } }); @@ -482,23 +503,6 @@ const system_messages = { }, }; -export const event_types = { - EXTRAS_CONNECTED: 'extras_connected', - MESSAGE_SWIPED: 'message_swiped', - MESSAGE_SENT: 'message_sent', - MESSAGE_RECEIVED: 'message_received', - MESSAGE_EDITED: 'message_edited', - MESSAGE_DELETED: 'message_deleted', - IMPERSONATE_READY: 'impersonate_ready', - CHAT_CHANGED: 'chat_id_changed', - GENERATION_STOPPED: 'generation_stopped', - SETTINGS_UPDATED: 'settings_updated', - GROUP_UPDATED: 'group_updated', - MOVABLE_PANELS_RESET: 'movable_panels_reset', -} - -export const eventSource = new EventEmitter(); - $(document).ajaxError(function myErrorHandler(_, xhr) { if (xhr.status == 403) { toastr.warning("doubleCsrf errors in console are NORMAL in this case. Just reload the page or close this tab.", "Looks like you've opened SillyTavern in another browser tab", { timeOut: 0, extendedTimeOut: 0, preventDuplicates: true }); @@ -1112,6 +1116,11 @@ function messageFormatting(mes, ch_name, isSystem, isUser) { mes = mes.replaceAll(substituteParams(power_user.user_prompt_bias), ""); } + const regexResult = getRegexedString(mes, regex_placement.MD_DISPLAY); + if (regexResult) { + mes = regexResult; + } + if (power_user.auto_fix_generated_markdown) { mes = fixMarkdown(mes); } @@ -2869,6 +2878,11 @@ export function replaceBiasMarkup(str) { } export async function sendMessageAsUser(textareaText, messageBias) { + const regexResult = getRegexedString(textareaText, regex_placement.USER_INPUT); + if (regexResult) { + textareaText = regexResult; + } + chat[chat.length] = {}; chat[chat.length - 1]['name'] = name1; chat[chat.length - 1]['is_user'] = true; @@ -3449,11 +3463,21 @@ 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 + const regexResult = getRegexedString(getMessage, isImpersonate ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT); + if (regexResult) { + getMessage = regexResult; + } + if (!displayIncompleteSentences && power_user.trim_sentences) { getMessage = end_trim_to_sentence(getMessage, power_user.include_newline); } @@ -4776,10 +4800,13 @@ async function getSettings(type) { if (data.enable_extensions) { await loadExtensionSettings(settings); + eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED); } } if (!is_checked_colab) isColab(); + + eventSource.emit(event_types.SETTINGS_LOADED); } function selectKoboldGuiPreset() { @@ -4868,13 +4895,34 @@ 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]; + let regexPlacement; + if (mes.is_name && !mes.is_user && mes.name !== name2) { + regexPlacement = regex_placement.SENDAS; + } else if (mes.is_name && !mes.is_user) { + regexPlacement = regex_placement.AI_OUTPUT; + } else if (mes.is_name && mes.is_user) { + regexPlacement = regex_placement.USER_INPUT; + } else if (mes.extra?.type === "narrator") { + regexPlacement = regex_placement.SYSTEM; + } + + const regexResult = getRegexedString( + text, + regexPlacement, + { + characterOverride: regexPlacement === regex_placement.SENDAS ? mes.name : undefined + } + ); + 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; @@ -5341,7 +5389,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; } @@ -5349,30 +5397,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"); @@ -5970,9 +6018,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( @@ -5983,6 +6033,13 @@ async function createOrEditCharacter(e) { .text() ) ) { + // MARK - kingbri: Regex on character greeting message + // May need to be placed somewhere else + const regexResult = getRegexedString(this_ch_mes, regex_placement.AI_OUTPUT); + 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..b43f622ed 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: {}, @@ -414,6 +415,7 @@ async function loadExtensionSettings(settings) { $("#extensions_autoconnect").prop('checked', extension_settings.autoConnect); // Activate offline extensions + eventSource.emit(event_types.EXTENSIONS_FIRST_LOAD); extensionNames = await discoverExtensions(); manifests = await getManifests(extensionNames) await activateExtensions(); diff --git a/public/scripts/extensions/floating-prompt/index.js b/public/scripts/extensions/floating-prompt/index.js index d910164ea..dbf3eb075 100644 --- a/public/scripts/extensions/floating-prompt/index.js +++ b/public/scripts/extensions/floating-prompt/index.js @@ -9,7 +9,7 @@ import { import { selected_group } from "../../group-chats.js"; import { ModuleWorkerWrapper, extension_settings, getContext, saveMetadataDebounced } from "../../extensions.js"; import { registerSlashCommand } from "../../slash-commands.js"; -import { getCharaFilename, debounce } from "../../utils.js"; +import { getCharaFilename, debounce, waitUntilCondition } from "../../utils.js"; export { MODULE_NAME as NOTE_MODULE_NAME }; const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory @@ -339,136 +339,138 @@ function onChatChanged() { $('#extension_floating_default_token_counter').text(tokenCounter3); } -//for some reason exporting metadata_keys for WI usage caused this to throw errors -//"accessing eventSource before initialization" -//putting it on a 1ms Timeout solved this. -setTimeout(function () { - function addExtensionsSettings() { - const settingsHtml = ` -
-
-
-
-
-
-
-
- Author's Note -
+function addExtensionsSettings() { + const settingsHtml = ` +
+
+
+
+
+
+
+
+ Author's Note +
+
+
+ + Unique to this chat.
+ Bookmarks inherit the Note from their parent, and can be changed individually after that.
+
+ + +
Tokens: 0
+ +
+ + +
+ + + + + (0 = Disable, 1 = Always) +
+ + User inputs until next insertion: (disabled) + +
+
+
+
+
+ Character Author's Note +
- - Unique to this chat.
- Bookmarks inherit the Note from their parent, and can be changed individually after that.
-
+ Will be automatically added as the author's note for this character. Will be used in groups, but can't be modified when a group chat is open. - -
Tokens: 0
+ +
Tokens: 0
+
+
- - - - - (0 = Disable, 1 = Always) -
- - User inputs until next insertion: (disabled) - -
-
-
-
- Character Author's Note -
-
-
- Will be automatically added as the author's note for this character. Will be used in groups, but can't be modified when a group chat is open. - - -
Tokens: 0
- - -
- - - -
-
+
+
+
+
+ Default Author's Note +
-
-
-
- Default Author's Note -
-
-
- Will be automatically added as the Author's Note for all new chats. +
+ Will be automatically added as the Author's Note for all new chats. - -
Tokens: 0
-
+ +
Tokens: 0
- `; - - const ANButtonHtml = ` - - - Author's Note - +
`; - $('#options .options-content').prepend(ANButtonHtml); - $('#movingDivs').append(settingsHtml); - $('#extension_floating_prompt').on('input', onExtensionFloatingPromptInput); - $('#extension_floating_interval').on('input', onExtensionFloatingIntervalInput); - $('#extension_floating_depth').on('input', onExtensionFloatingDepthInput); - $('#extension_floating_chara').on('input', onExtensionFloatingCharaPromptInput); - $('#extension_use_floating_chara').on('input', onExtensionFloatingCharaCheckboxChanged); - $('#extension_floating_default').on('input', onExtensionFloatingDefaultInput); - $('input[name="extension_floating_position"]').on('change', onExtensionFloatingPositionInput); - $('input[name="extension_floating_char_position"]').on('change', onExtensionFloatingCharPositionInput); - $('#ANClose').on('click', function () { - $("#floatingPrompt").transition({ - opacity: 0, - duration: 200, - easing: 'ease-in-out', - }); - setTimeout(function () { $('#floatingPrompt').hide() }, 200); - }) - $("#option_toggle_AN").on('click', onANMenuItemClick); - } - addExtensionsSettings(); + const ANButtonHtml = ` + + + Author's Note + + `; + + $('#options .options-content').prepend(ANButtonHtml); + $('#movingDivs').append(settingsHtml); + $('#extension_floating_prompt').on('input', onExtensionFloatingPromptInput); + $('#extension_floating_interval').on('input', onExtensionFloatingIntervalInput); + $('#extension_floating_depth').on('input', onExtensionFloatingDepthInput); + $('#extension_floating_chara').on('input', onExtensionFloatingCharaPromptInput); + $('#extension_use_floating_chara').on('input', onExtensionFloatingCharaCheckboxChanged); + $('#extension_floating_default').on('input', onExtensionFloatingDefaultInput); + $('input[name="extension_floating_position"]').on('change', onExtensionFloatingPositionInput); + $('input[name="extension_floating_char_position"]').on('change', onExtensionFloatingCharPositionInput); + $('#ANClose').on('click', function () { + $("#floatingPrompt").transition({ + opacity: 0, + duration: 200, + easing: 'ease-in-out', + }); + setTimeout(function () { $('#floatingPrompt').hide() }, 200); + }) + $("#option_toggle_AN").on('click', onANMenuItemClick); +} + +// Inject extension when extensions_activating is fired +// Inserts the extension first since it's statically imported +jQuery(async () => { + await waitUntilCondition(() => eventSource !== undefined); + eventSource.on(event_types.EXTENSIONS_FIRST_LOAD, addExtensionsSettings); + registerSlashCommand('note', setNoteTextCommand, [], "(text) – sets an author's note for the currently selected chat", true, true); registerSlashCommand('depth', setNoteDepthCommand, [], "(number) – sets an author's note depth for in-chat positioning", true, true); registerSlashCommand('freq', setNoteIntervalCommand, ['interval'], "(number) – sets an author's note insertion frequency", true, true); registerSlashCommand('pos', setNotePositionCommand, ['position'], "(chat or scenario) – sets an author's note position", true, true); eventSource.on(event_types.CHAT_CHANGED, onChatChanged); -}, 1); +}); 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..31bb8eada --- /dev/null +++ b/public/scripts/extensions/regex/editor.html @@ -0,0 +1,99 @@ +
+
+

Regex Editor

+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ + +
+
+
diff --git a/public/scripts/extensions/regex/engine.js b/public/scripts/extensions/regex/engine.js new file mode 100644 index 000000000..ac7d9426c --- /dev/null +++ b/public/scripts/extensions/regex/engine.js @@ -0,0 +1,105 @@ +import { substituteParams } from "../../../script.js"; +import { extension_settings } from "../../extensions.js"; +export { + regex_placement, + getRegexedString, + runRegexScript +} + +const regex_placement = { + MD_DISPLAY: 0, + USER_INPUT: 1, + AI_OUTPUT: 2, + SYSTEM: 3, + SENDAS: 4 +} + +// 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]); +} + +function getRegexedString(rawString, placement, { characterOverride } = {}) { + if (extension_settings.disabledExtensions.includes("regex") || !rawString || placement === undefined) { + return; + } + + let finalString; + extension_settings.regex.forEach((script) => { + if (script.placement.includes(placement)) { + finalString = runRegexScript(script, rawString, { characterOverride }); + } + }); + + return finalString; +} + +// Runs the provided regex script on the given string +function runRegexScript(regexScript, rawString, { characterOverride } = {}) { + if (!regexScript || !!(regexScript.disabled) || !regexScript?.findRegex || !rawString) { + return; + } + + let match; + let newString; + const findRegex = regexFromString(regexScript.substituteRegex ? substituteParams(regexScript.findRegex) : regexScript.findRegex); + while ((match = findRegex.exec(rawString)) !== null) { + const fencedMatch = match[0]; + const capturedMatch = match[1]; + + let trimCapturedMatch; + let trimFencedMatch; + if (capturedMatch) { + const tempTrimCapture = filterString(capturedMatch, regexScript.trimStrings, { characterOverride }); + trimFencedMatch = fencedMatch.replaceAll(capturedMatch, tempTrimCapture); + trimCapturedMatch = tempTrimCapture; + } else { + trimFencedMatch = filterString(fencedMatch, regexScript.trimStrings, { characterOverride }); + } + + // 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, trimCapturedMatch ?? trimFencedMatch, { characterOverride }); + 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; +} + +// Filters anything to trim from the regex match +function filterString(rawString, trimStrings, { characterOverride } = {}) { + let finalString = rawString; + trimStrings.forEach((trimString) => { + const subTrimString = substituteParams(trimString, undefined, characterOverride); + finalString = finalString.replaceAll(subTrimString, ""); + }); + + return finalString; +} + +// Substitutes regex-specific and normal parameters +function substituteRegexParams(rawString, regexMatch, { characterOverride } = {}) { + let finalString = rawString; + finalString = finalString.replace("{{match}}", regexMatch); + finalString = substituteParams(finalString, undefined, characterOverride); + + return finalString; +} diff --git a/public/scripts/extensions/regex/index.js b/public/scripts/extensions/regex/index.js new file mode 100644 index 000000000..e577437ba --- /dev/null +++ b/public/scripts/extensions/regex/index.js @@ -0,0 +1,184 @@ +import { callPopup, eventSource, event_types, reloadCurrentChat, saveSettingsDebounced } from "../../../script.js"; +import { extension_settings } from "../../extensions.js"; +import { uuidv4, waitUntilCondition } from "../../utils.js"; +import { regex_placement } from "./engine.js"; + +async function saveRegexScript(regexScript, existingScriptIndex) { + // If not editing + if (existingScriptIndex === -1) { + // Is the script name undefined? + if (!regexScript.scriptName) { + 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 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 script: One placement checkbox must be selected!`); + return; + } + + if (existingScriptIndex !== -1) { + extension_settings.regex[existingScriptIndex] = regexScript; + } else { + extension_settings.regex.push(regexScript); + } + + saveSettingsDebounced(); + await loadRegexScripts(); + + // Markdown is global, so reload the chat. + if (regexScript.placement.includes(regex_placement.MD_DISPLAY)) { + await reloadCurrentChat(); + } +} + +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]; + if (existingScript.scriptName) { + editorHtml.find(`.regex_script_name`).val(existingScript.scriptName); + } else { + toastr.error("This script doesn't have a name! Please delete it.") + return; + } + + 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 ?? false); + editorHtml + .find(`input[name="run_on_edit"]`) + .prop("checked", existingScript.runOnEdit ?? false); + editorHtml + .find(`input[name="substitute_regex"]`) + .prop("checked", existingScript.substituteRegex ?? false); + + 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(), + 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) || [], + disabled: + editorHtml + .find(`input[name="disabled"]`) + .prop("checked"), + runOnEdit: + editorHtml + .find(`input[name="run_on_edit"]`) + .prop("checked"), + substituteRegex: + editorHtml + .find(`input[name="substitute_regex"]`) + .prop("checked") + }; + + saveRegexScript(newRegexScript, existingScriptIndex); + } +} + +// 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 () => { + console.log("REGEX CALLED") + // Manually disable the extension since static imports auto-import the JS file + if (extension_settings.disabledExtensions.includes("regex")) { + return; + } + + const settingsHtml = await $.get("scripts/extensions/regex/dropdown.html"); + $("#extensions_settings2").append(settingsHtml); + $("#open_regex_editor").on("click", function() { + onRegexEditorOpenClick(false); + }); + + await loadRegexScripts(); +}); diff --git a/public/scripts/extensions/regex/manifest.json b/public/scripts/extensions/regex/manifest.json new file mode 100644 index 000000000..d2e4215be --- /dev/null +++ b/public/scripts/extensions/regex/manifest.json @@ -0,0 +1,11 @@ +{ + "display_name": "Regex", + "loading_order": 1, + "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..382e20c48 --- /dev/null +++ b/public/scripts/extensions/regex/scriptTemplate.html @@ -0,0 +1,11 @@ +
+
+
+ + +
+
diff --git a/public/scripts/extensions/regex/style.css b/public/scripts/extensions/regex/style.css new file mode 100644 index 000000000..4be26dcdd --- /dev/null +++ b/public/scripts/extensions/regex/style.css @@ -0,0 +1,20 @@ +.regex_settings .menu_button { + width: fit-content; + display: flex; + gap: 10px; + flex-direction: row; +} + +.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; +} diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index fd3d2123a..33c83f06f 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -22,6 +22,7 @@ import { } from "../script.js"; import { humanizedDateTime } from "./RossAscends-mods.js"; import { resetSelectedGroup } from "./group-chats.js"; +import { getRegexedString, regex_placement } from "./extensions/regex/engine.js"; import { chat_styles, power_user } from "./power-user.js"; export { executeSlashCommands, @@ -218,14 +219,18 @@ 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(); + const regexResult = getRegexedString(mesText, regex_placement.SENDAS, { characterOverride: name }); + 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 +273,11 @@ async function sendNarratorMessage(_, text) { return; } + const regexResult = getRegexedString(text, regex_placement.SYSTEM); + 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 d7e3739a4..bbfbd7083 100644 --- a/public/style.css +++ b/public/style.css @@ -3999,6 +3999,16 @@ toolcool-color-picker { align-items: center; } +.alignitemsstart { + align-items: start; +} + +.overflow-hidden { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + .padding5 { padding: 5px; } @@ -4062,6 +4072,10 @@ toolcool-color-picker { justify-content: space-around; } +.justifyContentFlexStart { + justify-content: flex-start; +} + .justifyContentFlexEnd { justify-content: flex-end; } @@ -4089,6 +4103,10 @@ toolcool-color-picker { width: 100%; } +.wide50p { + width: 50%; +} + .wide50px { width: 50px; }