| import { Handlebars, moment, seedrandom, droll } from '../lib.js'; |
| import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams, eventSource, event_types, extension_prompts } from '../script.js'; |
| import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex, uuidv4 } from './utils.js'; |
| import { textgenerationwebui_banned_in_macros } from './textgen-settings.js'; |
| import { getInstructMacros } from './instruct-mode.js'; |
| import { getVariableMacros } from './variables.js'; |
| import { isMobile } from './RossAscends-mods.js'; |
| import { inject_ids } from './constants.js'; |
| import { initRegisterMacros, macros as macroSystem } from './macros/macro-system.js'; |
| import { power_user } from './power-user.js'; |
|
|
| |
| |
| |
| |
| |
|
|
| |
| Handlebars.registerHelper('trim', () => '{{trim}}'); |
| |
| Handlebars.registerHelper('helperMissing', function () { |
| const options = arguments[arguments.length - 1]; |
| const macroName = options.name; |
| return substituteParams(`{{${macroName}}}`); |
| }); |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| export class MacrosParser { |
| |
| |
| |
| |
| static #macros = new Map(); |
|
|
| |
| |
| |
| |
| static #descriptions = new Map(); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| static #logDeprecated(method, replacement) { |
| console.warn(`[DEPRECATED] MacrosParser.${method} is deprecated and will be removed in a future version. Use ${replacement} instead.`); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| static #registerMacroInNewEngine(key, value, description) { |
| if (!power_user.experimental_macro_engine) { |
| return; |
| } |
|
|
| |
| if (macroSystem.registry.hasMacro(key)) { |
| console.warn(`Macro ${key} is already registered`); |
| } |
|
|
| const legacyValue = value; |
|
|
| macroSystem.registry.registerMacro(key, { |
| |
| |
| category: 'legacy', |
| description: typeof description === 'string' ? description : 'Automatically registered macro from MacrosParser', |
| handler: () => { |
| |
| let stored = legacyValue; |
|
|
| if (typeof stored === 'function') { |
| try { |
| const nonce = uuidv4(); |
| stored = stored(nonce); |
| } catch (e) { |
| console.warn(`Macro "${key}" function threw an error.`, e); |
| stored = ''; |
| } |
| } |
|
|
| |
| |
| return stored; |
| }, |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| static #unregisterMacroInNewEngine(key) { |
| if (!power_user.experimental_macro_engine) { |
| return; |
| } |
|
|
| macroSystem.registry.unregisterMacro(key); |
| } |
|
|
| |
| |
| |
| |
| static [Symbol.iterator] = function* () { |
| |
| if (power_user.experimental_macro_engine) { |
| |
| for (const def of macroSystem.registry.getAllMacros({ excludeHiddenAliases: true })) { |
| yield { key: def.name, description: def.description || '' }; |
| } |
| return; |
| } |
|
|
| for (const macro of MacrosParser.#macros.keys()) { |
| yield { key: macro, description: MacrosParser.#descriptions.get(macro) }; |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| static get(key) { |
| MacrosParser.#logDeprecated('get', 'macros.registry.getMacro (from scripts/macros/macro-system.js)'); |
| return MacrosParser.#macros.get(key); |
| } |
|
|
| |
| |
| |
| |
| |
| static has(key) { |
| MacrosParser.#logDeprecated('has', 'macros.registry.hasMacro (from scripts/macros/macro-system.js)'); |
| if (power_user.experimental_macro_engine) { |
| return macroSystem.registry.hasMacro(key); |
| } |
|
|
| return MacrosParser.#macros.has(key); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| static registerMacro(key, value, description = '') { |
| MacrosParser.#logDeprecated('registerMacro', 'macros.registry.registerMacro (from scripts/macros/macro-system.js) or substituteParams({ dynamicMacros })'); |
| if (typeof key !== 'string') { |
| throw new Error('Macro key must be a string'); |
| } |
|
|
| |
| key = key.trim(); |
|
|
| if (!key) { |
| throw new Error('Macro key must not be empty or whitespace only'); |
| } |
|
|
| if (key.startsWith('{{') || key.endsWith('}}')) { |
| throw new Error('Macro key must not include the surrounding braces'); |
| } |
|
|
| if (typeof value !== 'string' && typeof value !== 'function') { |
| console.warn(`Macro value for "${key}" will be converted to a string`); |
| value = this.sanitizeMacroValue(value); |
| } |
|
|
| MacrosParser.#registerMacroInNewEngine(key, value, description); |
| if (power_user.experimental_macro_engine) { |
| return; |
| } |
|
|
| if (this.#macros.has(key)) { |
| console.warn(`Macro ${key} is already registered`); |
| } |
|
|
| this.#macros.set(key, value); |
|
|
| if (typeof description === 'string' && description) { |
| this.#descriptions.set(key, description); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| static unregisterMacro(key) { |
| MacrosParser.#logDeprecated('unregisterMacro', 'macros.registry.unregisterMacro (from scripts/macros/macro-system.js)'); |
| if (typeof key !== 'string') { |
| throw new Error('Macro key must be a string'); |
| } |
|
|
| |
| key = key.trim(); |
|
|
| if (!key) { |
| throw new Error('Macro key must not be empty or whitespace only'); |
| } |
|
|
| if (power_user.experimental_macro_engine) { |
| MacrosParser.#unregisterMacroInNewEngine(key); |
| return; |
| } |
|
|
| const deleted = this.#macros.delete(key); |
|
|
| if (!deleted) { |
| console.warn(`Macro ${key} was not registered`); |
| } |
|
|
| this.#descriptions.delete(key); |
| } |
|
|
| |
| |
| |
| |
| |
| static populateEnv(env) { |
| if (!env || typeof env !== 'object') { |
| console.warn('Env object is not provided'); |
| return; |
| } |
|
|
| |
| if (this.#macros.size === 0) { |
| return; |
| } |
|
|
| for (const [key, value] of this.#macros) { |
| env[key] = value; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| static sanitizeMacroValue(value) { |
| if (typeof value === 'string') { |
| return value; |
| } |
|
|
| if (value === null || value === undefined) { |
| return ''; |
| } |
|
|
| if (value instanceof Promise) { |
| console.warn('Promises are not supported as macro values'); |
| return ''; |
| } |
|
|
| if (typeof value === 'function') { |
| console.warn('Functions are not supported as macro values'); |
| return ''; |
| } |
|
|
| if (value instanceof Date) { |
| return value.toISOString(); |
| } |
|
|
| if (typeof value === 'object') { |
| return JSON.stringify(value); |
| } |
|
|
| return String(value); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| function getChatIdHash() { |
| const cachedIdHash = chat_metadata['chat_id_hash']; |
|
|
| |
| if (!cachedIdHash) { |
| |
| const chatId = chat_metadata['main_chat'] ?? getCurrentChatId(); |
| const chatIdHash = getStringHash(chatId); |
| chat_metadata['chat_id_hash'] = chatIdHash; |
| return chatIdHash; |
| } |
|
|
| return cachedIdHash; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function getLastMessageId({ exclude_swipe_in_propress = true, filter = null } = {}) { |
| for (let i = chat?.length - 1; i >= 0; i--) { |
| let message = chat[i]; |
|
|
| |
| |
| if (exclude_swipe_in_propress && message.swipes && message.swipe_id >= message.swipes.length) { |
| continue; |
| } |
|
|
| |
| if (!filter || filter(message)) { |
| return i; |
| } |
| } |
|
|
| return null; |
| } |
|
|
| |
| |
| |
| |
| |
| function getFirstIncludedMessageId() { |
| return chat_metadata['lastInContextMessageId']; |
| } |
|
|
| |
| |
| |
| |
| |
| function getFirstDisplayedMessageId() { |
| const mesId = Number(document.querySelector('#chat .mes')?.getAttribute('mesid')); |
|
|
| if (!isNaN(mesId) && mesId >= 0) { |
| return mesId; |
| } |
|
|
| return null; |
| } |
|
|
| |
| |
| |
| |
| |
| function getLastMessage() { |
| const mid = getLastMessageId(); |
| return chat[mid]?.mes ?? ''; |
| } |
|
|
| |
| |
| |
| |
| |
| function getLastUserMessage() { |
| const mid = getLastMessageId({ filter: m => m.is_user && !m.is_system }); |
| return chat[mid]?.mes ?? ''; |
| } |
|
|
| |
| |
| |
| |
| |
| function getLastCharMessage() { |
| const mid = getLastMessageId({ filter: m => !m.is_user && !m.is_system }); |
| return chat[mid]?.mes ?? ''; |
| } |
|
|
| |
| |
| |
| |
| |
| function getLastSwipeId() { |
| |
| const mid = getLastMessageId({ exclude_swipe_in_propress: false }); |
| const swipes = chat[mid]?.swipes; |
| return swipes?.length; |
| } |
|
|
| |
| |
| |
| |
| |
| function getCurrentSwipeId() { |
| |
| const mid = getLastMessageId({ exclude_swipe_in_propress: false }); |
| const swipeId = chat[mid]?.swipe_id; |
| return swipeId !== null ? swipeId + 1 : null; |
| } |
|
|
| |
| |
| |
| |
| |
| function getBannedWordsMacro() { |
| const banPattern = /{{banned "(.*)"}}/gi; |
| const banReplace = (match, bannedWord) => { |
| if (main_api == 'textgenerationwebui') { |
| console.log('Found banned word in macros: ' + bannedWord); |
| textgenerationwebui_banned_in_macros.push(bannedWord); |
| } |
| return ''; |
| }; |
|
|
| return { regex: banPattern, replace: banReplace }; |
| } |
|
|
| 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 getRandomReplaceMacro() { |
| const randomPattern = /{{random\s?::?([^}]+)}}/gi; |
| const randomReplace = (match, listString) => { |
| |
| const list = listString.includes('::') |
| ? listString.split('::') |
| |
| : listString.replace(/\\,/g, '##�COMMA�##').split(',').map(item => item.trim().replace(/##�COMMA�##/g, ',')); |
|
|
| if (list.length === 0) { |
| return ''; |
| } |
| const rng = seedrandom('added entropy.', { entropy: true }); |
| const randomIndex = Math.floor(rng() * list.length); |
| return list[randomIndex]; |
| }; |
|
|
| return { regex: randomPattern, replace: randomReplace }; |
| } |
|
|
| |
| |
| |
| |
| |
| function getPickReplaceMacro(rawContent) { |
| |
| |
| const chatIdHash = getChatIdHash(); |
| const rawContentHash = getStringHash(rawContent); |
|
|
| const pickPattern = /{{pick\s?::?([^}]+)}}/gi; |
| const pickReplace = (match, listString, offset) => { |
| |
| const list = listString.includes('::') |
| ? listString.split('::') |
| |
| : listString.replace(/\\,/g, '##�COMMA�##').split(',').map(item => item.trim().replace(/##�COMMA�##/g, ',')); |
|
|
| if (list.length === 0) { |
| return ''; |
| } |
|
|
| |
| |
| const combinedSeedString = `${chatIdHash}-${rawContentHash}-${offset}`; |
| const finalSeed = getStringHash(combinedSeedString); |
| |
| const rng = seedrandom(finalSeed); |
| const randomIndex = Math.floor(rng() * list.length); |
| return list[randomIndex]; |
| }; |
|
|
| return { regex: pickPattern, replace: pickReplace }; |
| } |
|
|
| |
| |
| |
| function getDiceRollMacro() { |
| const rollPattern = /{{roll[ : ]([^}]+)}}/gi; |
| const rollReplace = (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 ''; |
| } |
|
|
| const result = droll.roll(formula); |
| if (result === false) return ''; |
| return String(result.total); |
| }; |
|
|
| return { regex: rollPattern, replace: rollReplace }; |
| } |
|
|
| |
| |
| |
| |
| |
| function getTimeDiffMacro() { |
| const timeDiffPattern = /{{timeDiff::(.*?)::(.*?)}}/gi; |
| const timeDiffReplace = (_match, matchPart1, matchPart2) => { |
| const time1 = moment(matchPart1); |
| const time2 = moment(matchPart2); |
|
|
| const timeDifference = moment.duration(time1.diff(time2)); |
| return timeDifference.humanize(true); |
| }; |
|
|
| return { regex: timeDiffPattern, replace: timeDiffReplace }; |
| } |
|
|
| |
| |
| |
| |
| |
| function getOutletPrompt(key) { |
| const value = extension_prompts[inject_ids.CUSTOM_WI_OUTLET(key)]?.value; |
| return value || ''; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function evaluateMacros(content, env, postProcessFn) { |
| if (!content) { |
| return ''; |
| } |
|
|
| postProcessFn = typeof postProcessFn === 'function' ? postProcessFn : (x => x); |
| const rawContent = content; |
|
|
| |
| |
| |
| |
| const preEnvMacros = [ |
| |
| { regex: /<USER>/gi, replace: () => typeof env.user === 'function' ? env.user() : env.user }, |
| { regex: /<BOT>/gi, replace: () => typeof env.char === 'function' ? env.char() : env.char }, |
| { regex: /<CHAR>/gi, replace: () => typeof env.char === 'function' ? env.char() : env.char }, |
| { regex: /<CHARIFNOTGROUP>/gi, replace: () => typeof env.group === 'function' ? env.group() : env.group }, |
| { regex: /<GROUP>/gi, replace: () => typeof env.group === 'function' ? env.group() : env.group }, |
| getDiceRollMacro(), |
| ...getInstructMacros(env), |
| ...getVariableMacros(), |
| { regex: /{{newline}}/gi, replace: () => '\n' }, |
| { regex: /(?:\r?\n)*{{trim}}(?:\r?\n)*/gi, replace: () => '' }, |
| { regex: /{{noop}}/gi, replace: () => '' }, |
| { regex: /{{input}}/gi, replace: () => String($('#send_textarea').val()) }, |
| ]; |
|
|
| |
| |
| |
| |
| const postEnvMacros = [ |
| { regex: /{{maxPrompt}}/gi, replace: () => String(getMaxContextSize()) }, |
| { regex: /{{lastMessage}}/gi, replace: () => getLastMessage() }, |
| { regex: /{{lastMessageId}}/gi, replace: () => String(getLastMessageId() ?? '') }, |
| { regex: /{{lastUserMessage}}/gi, replace: () => getLastUserMessage() }, |
| { regex: /{{lastCharMessage}}/gi, replace: () => getLastCharMessage() }, |
| { regex: /{{firstIncludedMessageId}}/gi, replace: () => String(getFirstIncludedMessageId() ?? '') }, |
| { regex: /{{firstDisplayedMessageId}}/gi, replace: () => String(getFirstDisplayedMessageId() ?? '') }, |
| { regex: /{{lastSwipeId}}/gi, replace: () => String(getLastSwipeId() ?? '') }, |
| { regex: /{{currentSwipeId}}/gi, replace: () => String(getCurrentSwipeId() ?? '') }, |
| { regex: /{{reverse:(.+?)}}/gi, replace: (_, str) => Array.from(str).reverse().join('') }, |
| { regex: /\{\{\/\/([\s\S]*?)\}\}/gm, replace: () => '' }, |
| { regex: /{{time}}/gi, replace: () => moment().format('LT') }, |
| { regex: /{{date}}/gi, replace: () => moment().format('LL') }, |
| { regex: /{{weekday}}/gi, replace: () => moment().format('dddd') }, |
| { regex: /{{isotime}}/gi, replace: () => moment().format('HH:mm') }, |
| { regex: /{{isodate}}/gi, replace: () => moment().format('YYYY-MM-DD') }, |
| { regex: /{{datetimeformat +([^}]*)}}/gi, replace: (_, format) => moment().format(format) }, |
| { regex: /{{idle_duration}}/gi, replace: () => getTimeSinceLastMessage() }, |
| { regex: /{{time_UTC([-+]\d+)}}/gi, replace: (_, offset) => moment().utc().utcOffset(parseInt(offset, 10)).format('LT') }, |
| { regex: /{{outlet::(.+?)}}/gi, replace: (_, key) => getOutletPrompt(key.trim()) || '' }, |
| getTimeDiffMacro(), |
| getBannedWordsMacro(), |
| getRandomReplaceMacro(), |
| getPickReplaceMacro(rawContent), |
| ]; |
|
|
| |
| MacrosParser.populateEnv(env); |
| const nonce = uuidv4(); |
| const envMacros = []; |
|
|
| |
| for (const varName in env) { |
| if (!Object.hasOwn(env, varName)) continue; |
|
|
| const envRegex = new RegExp(`{{${escapeRegex(varName)}}}`, 'gi'); |
| const envReplace = () => { |
| const param = env[varName]; |
| const value = MacrosParser.sanitizeMacroValue(typeof param === 'function' ? param(nonce) : param); |
| return value; |
| }; |
|
|
| envMacros.push({ regex: envRegex, replace: envReplace }); |
| } |
|
|
| const macros = [...preEnvMacros, ...envMacros, ...postEnvMacros]; |
|
|
| for (const macro of macros) { |
| |
| if (!content) { |
| break; |
| } |
|
|
| |
| if (!macro.regex.source.startsWith('<') && !content.includes('{{')) { |
| break; |
| } |
|
|
| try { |
| content = content.replace(macro.regex, (...args) => postProcessFn(macro.replace(...args))); |
| } catch (e) { |
| console.warn(`Macro content can't be replaced: ${macro.regex} in ${content}`, e); |
| } |
| } |
|
|
| return content; |
| } |
|
|
| export function initMacros() { |
| |
| if (!power_user.experimental_macro_engine) { |
| function initLastGenerationType() { |
| let lastGenerationType = ''; |
|
|
| MacrosParser.registerMacro('lastGenerationType', |
| () => lastGenerationType, |
| 'Returns the type of the last generation (e.g., "normal", "swipe", "continue", "impersonate", "quiet").', |
| ); |
|
|
| eventSource.on(event_types.GENERATION_STARTED, (type, _params, isDryRun) => { |
| if (isDryRun) return; |
| lastGenerationType = type || 'normal'; |
| }); |
|
|
| eventSource.on(event_types.CHAT_CHANGED, () => { |
| lastGenerationType = ''; |
| }); |
| } |
|
|
| MacrosParser.registerMacro('isMobile', |
| () => String(isMobile()), |
| 'Returns "true" if the user is on a mobile device, "false" otherwise.', |
| ); |
| initLastGenerationType(); |
| } |
|
|
| |
| initRegisterMacros(); |
| } |
|
|