diff --git a/public/index.html b/public/index.html index 7db18538c..cff0b4d5b 100644 --- a/public/index.html +++ b/public/index.html @@ -2344,6 +2344,9 @@ +
@@ -2425,9 +2428,9 @@
-
+
-
+
@@ -2627,6 +2630,18 @@
+
+
+ Creator's Notes + + +
+
+ +
+ +
@@ -3311,6 +3326,7 @@
+
@@ -3568,4 +3584,4 @@ - \ No newline at end of file + diff --git a/public/script.js b/public/script.js index f22757020..f0ff58d16 100644 --- a/public/script.js +++ b/public/script.js @@ -101,7 +101,10 @@ import { nai_settings, } from "./scripts/nai-settings.js"; -import { showBookmarksButtons } from "./scripts/bookmarks.js"; +import { + createNewBookmark, + showBookmarksButtons +} from "./scripts/bookmarks.js"; import { horde_settings, @@ -162,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 { @@ -238,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 } }); @@ -479,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 }); @@ -1109,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); } @@ -1484,9 +1496,25 @@ function substituteParams(content, _name1, _name2, _original) { content = content.replace(//gi, _name2); content = content.replace(/{{time}}/gi, moment().format('LT')); content = content.replace(/{{date}}/gi, moment().format('LL')); + content = randomReplace(content); return content; } +function randomReplace(input, emptyListPlaceholder = '') { + const randomPattern = /{{random:([^}]+)}}/g; + + return input.replace(randomPattern, (match, listString) => { + const list = listString.split(',').map(item => item.trim()).filter(item => item.length > 0); + + if (list.length === 0) { + return emptyListPlaceholder; + } + + const randomIndex = Math.floor(Math.random() * list.length); + return list[randomIndex]; + }); +} + function getStoppingStrings(isImpersonate, addSpace) { const charString = `\n${name2}:`; const youString = `\nYou:`; @@ -1588,7 +1616,7 @@ export function extractMessageBias(message) { return null; } - const forbiddenMatches = ['user', 'char', 'time', 'date']; + const forbiddenMatches = ['user', 'char', 'time', 'date', 'random']; const found = []; const rxp = /\{\{([\s\S]+?)\}\}/gm; //const rxp = /{([^}]+)}/g; @@ -1597,7 +1625,12 @@ export function extractMessageBias(message) { while ((curMatch = rxp.exec(message))) { const match = curMatch[1].trim(); - if (forbiddenMatches.includes(match)) { + // Ignore random pattern matches + if (/^random:.+/i.test(match)) { + continue; + } + + if (forbiddenMatches.includes(match.toLowerCase())) { continue; } @@ -2866,6 +2899,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; @@ -3446,11 +3484,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); } @@ -3816,7 +3864,7 @@ async function renamePastChats(newAvatar, newValue) { } } -async function saveChat(chat_name, withMetadata) { +async function saveChat(chat_name, withMetadata, mesId) { const metadata = { ...chat_metadata, ...(withMetadata || {}) }; let file_name = chat_name ?? characters[this_chid].chat; characters[this_chid]['date_last_chat'] = Date.now(); @@ -3837,6 +3885,11 @@ async function saveChat(chat_name, withMetadata) { } */ }); + + const trimmed_chat = (mesId !== undefined && mesId >= 0 && mesId < chat.length) + ? chat.slice(0, parseInt(mesId) + 1) + : chat; + var save_chat = [ { user_name: name1, @@ -3844,7 +3897,7 @@ async function saveChat(chat_name, withMetadata) { create_date: chat_create_date, chat_metadata: metadata, }, - ...chat, + ...trimmed_chat, ]; return jQuery.ajax({ type: "POST", @@ -4768,10 +4821,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() { @@ -4860,13 +4916,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; @@ -5189,6 +5266,7 @@ export function select_selected_character(chid) { $("#description_textarea").val(characters[chid].description); $("#character_world").val(characters[chid].data?.extensions?.world || ''); $("#creator_notes_textarea").val(characters[chid].data?.creator_notes || characters[chid].creatorcomment); + $("#creator_notes_spoiler").text(characters[chid].data?.creator_notes || characters[chid].creatorcomment); $("#character_version_textarea").val(characters[chid].data?.character_version || ''); $("#system_prompt_textarea").val(characters[chid].data?.system_prompt || ''); $("#post_history_instructions_textarea").val(characters[chid].data?.post_history_instructions || ''); @@ -5257,6 +5335,7 @@ function select_rm_create() { $("#description_textarea").val(create_save.description); $('#character_world').val(create_save.world); $("#creator_notes_textarea").val(create_save.creator_notes); + $("#creator_notes_spoiler").text(create_save.creator_notes); $("#post_history_instructions_textarea").val(create_save.post_history_instructions); $("#system_prompt_textarea").val(create_save.system_prompt); $("#tags_textarea").val(create_save.tags); @@ -5331,7 +5410,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; } @@ -5339,30 +5418,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"); @@ -5960,9 +6039,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( @@ -5973,6 +6054,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] = {}; @@ -7884,6 +7972,13 @@ $(document).ready(function () { $("#load_select_chat_div").css("display", "block"); }); + $(document).on("click", ".mes_create_bookmark", async function () { + var selected_mes_id = $(this).closest(".mes").attr("mesid"); + if (selected_mes_id !== undefined) { + createNewBookmark(selected_mes_id); + } + }); + $(document).on("click", ".mes_stop", function () { if (streamingProcessor) { streamingProcessor.abortController.abort(); diff --git a/public/scripts/bookmarks.js b/public/scripts/bookmarks.js index b69dbc1ff..ba77eda84 100644 --- a/public/scripts/bookmarks.js +++ b/public/scripts/bookmarks.js @@ -32,6 +32,7 @@ import { } from "./utils.js"; export { + createNewBookmark, showBookmarksButtons, } @@ -123,13 +124,26 @@ function showBookmarksButtons() { } } -async function createNewBookmark() { +async function saveBookmarkMenu() { if (!chat.length) { toastr.warning('The chat is empty.', 'Bookmark creation failed'); return; } - const mesId = chat.length - 1; + return createNewBookmark(chat.length - 1); +} + +async function createNewBookmark(mesId) { + if (!chat.length) { + toastr.warning('The chat is empty.', 'Bookmark creation failed'); + return; + } + + if (mesId < 0 || mesId >= chat.length) { + toastr.warning('Invalid message ID.', 'Bookmark creation failed'); + return; + } + const lastMes = chat[mesId]; if (typeof lastMes.extra !== 'object') { @@ -155,9 +169,9 @@ async function createNewBookmark() { const newMetadata = { main_chat: mainChat }; if (selected_group) { - await saveGroupBookmarkChat(selected_group, name, newMetadata); + await saveGroupBookmarkChat(selected_group, name, newMetadata, mesId); } else { - await saveChat(name, newMetadata); + await saveChat(name, newMetadata, mesId); } lastMes.extra['bookmark_link'] = name; @@ -294,7 +308,7 @@ async function convertSoloToGroupChat() { } $(document).ready(function () { - $('#option_new_bookmark').on('click', createNewBookmark); + $('#option_new_bookmark').on('click', saveBookmarkMenu); $('#option_back_to_main').on('click', backToMainChat); $('#option_convert_to_group').on('click', convertSoloToGroupChat); }); 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/group-chats.js b/public/scripts/group-chats.js index f6a9e08fd..821b4b212 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -1440,7 +1440,7 @@ export async function importGroupChat(formData) { }); } -export async function saveGroupBookmarkChat(groupId, name, metadata) { +export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) { const group = groups.find(x => x.id === groupId); if (!group) { @@ -1450,12 +1450,16 @@ export async function saveGroupBookmarkChat(groupId, name, metadata) { group.past_metadata[name] = { ...chat_metadata, ...(metadata || {}) }; group.chats.push(name); + const trimmed_chat = (mesId !== undefined && mesId >= 0 && mesId < chat.length) + ? chat.slice(0, parseInt(mesId) + 1) + : chat; + await editGroup(groupId, true); await fetch("/savegroupchat", { method: "POST", headers: getRequestHeaders(), - body: JSON.stringify({ id: name, chat: [...chat] }), + body: JSON.stringify({ id: name, chat: [...trimmed_chat] }), }); } diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index c1a548a22..5fa816999 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -316,6 +316,32 @@ function switchWaifuMode() { scrollChatToBottom(); } +function switchSpoilerMode() { + if (power_user.spoiler_free_mode) { + $("#description_div").hide(); + $("#description_textarea").hide(); + $("#firstmessage_textarea").hide(); + $("#first_message_div").hide(); + $("#spoiler_free_desc").show(); + } + else { + $("#description_div").show(); + $("#description_textarea").show(); + $("#firstmessage_textarea").show(); + $("#first_message_div").show(); + $("#spoiler_free_desc").hide(); + } +} + +function peekSpoilerMode() { + $("#description_div").toggle(); + $("#description_textarea").toggle(); + $("#firstmessage_textarea").toggle(); + $("#first_message_div").toggle(); + +} + + function switchMovingUI() { const movingUI = localStorage.getItem(storage_keys.movingUI); power_user.movingUI = movingUI === null ? false : movingUI == "true"; @@ -632,6 +658,7 @@ function loadPowerUserSettings(settings, data) { $(`#pygmalion_formatting option[value=${power_user.pygmalion_formatting}]`).attr("selected", true); $(`#send_on_enter option[value=${power_user.send_on_enter}]`).attr("selected", true); $("#import_card_tags").prop("checked", power_user.import_card_tags); + $("#spoiler_free_mode").prop("checked", power_user.spoiler_free_mode); $("#collapse-newlines-checkbox").prop("checked", power_user.collapse_newlines); $("#pin-examples-checkbox").prop("checked", power_user.pin_examples); $("#disable-description-formatting-checkbox").prop("checked", power_user.disable_description_formatting); @@ -705,6 +732,7 @@ function loadPowerUserSettings(settings, data) { loadInstructMode(); loadMaxContextUnlocked(); switchWaifuMode(); + switchSpoilerMode(); loadMovingUIState(); //console.log(power_user) @@ -1138,7 +1166,7 @@ function setAvgBG() { .attr('src') .replace(/^url\(['"]?/, '') .replace(/['"]?\)$/, ''); - + const userAvatar = new Image() userAvatar.src = $("#user_avatar_block .avatar.selected img") .attr('src') @@ -1157,7 +1185,7 @@ function setAvgBG() { .replace('rgb', '') .replace('(', '[') .replace(')', ']'); //[50, 120, 200, 1]; // Example background color - const backgroundColorArray = JSON.parse(backgroundColorString) //[200, 200, 200, 1] + const backgroundColorArray = JSON.parse(backgroundColorString) //[200, 200, 200, 1] console.log(backgroundColorArray) $("#main-text-color-picker").attr('color', getReadableTextColor(backgroundColorArray)); console.log($("#main-text-color-picker").attr('color')); // Output: 'rgba(0, 47, 126, 1)' @@ -1169,7 +1197,7 @@ function setAvgBG() { //console.log(rgb); $("#bot-mes-blur-tint-color-picker").attr('color', 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')'); } - + userAvatar.onload = function () { var rgb = getAverageRGB(userAvatar); //console.log(`average color of the user avatar is:`); @@ -1272,16 +1300,16 @@ function setAvgBG() { //this version keeps BG and main text in same hue /* function getReadableTextColor(rgb) { const [r, g, b] = rgb; - + // Convert RGB to HSL const rgbToHsl = (r, g, b) => { const max = Math.max(r, g, b); const min = Math.min(r, g, b); const d = max - min; const l = (max + min) / 2; - + if (d === 0) return [0, 0, l]; - + const s = l > 0.5 ? d / (2 - max - min) : d / (max + min); const h = (() => { switch (max) { @@ -1293,16 +1321,16 @@ function setAvgBG() { return (r - g) / d + 4; } })() / 6; - + return [h, s, l]; }; const [h, s, l] = rgbToHsl(r / 255, g / 255, b / 255); - + // Calculate appropriate text color based on background color const targetLuminance = l > 0.5 ? 0.2 : 0.8; const targetSaturation = s > 0.5 ? s - 0.2 : s + 0.2; const [rNew, gNew, bNew] = hslToRgb(h, targetSaturation, targetLuminance); - + // Return the text color in RGBA format return `rgba(${rNew.toFixed(0)}, ${gNew.toFixed(0)}, ${bNew.toFixed(0)}, 1)`; }*/ @@ -1786,6 +1814,17 @@ $(document).ready(() => { saveSettingsDebounced(); }); + $('#spoiler_free_mode').on('input', function () { + power_user.spoiler_free_mode = !!$(this).prop('checked'); + switchSpoilerMode(); + saveSettingsDebounced(); + }); + + $('#spoiler_free_desc_button').on('click', function () { + peekSpoilerMode(); + $(this).toggleClass('fa-eye fa-eye-slash'); + }); + $(window).on('focus', function () { browser_has_focus = true; }); 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 48c93ebd4..0cd0e3cd8 100644 --- a/public/style.css +++ b/public/style.css @@ -2478,6 +2478,7 @@ input[type="range"]::-webkit-slider-thumb { .mes_buttons .mes_edit, .mes_buttons .mes_bookmark, +.mes_buttons .mes_create_bookmark, .extraMesButtonsHint, .tagListHint, .extraMesButtons div { @@ -2489,6 +2490,7 @@ input[type="range"]::-webkit-slider-thumb { .mes_buttons .mes_edit:hover, .mes_buttons .mes_bookmark:hover, +.mes_buttons .mes_create_bookmark:hover, .extraMesButtonsHint:hover, .tagListHint:hover, .extraMesButtons div:hover { @@ -4005,6 +4007,16 @@ toolcool-color-picker { align-items: center; } +.alignitemsstart { + align-items: start; +} + +.overflow-hidden { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + .padding5 { padding: 5px; } @@ -4068,6 +4080,10 @@ toolcool-color-picker { justify-content: space-around; } +.justifyContentFlexStart { + justify-content: flex-start; +} + .justifyContentFlexEnd { justify-content: flex-end; } @@ -4095,6 +4111,10 @@ toolcool-color-picker { width: 100%; } +.wide50p { + width: 50%; +} + .wide50px { width: 50px; }