Macros 2.0 (v0.5.2) - Onboarding for new Macro Engine (#4955) * Macros 2.0 [Feature] - Add onboarding popup for experimental macro engine features - Add `onbordingExperimentalMacroEngine()` function in MacroDiagnostics.js to detect and prompt users about experimental features - Detect experimental macro features in `substituteParamsLegacy()`: {{if}}, scoped macros, macro flags, variable shorthands, nested macros - Add feature detection in autocomplete parser for macro flags, variable shorthands, and leading whitespace * Fix typo: rename `onbording` to `onboarding` throughout macro diagnostics system * Add `void` operator to async onboarding calls and fix typo in comment - Add `void` operator to all `onboardingExperimentalMacroEngine()` calls to explicitly discard Promise return values - Fix typo in MacroDiagnostics.js comment: "hat" → "that" * Fix typo in experimental macro engine onboarding popup: "anytime time" → "any time" --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -282,6 +282,7 @@ import { AudioPlayer } from './scripts/audio-player.js'; | ||
| 282 | 282 | import { MacroEnvBuilder } from './scripts/macros/engine/MacroEnvBuilder.js'; |
| 283 | 283 | import { MacroEngine } from './scripts/macros/engine/MacroEngine.js'; |
| 284 | 284 | import { addChatBackupsBrowser } from './scripts/chat-backups.js'; |
| 285 | +import { onboardingExperimentalMacroEngine } from './scripts/macros/engine/MacroDiagnostics.js'; | |
| 285 | 286 | |
| 286 | 287 | // API OBJECT FOR EXTERNAL WIRING |
| 287 | 288 | globalThis.SillyTavern = { |
| @@ -2711,6 +2712,20 @@ export function substituteParamsLegacy(content, _name1, _name2, _original, _grou | ||
| 2711 | 2712 | }); |
| 2712 | 2713 | } |
| 2713 | 2714 | |
| 2715 | + // Try to roughly detect experimental macro features to show the onboarding if needed. | |
| 2716 | + // This does not have to be 100% accurate, only best effort what we can quickly check. | |
| 2717 | + // Only do this if the warning wasn't shown yet, to prevent needless regex checks. | |
| 2718 | + if (accountStorage.getItem('slash_command_experimental_engine_warning_shown') !== 'true') { | |
| 2719 | + let feature = /** @type {string|null} */ (null); | |
| 2720 | + if (/{{\s*if/.test(content)) feature = '{{if}} macro'; | |
| 2721 | + else if (/{{\s*\//.test(content)) feature = 'scoped macro'; | |
| 2722 | + else if (/{{\s*[!?~#/]/.test(content)) feature = 'macro flags'; | |
| 2723 | + else if (/{{\s*[.$]/.test(content)) feature = 'variable shorthands'; | |
| 2724 | + else if (/\{\{(?:(?!\}\}).)*\{\{(?=[\s\S]*?\}\}[\s\S]*?\}\})/.test(content)) feature = 'nested macro'; | |
| 2725 | + | |
| 2726 | + if (feature) void onboardingExperimentalMacroEngine(feature); | |
| 2727 | + } | |
| 2728 | + | |
| 2714 | 2729 | const environment = {}; |
| 2715 | 2730 | |
| 2716 | 2731 | if (typeof _original === 'string') { |
| @@ -13,6 +13,7 @@ import { | ||
| 13 | 13 | import { enumIcons } from '../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 14 | 14 | import { ValidFlagSymbols } from '../macros/engine/MacroFlags.js'; |
| 15 | 15 | import { MACRO_VARIABLE_SHORTHAND_PATTERN } from '../macros/engine/MacroLexer.js'; |
| 16 | +import { onboardingExperimentalMacroEngine } from '../macros/engine/MacroDiagnostics.js'; | |
| 16 | 17 | |
| 17 | 18 | /** @typedef {import('../macros/engine/MacroRegistry.js').MacroDefinition} MacroDefinition */ |
| 18 | 19 | |
| @@ -1093,6 +1094,10 @@ export function parseMacroContext(macroText, cursorOffset) { | ||
| 1093 | 1094 | } |
| 1094 | 1095 | } |
| 1095 | 1096 | |
| 1097 | + if (flags.length > 0) { | |
| 1098 | + void onboardingExperimentalMacroEngine('macro flags'); | |
| 1099 | + } | |
| 1100 | + | |
| 1096 | 1101 | // Check for variable shorthand prefix (. or $) |
| 1097 | 1102 | // These trigger variable expression mode instead of regular macro parsing |
| 1098 | 1103 | /** @type {'.'|'$'|null} */ |
| @@ -1183,6 +1188,8 @@ export function parseMacroContext(macroText, cursorOffset) { | ||
| 1183 | 1188 | // For invalid trailing chars, none of the typing flags will be true |
| 1184 | 1189 | const isOperatorComplete = (variableOperator === '++' || variableOperator === '--'); |
| 1185 | 1190 | |
| 1191 | + void onboardingExperimentalMacroEngine('variable shorthands'); | |
| 1192 | + | |
| 1186 | 1193 | // Return early for variable shorthand - different structure than regular macros |
| 1187 | 1194 | return { |
| 1188 | 1195 | fullText: macroText, |
| @@ -1308,6 +1315,10 @@ export function parseMacroContext(macroText, cursorOffset) { | ||
| 1308 | 1315 | |
| 1309 | 1316 | const leftPadding = macroText.match(/^\s+/)?.[0] ?? ''; |
| 1310 | 1317 | |
| 1318 | + if (leftPadding) { | |
| 1319 | + void onboardingExperimentalMacroEngine('leading whitespace'); | |
| 1320 | + } | |
| 1321 | + | |
| 1311 | 1322 | // Clean identifier: strip trailing colons (for partial :: typing) |
| 1312 | 1323 | let cleanIdentifier = identifierOnly.replace(/:+$/, ''); |
| 1313 | 1324 | |
| @@ -3,6 +3,13 @@ | ||
| 3 | 3 | /** @typedef {import('chevrotain').ILexingError} ILexingError */ |
| 4 | 4 | /** @typedef {import('chevrotain').IRecognitionException} IRecognitionException */ |
| 5 | 5 | |
| 6 | +import { saveSettingsDebounced } from '/script.js'; | |
| 7 | +import { t } from '/scripts/i18n.js'; | |
| 8 | +import { Popup, POPUP_RESULT } from '/scripts/popup.js'; | |
| 9 | +import { power_user } from '/scripts/power-user.js'; | |
| 10 | +import { accountStorage } from '/scripts/util/AccountStorage.js'; | |
| 11 | +import { SimpleMutex } from '/scripts/util/SimpleMutex.js'; | |
| 12 | + | |
| 6 | 13 | /** |
| 7 | 14 | * @typedef {Object} MacroErrorContext |
| 8 | 15 | * @property {string} [macroName] |
| @@ -22,6 +29,46 @@ | ||
| 22 | 29 | * @typedef {MacroErrorContext & { message: string, error?: any }} MacroLogOptions |
| 23 | 30 | */ |
| 24 | 31 | |
| 32 | + | |
| 33 | +// Use mutex here so even on parallel usage without awaiting the popup, this will only show up once. | |
| 34 | +export const onboardingExperimentalMacroEngineMutex = new SimpleMutex(onboardingExperimentalMacroEngineUnsafe); | |
| 35 | + | |
| 36 | +/** | |
| 37 | + * Onboards the user to use the experimental macro engine. | |
| 38 | + * Asks the user to enable it if they haven't already. | |
| 39 | + * | |
| 40 | + * @param {string|null} feature - The feature that requires the experimental macro engine, or null if not applicable or unknown. | |
| 41 | + * @returns {Promise<void>} - A promise that resolves when the user has been onboarded. | |
| 42 | + */ | |
| 43 | +export const onboardingExperimentalMacroEngine = onboardingExperimentalMacroEngineMutex.update.bind(onboardingExperimentalMacroEngineMutex); | |
| 44 | + | |
| 45 | +async function onboardingExperimentalMacroEngineUnsafe(feature = null) { | |
| 46 | + // Show a popup once telling a user that they are using experimental features that only work with the new engine. | |
| 47 | + // Ask them if they want to turn the experimental engine on. | |
| 48 | + if (power_user.experimental_macro_engine) return; | |
| 49 | + | |
| 50 | + // If already shown, do not show again | |
| 51 | + const shown = accountStorage.getItem('slash_command_experimental_engine_warning_shown'); | |
| 52 | + if (shown === 'true') return; | |
| 53 | + | |
| 54 | + const result = await Popup.show.confirm(t`Experimental Macro Engine`, ` | |
| 55 | + <p>${t`You are using experimental macro features that require the new macro engine.`}</p> | |
| 56 | + ${feature ? `<div class="info-block hint"> | |
| 57 | + <span>${t`Recognized Feature: `}<strong>${feature}</strong></span> | |
| 58 | + </div>` : ''} | |
| 59 | + <p>${t`For more information on the new macro engine, visit the <br />${`<a href="https://docs.sillytavern.app/usage/core-concepts/macros/">${t`Macro Documentation`}</a>`}.`}</p> | |
| 60 | + <p>${t`You can enable the engine any time under:<br />${t`User Settings`} → ${t`Experimental Macro Engine`}`}</p> | |
| 61 | + <p>${t`Would you like to enable it now?`}</p>`); | |
| 62 | + if (result == POPUP_RESULT.AFFIRMATIVE) { | |
| 63 | + power_user.experimental_macro_engine = true; | |
| 64 | + $('#experimental_macro_engine').prop('checked', power_user.experimental_macro_engine); | |
| 65 | + saveSettingsDebounced(); | |
| 66 | + } | |
| 67 | + | |
| 68 | + // Only show this once | |
| 69 | + accountStorage.setItem('slash_command_experimental_engine_warning_shown', 'true'); | |
| 70 | +} | |
| 71 | + | |
| 25 | 72 | /** |
| 26 | 73 | * Creates an error representing a runtime macro invocation problem (such as |
| 27 | 74 | * arity or type mismatches). These errors are intended to be caught by the |
| @@ -39,6 +39,7 @@ import { macros as macroSystem } from '../macros/macro-system.js'; | ||
| 39 | 39 | import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js'; |
| 40 | 40 | import { chat_metadata } from '/script.js'; |
| 41 | 41 | import { extension_settings } from '../extensions.js'; |
| 42 | +import { onboardingExperimentalMacroEngine } from '../macros/engine/MacroDiagnostics.js'; | |
| 42 | 43 | |
| 43 | 44 | /** @typedef {import('./SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture */ |
| 44 | 45 | /** @typedef {import('./SlashCommand.js').NamedArguments} NamedArguments */ |
| @@ -618,6 +619,8 @@ export class SlashCommandParser { | ||
| 618 | 619 | resultStart = conditionStartInText + macroNameStart; |
| 619 | 620 | } |
| 620 | 621 | |
| 622 | + await onboardingExperimentalMacroEngine('{{if}} macro'); | |
| 623 | + | |
| 621 | 624 | const result = new AutoCompleteNameResult( |
| 622 | 625 | resultIdentifier, |
| 623 | 626 | resultStart, |
| @@ -736,6 +739,8 @@ export class SlashCommandParser { | ||
| 736 | 739 | scopedMacroName: scopedMacro.name, |
| 737 | 740 | }; |
| 738 | 741 | |
| 742 | + await onboardingExperimentalMacroEngine('scoped macros'); | |
| 743 | + | |
| 739 | 744 | // Only show the scoped macro's details - no list of other macros |
| 740 | 745 | // This creates a "details only" view showing the scoped arg being typed |
| 741 | 746 | const macroDef = macroSystem.registry.getPrimaryMacro(scopedMacro.name); |