2024-04-01 01:47:56 +02:00
|
|
|
import { chat, main_api, getMaxContextSize, getCurrentChatId } from '../script.js';
|
|
|
|
import { timestampToMoment, isDigitsOnly, getStringHash } from './utils.js';
|
2024-01-12 10:47:00 +01:00
|
|
|
import { textgenerationwebui_banned_in_macros } from './textgen-settings.js';
|
|
|
|
import { replaceInstructMacros } from './instruct-mode.js';
|
|
|
|
import { replaceVariableMacros } from './variables.js';
|
2024-01-09 19:23:51 +01:00
|
|
|
|
2024-03-30 11:26:21 +01:00
|
|
|
// Register any macro that you want to leave in the compiled story string
|
|
|
|
Handlebars.registerHelper('trim', () => '{{trim}}');
|
|
|
|
|
2024-01-09 19:23:51 +01:00
|
|
|
/**
|
|
|
|
* Returns the ID of the last message in the chat.
|
|
|
|
* @returns {string} The ID of the last message in the chat.
|
|
|
|
*/
|
|
|
|
function getLastMessageId() {
|
|
|
|
const index = chat?.length - 1;
|
|
|
|
|
|
|
|
if (!isNaN(index) && index >= 0) {
|
|
|
|
return String(index);
|
|
|
|
}
|
|
|
|
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the ID of the first message included in the context.
|
|
|
|
* @returns {string} The ID of the first message in the context.
|
|
|
|
*/
|
|
|
|
function getFirstIncludedMessageId() {
|
|
|
|
const index = document.querySelector('.lastInContext')?.getAttribute('mesid');
|
|
|
|
|
|
|
|
if (!isNaN(index) && index >= 0) {
|
|
|
|
return String(index);
|
|
|
|
}
|
|
|
|
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the last message in the chat.
|
|
|
|
* @returns {string} The last message in the chat.
|
|
|
|
*/
|
|
|
|
function getLastMessage() {
|
|
|
|
const index = chat?.length - 1;
|
|
|
|
|
|
|
|
if (!isNaN(index) && index >= 0) {
|
|
|
|
return chat[index].mes;
|
|
|
|
}
|
|
|
|
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
2024-03-17 01:45:22 +01:00
|
|
|
/**
|
|
|
|
* Returns the last message from the user.
|
|
|
|
* @returns {string} The last message from the user.
|
|
|
|
*/
|
|
|
|
function getLastUserMessage() {
|
|
|
|
if (!Array.isArray(chat) || chat.length === 0) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = chat.length - 1; i >= 0; i--) {
|
|
|
|
if (chat[i].is_user && !chat[i].is_system) {
|
|
|
|
return chat[i].mes;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the last message from the bot.
|
|
|
|
* @returns {string} The last message from the bot.
|
|
|
|
*/
|
|
|
|
function getLastCharMessage() {
|
|
|
|
if (!Array.isArray(chat) || chat.length === 0) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = chat.length - 1; i >= 0; i--) {
|
|
|
|
if (!chat[i].is_user && !chat[i].is_system) {
|
|
|
|
return chat[i].mes;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
2024-01-09 19:23:51 +01:00
|
|
|
/**
|
|
|
|
* Returns the ID of the last swipe.
|
|
|
|
* @returns {string} The 1-based ID of the last swipe
|
|
|
|
*/
|
|
|
|
function getLastSwipeId() {
|
|
|
|
const index = chat?.length - 1;
|
|
|
|
|
|
|
|
if (!isNaN(index) && index >= 0) {
|
|
|
|
const swipes = chat[index].swipes;
|
|
|
|
|
|
|
|
if (!Array.isArray(swipes) || swipes.length === 0) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
return String(swipes.length);
|
|
|
|
}
|
|
|
|
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the ID of the current swipe.
|
|
|
|
* @returns {string} The 1-based ID of the current swipe.
|
|
|
|
*/
|
|
|
|
function getCurrentSwipeId() {
|
|
|
|
const index = chat?.length - 1;
|
|
|
|
|
|
|
|
if (!isNaN(index) && index >= 0) {
|
|
|
|
const swipeId = chat[index].swipe_id;
|
|
|
|
|
|
|
|
if (swipeId === undefined || isNaN(swipeId)) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
return String(swipeId + 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Replaces banned words in macros with an empty string.
|
|
|
|
* Adds them to textgenerationwebui ban list.
|
|
|
|
* @param {string} inText Text to replace banned words in
|
|
|
|
* @returns {string} Text without the "banned" macro
|
|
|
|
*/
|
|
|
|
function bannedWordsReplace(inText) {
|
|
|
|
if (!inText) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
const banPattern = /{{banned "(.*)"}}/gi;
|
|
|
|
|
|
|
|
if (main_api == 'textgenerationwebui') {
|
|
|
|
const bans = inText.matchAll(banPattern);
|
|
|
|
if (bans) {
|
|
|
|
for (const banCase of bans) {
|
|
|
|
console.log('Found banned words in macros: ' + banCase[1]);
|
|
|
|
textgenerationwebui_banned_in_macros.push(banCase[1]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
inText = inText.replaceAll(banPattern, '');
|
|
|
|
return inText;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getTimeSinceLastMessage() {
|
|
|
|
const now = moment();
|
|
|
|
|
|
|
|
if (Array.isArray(chat) && chat.length > 0) {
|
|
|
|
let lastMessage;
|
|
|
|
let takeNext = false;
|
|
|
|
|
|
|
|
for (let i = chat.length - 1; i >= 0; i--) {
|
|
|
|
const message = chat[i];
|
|
|
|
|
|
|
|
if (message.is_system) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (message.is_user && takeNext) {
|
|
|
|
lastMessage = message;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
takeNext = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (lastMessage?.send_date) {
|
|
|
|
const lastMessageDate = timestampToMoment(lastMessage.send_date);
|
|
|
|
const duration = moment.duration(now.diff(lastMessageDate));
|
|
|
|
return duration.humanize();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 'just now';
|
|
|
|
}
|
|
|
|
|
|
|
|
function randomReplace(input, emptyListPlaceholder = '') {
|
2024-04-02 01:02:02 +02:00
|
|
|
const randomPattern = /{{random\s?::?([^}]+)}}/gi;
|
2024-01-09 19:23:51 +01:00
|
|
|
|
2024-04-02 01:02:02 +02:00
|
|
|
input = input.replace(randomPattern, (match, listString) => {
|
|
|
|
// Split on either double colons or comma. If comma is the separator, we are also trimming all items.
|
2024-04-02 00:16:25 +02:00
|
|
|
const list = listString.includes('::')
|
2024-04-02 01:02:02 +02:00
|
|
|
? listString.split('::')
|
|
|
|
: listString.split(',').map(item => item.trim());
|
2024-04-02 00:16:25 +02:00
|
|
|
|
2024-03-22 23:57:33 +01:00
|
|
|
if (list.length === 0) {
|
|
|
|
return emptyListPlaceholder;
|
|
|
|
}
|
|
|
|
const rng = new Math.seedrandom('added entropy.', { entropy: true });
|
|
|
|
const randomIndex = Math.floor(rng() * list.length);
|
2024-04-02 01:02:02 +02:00
|
|
|
return list[randomIndex];
|
2024-03-22 23:57:33 +01:00
|
|
|
});
|
|
|
|
return input;
|
2024-01-09 19:23:51 +01:00
|
|
|
}
|
|
|
|
|
2024-04-01 01:47:56 +02:00
|
|
|
function pickReplace(input, rawContent, emptyListPlaceholder = '') {
|
2024-04-02 01:02:02 +02:00
|
|
|
const pickPattern = /{{pick\s?::?([^}]+)}}/gi;
|
2024-04-01 01:47:56 +02:00
|
|
|
const chatIdHash = getStringHash(getCurrentChatId());
|
|
|
|
const rawContentHash = getStringHash(rawContent);
|
|
|
|
|
|
|
|
return input.replace(pickPattern, (match, listString, offset) => {
|
2024-04-02 01:02:02 +02:00
|
|
|
// Split on either double colons or comma. If comma is the separator, we are also trimming all items.
|
2024-04-01 01:47:56 +02:00
|
|
|
const list = listString.includes('::')
|
2024-04-02 01:02:02 +02:00
|
|
|
? listString.split('::')
|
|
|
|
: listString.split(',').map(item => item.trim());
|
2024-04-01 01:47:56 +02:00
|
|
|
|
|
|
|
if (list.length === 0) {
|
|
|
|
return emptyListPlaceholder;
|
|
|
|
}
|
|
|
|
|
2024-04-02 01:02:02 +02:00
|
|
|
// We build a hash seed based on: unique chat file, raw content, and the placement inside this content
|
|
|
|
// This allows us to get unique but repeatable picks in nearly all cases
|
2024-04-01 01:47:56 +02:00
|
|
|
const combinedSeedString = `${chatIdHash}-${rawContentHash}-${offset}`;
|
|
|
|
const finalSeed = getStringHash(combinedSeedString);
|
|
|
|
const rng = new Math.seedrandom(finalSeed);
|
|
|
|
const randomIndex = Math.floor(rng() * list.length);
|
2024-04-02 01:02:02 +02:00
|
|
|
return list[randomIndex];
|
2024-04-01 01:47:56 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-01-09 19:23:51 +01:00
|
|
|
function diceRollReplace(input, invalidRollPlaceholder = '') {
|
|
|
|
const rollPattern = /{{roll[ : ]([^}]+)}}/gi;
|
|
|
|
|
|
|
|
return input.replace(rollPattern, (match, matchValue) => {
|
|
|
|
let formula = matchValue.trim();
|
|
|
|
|
|
|
|
if (isDigitsOnly(formula)) {
|
|
|
|
formula = `1d${formula}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
const isValid = droll.validate(formula);
|
|
|
|
|
|
|
|
if (!isValid) {
|
|
|
|
console.debug(`Invalid roll formula: ${formula}`);
|
|
|
|
return invalidRollPlaceholder;
|
|
|
|
}
|
|
|
|
|
|
|
|
const result = droll.roll(formula);
|
|
|
|
return new String(result.total);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Substitutes {{macro}} parameters in a string.
|
|
|
|
* @param {string} content - The string to substitute parameters in.
|
2024-01-12 11:19:37 +01:00
|
|
|
* @param {Object<string, *>} env - Map of macro names to the values they'll be substituted with. If the param
|
|
|
|
* values are functions, those functions will be called and their return values are used.
|
2024-01-09 19:23:51 +01:00
|
|
|
* @returns {string} The string with substituted parameters.
|
|
|
|
*/
|
2024-01-12 11:19:37 +01:00
|
|
|
export function evaluateMacros(content, env) {
|
2024-01-09 19:23:51 +01:00
|
|
|
if (!content) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
2024-04-01 01:47:56 +02:00
|
|
|
const rawContent = content;
|
|
|
|
|
2024-03-16 00:38:23 +01:00
|
|
|
// Legacy non-macro substitutions
|
|
|
|
content = content.replace(/<USER>/gi, typeof env.user === 'function' ? env.user() : env.user);
|
|
|
|
content = content.replace(/<BOT>/gi, typeof env.char === 'function' ? env.char() : env.char);
|
|
|
|
content = content.replace(/<CHARIFNOTGROUP>/gi, typeof env.group === 'function' ? env.group() : env.group);
|
|
|
|
content = content.replace(/<GROUP>/gi, typeof env.group === 'function' ? env.group() : env.group);
|
|
|
|
|
|
|
|
// Short circuit if there are no macros
|
|
|
|
if (!content.includes('{{')) {
|
|
|
|
return content;
|
|
|
|
}
|
|
|
|
|
2024-01-09 19:23:51 +01:00
|
|
|
content = diceRollReplace(content);
|
|
|
|
content = replaceInstructMacros(content);
|
|
|
|
content = replaceVariableMacros(content);
|
|
|
|
content = content.replace(/{{newline}}/gi, '\n');
|
2024-03-30 11:26:21 +01:00
|
|
|
content = content.replace(/\n*{{trim}}\n*/gi, '');
|
2024-03-16 00:38:23 +01:00
|
|
|
content = content.replace(/{{input}}/gi, () => String($('#send_textarea').val()));
|
2024-01-09 19:23:51 +01:00
|
|
|
|
2024-01-12 11:19:37 +01:00
|
|
|
// Substitute passed-in variables
|
|
|
|
for (const varName in env) {
|
|
|
|
if (!Object.hasOwn(env, varName)) continue;
|
|
|
|
|
|
|
|
const param = env[varName];
|
2024-01-28 23:58:29 +01:00
|
|
|
content = content.replace(new RegExp(`{{${varName}}}`, 'gi'), param);
|
2024-01-09 19:23:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
content = content.replace(/{{maxPrompt}}/gi, () => String(getMaxContextSize()));
|
2024-03-16 00:38:23 +01:00
|
|
|
content = content.replace(/{{lastMessage}}/gi, () => getLastMessage());
|
|
|
|
content = content.replace(/{{lastMessageId}}/gi, () => getLastMessageId());
|
2024-03-17 01:45:22 +01:00
|
|
|
content = content.replace(/{{lastUserMessage}}/gi, () => getLastUserMessage());
|
|
|
|
content = content.replace(/{{lastCharMessage}}/gi, () => getLastCharMessage());
|
2024-03-16 00:38:23 +01:00
|
|
|
content = content.replace(/{{firstIncludedMessageId}}/gi, () => getFirstIncludedMessageId());
|
|
|
|
content = content.replace(/{{lastSwipeId}}/gi, () => getLastSwipeId());
|
|
|
|
content = content.replace(/{{currentSwipeId}}/gi, () => getCurrentSwipeId());
|
2024-01-09 19:23:51 +01:00
|
|
|
|
|
|
|
content = content.replace(/\{\{\/\/([\s\S]*?)\}\}/gm, '');
|
|
|
|
|
2024-03-16 00:38:23 +01:00
|
|
|
content = content.replace(/{{time}}/gi, () => moment().format('LT'));
|
|
|
|
content = content.replace(/{{date}}/gi, () => moment().format('LL'));
|
|
|
|
content = content.replace(/{{weekday}}/gi, () => moment().format('dddd'));
|
|
|
|
content = content.replace(/{{isotime}}/gi, () => moment().format('HH:mm'));
|
|
|
|
content = content.replace(/{{isodate}}/gi, () => moment().format('YYYY-MM-DD'));
|
2024-01-09 19:23:51 +01:00
|
|
|
|
|
|
|
content = content.replace(/{{datetimeformat +([^}]*)}}/gi, (_, format) => {
|
|
|
|
const formattedTime = moment().format(format);
|
|
|
|
return formattedTime;
|
|
|
|
});
|
|
|
|
content = content.replace(/{{idle_duration}}/gi, () => getTimeSinceLastMessage());
|
|
|
|
content = content.replace(/{{time_UTC([-+]\d+)}}/gi, (_, offset) => {
|
|
|
|
const utcOffset = parseInt(offset, 10);
|
|
|
|
const utcTime = moment().utc().utcOffset(utcOffset).format('LT');
|
|
|
|
return utcTime;
|
|
|
|
});
|
|
|
|
content = bannedWordsReplace(content);
|
|
|
|
content = randomReplace(content);
|
2024-04-01 01:47:56 +02:00
|
|
|
content = pickReplace(content, rawContent);
|
2024-01-09 19:23:51 +01:00
|
|
|
return content;
|
|
|
|
}
|