diff --git a/public/script.js b/public/script.js
index 4cca3d855..d3e40c55e 100644
--- a/public/script.js
+++ b/public/script.js
@@ -194,6 +194,7 @@ import { hideLoader, showLoader } from "./scripts/loader.js";
import { CharacterContextMenu, BulkEditOverlay } from "./scripts/BulkEditOverlay.js";
import { loadMancerModels } from "./scripts/mancer-settings.js";
import { hasPendingFileAttachment, populateFileAttachment } from "./scripts/chats.js";
+import { replaceVariableMacros } from "./scripts/variables.js";
//exporting functions and vars for mods
export {
@@ -2008,6 +2009,7 @@ function substituteParams(content, _name1, _name2, _original, _group, _replaceCh
content = content.replace(/{{original}}/i, _original);
}
+ content = replaceVariableMacros(content);
content = content.replace(/{{input}}/gi, String($('#send_textarea').val()));
if (_replaceCharacterCard) {
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 54d80c9cc..ba8a315b1 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -33,6 +33,7 @@ import { autoSelectPersona } from "./personas.js";
import { getContext } from "./extensions.js";
import { hideChatMessage, unhideChatMessage } from "./chats.js";
import { stringToRange } from "./utils.js";
+import { registerVariableCommands } from "./variables.js";
export {
executeSlashCommands,
registerSlashCommand,
@@ -160,6 +161,7 @@ parser.addCommand('delswipe', deleteSwipeCallback, ['swipedel'], '(text) – echoes the text to toast message. Useful for pipes debugging.', true, true);
parser.addCommand('gen', generateCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe.', true, true);
parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '(text) – adds a swipe to the last chat message.', true, true);
+registerVariableCommands();
const NARRATOR_NAME_KEY = 'narrator_name';
const NARRATOR_NAME_DEFAULT = 'System';
@@ -179,12 +181,12 @@ async function generateCallback(_, arg) {
}
async function echoCallback(_, arg) {
- if (!arg) {
+ if (!String(arg)) {
console.warn('WARN: No argument provided for /echo command');
return;
}
- toastr.info(arg);
+ toastr.info(String(arg));
return arg;
}
@@ -961,6 +963,11 @@ function setBackgroundCallback(_, bg) {
}
}
+/**
+ * Executes slash commands in the provided text
+ * @param {string} text Slash command text
+ * @returns {Promise<{interrupt: boolean, newText: string, pipe: string} | boolean>}
+ */
async function executeSlashCommands(text) {
if (!text) {
return false;
@@ -1006,7 +1013,7 @@ async function executeSlashCommands(text) {
const newText = lines.filter(x => linesToRemove.indexOf(x) === -1).join('\n');
- return { interrupt, newText };
+ return { interrupt, newText, pipe: pipeResult };
}
function setSlashCommandAutocomplete(textarea) {
diff --git a/public/scripts/templates/macros.html b/public/scripts/templates/macros.html
index 9c55db112..8e33b15cd 100644
--- a/public/scripts/templates/macros.html
+++ b/public/scripts/templates/macros.html
@@ -1,4 +1,6 @@
-System-wide Replacement Macros (in order of evaluation):
+
+ System-wide Replacement Macros (in order of evaluation):
+
- {{original}} – global prompts defined in API settings. Only valid in Advanced Definitions prompt overrides.
- {{input}} – the user input
@@ -27,3 +29,16 @@ System-wide Replacement Macros (in order of evaluation):
- {{roll:(formula)}} – rolls a dice. (ex: {{roll:1d6}} will roll a 6- sided dice and return a number between 1 and 6)
- {{banned "text here"}} – dynamically add text in the quotes to banned words sequences, if Text Generation WebUI backend used. Do nothing for others backends. Can be used anywhere (Character description, WI, AN, etc.) Quotes around the text are important.
+
+ Chat variables Macros:
+
+Local variables = unique to the current chat
+Global variables = works in any chat for any character
+
+ - {{getvar::name}} – replaced with the value of the local variable "name"
+ - {{setvar::name::value}} – replaced with empty string, sets the local variable "name" to "value"
+ - {{addvar::name::increment}} – replaced with the result of addition numeric value of "increment" to the local variable "name"
+ - {{getglobalvar::name}} – replaced with the value of the global variable "name"
+ - {{setglobalvar::name::value}} – replaced with empty string, sets the global variable "name" to "value"
+ - {{addglobalvar::name::value}} – replaced with the result of addition numeric value of "increment" to the global variable "name"
+
diff --git a/public/scripts/variables.js b/public/scripts/variables.js
index d4b0227fc..eb54579f4 100644
--- a/public/scripts/variables.js
+++ b/public/scripts/variables.js
@@ -1,6 +1,6 @@
import { chat_metadata, getCurrentChatId, sendSystemMessage, system_message_types } from "../script.js";
import { extension_settings } from "./extensions.js";
-import { registerSlashCommand } from "./slash-commands.js";
+import { executeSlashCommands, registerSlashCommand } from "./slash-commands.js";
function getLocalVariable(name) {
const localVariable = chat_metadata?.variables[name];
@@ -14,6 +14,7 @@ function setLocalVariable(name, value) {
}
chat_metadata.variables[name] = value;
+ return value;
}
function getGlobalVariable(name) {
@@ -26,6 +27,42 @@ function setGlobalVariable(name, value) {
extension_settings.variables.global[name] = value;
}
+function addLocalVariable(name, value) {
+ const currentValue = getLocalVariable(name) || 0;
+ const increment = Number(value);
+
+ if (isNaN(increment)) {
+ return '';
+ }
+
+ const newValue = Number(currentValue) + increment;
+
+ if (isNaN(newValue)) {
+ return '';
+ }
+
+ setLocalVariable(name, newValue);
+ return newValue;
+}
+
+function addGlobalVariable(name, value) {
+ const currentValue = getGlobalVariable(name) || 0;
+ const increment = Number(value);
+
+ if (isNaN(increment)) {
+ return '';
+ }
+
+ const newValue = Number(currentValue) + increment;
+
+ if (isNaN(newValue)) {
+ return '';
+ }
+
+ setGlobalVariable(name, newValue);
+ return newValue;
+}
+
export function replaceVariableMacros(str) {
// Replace {{getvar::name}} with the value of the variable name
str = str.replace(/{{getvar::([^}]+)}}/gi, (_, name) => {
@@ -43,21 +80,7 @@ export function replaceVariableMacros(str) {
// Replace {{addvar::name::value}} with empty string and add value to the variable value
str = str.replace(/{{addvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => {
name = name.toLowerCase().trim();
- const currentValue = getLocalVariable(name) || 0;
- const increment = Number(value);
-
- if (isNaN(increment)) {
- return '';
- }
-
- const newValue = Number(currentValue) + increment;
-
- if (isNaN(newValue)) {
- return '';
- }
-
- setLocalVariable(name, newValue);
- return '';
+ return addLocalVariable(name, value);;
});
// Replace {{getglobalvar::name}} with the value of the global variable name
@@ -76,21 +99,7 @@ export function replaceVariableMacros(str) {
// Replace {{addglobalvar::name::value}} with empty string and add value to the global variable value
str = str.replace(/{{addglobalvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => {
name = name.toLowerCase().trim();
- const currentValue = getGlobalVariable(name) || 0;
- const increment = Number(value);
-
- if (isNaN(increment)) {
- return '';
- }
-
- const newValue = Number(currentValue) + increment;
-
- if (isNaN(newValue)) {
- return '';
- }
-
- setGlobalVariable(name, newValue);
- return '';
+ return addGlobalVariable(name, value);
});
return str;
@@ -110,11 +119,80 @@ function listVariablesCallback() {
const converter = new showdown.Converter();
const message = `### Local variables (${chatName}):\n${localVariablesString}\n\n### Global variables:\n${globalVariablesString}`;
- const htmlMessage = converter.makeHtml(message);
+ const htmlMessage = DOMPurify.sanitize(converter.makeHtml(message));
sendSystemMessage(system_message_types.GENERIC, htmlMessage);
}
+async function ifCallback(args, command) {
+ const a = getLocalVariable(args.a) || getGlobalVariable(args.a) || Number(args.a);
+ const b = getLocalVariable(args.b) || getGlobalVariable(args.b) || Number(args.b);
+ const rule = args.rule;
+
+ if (!a || !b || !rule) {
+ toastr.warning('Both operands and the rule must be specified for the /if command.', 'Invalid /if command');
+ return '';
+ }
+
+ const aNumber = Number(a);
+ const bNumber = Number(b);
+
+ if (isNaN(aNumber) || isNaN(bNumber)) {
+ toastr.warning('Both operands must be numbers for the /if command.', 'Invalid /if command');
+ return '';
+ }
+
+ let result = false;
+
+ switch (rule) {
+ case 'gt':
+ result = aNumber > bNumber;
+ break;
+ case 'gte':
+ result = aNumber >= bNumber;
+ break;
+ case 'lt':
+ result = aNumber < bNumber;
+ break;
+ case 'lte':
+ result = aNumber <= bNumber;
+ break;
+ case 'eq':
+ result = aNumber === bNumber;
+ break;
+ default:
+ toastr.warning('Unknown rule for the /if command.', 'Invalid /if command');
+ return '';
+ }
+
+ if (result && command) {
+ if (command.startsWith('"')) {
+ command = command.slice(1);
+ }
+
+ if (command.endsWith('"')) {
+ command = command.slice(0, -1);
+ }
+
+ const result = await executeSlashCommands(command);
+
+ if (!result || typeof result !== 'object') {
+ return '';
+ }
+
+ return result?.pipe || '';
+ }
+
+ return '';
+}
+
export function registerVariableCommands() {
registerSlashCommand('listvar', listVariablesCallback, [''], ' – list registered chat variables', true, true);
+ registerSlashCommand('setvar', (args, value) => setLocalVariable(args.key || args.name, value), [], 'key=varname (value) – set a local variable value and pass it down the pipe, e.g. /setvar key=color green', true, true);
+ registerSlashCommand('getvar', (_, value) => getLocalVariable(value), [], '(key) – get a local variable value and pass it down the pipe, e.g. /getvar height', true, true);
+ registerSlashCommand('addvar', (args, value) => addLocalVariable(args.key || args.name, value), [], 'key=varname (increment) – add a value to a local variable and pass the result down the pipe, e.g. /addvar score 10', true, true);
+ registerSlashCommand('setglobalvar', (args, value) => setGlobalVariable(args.key || args.name, value), [], 'key=varname (value) – set a global variable value and pass it down the pipe, e.g. /setglobalvar key=color green', true, true);
+ registerSlashCommand('getglobalvar', (_, value) => getGlobalVariable(value), [], '(key) – get a global variable value and pass it down the pipe, e.g. /getglobalvar height', true, true);
+ registerSlashCommand('addglobalvar', (args, value) => addGlobalVariable(args.key || args.name, value), [], 'key=varname (increment) – add a value to a global variable and pass the result down the pipe, e.g. /addglobalvar score 10', true, true);
+ registerSlashCommand('if', ifCallback, [], 'a=varname1 b=varname2 rule=comparison "(command)" – compare the value of variable "a" with the value of variable "b", and if the condition yields true, then execute any valid slash command enclosed in quotes and pass the result of the command execution down the pipe. Numeric values for "a" and "b" supported. Available rules: gt => a > b, gte => a >= b, lt => a < b, lte => a <= b, eq => a == b e.g. /if a=score a=10 rule=gte "/speak You win" triggers a /speak command if the value of "score" is greater or equals 10.', true, true);
}