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 | import { MacroEnvBuilder } from './scripts/macros/engine/MacroEnvBuilder.js'; | 282 | import { MacroEnvBuilder } from './scripts/macros/engine/MacroEnvBuilder.js'; |
| 283 | import { MacroEngine } from './scripts/macros/engine/MacroEngine.js'; | 283 | import { MacroEngine } from './scripts/macros/engine/MacroEngine.js'; |
| 284 | import { addChatBackupsBrowser } from './scripts/chat-backups.js'; | 284 | import { addChatBackupsBrowser } from './scripts/chat-backups.js'; |
| 285 | import { onboardingExperimentalMacroEngine } from './scripts/macros/engine/MacroDiagnostics.js'; | ||
| 285 | 286 | ||
| 286 | // API OBJECT FOR EXTERNAL WIRING | 287 | // API OBJECT FOR EXTERNAL WIRING |
| 287 | globalThis.SillyTavern = { | 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 | const environment = {}; | 2729 | const environment = {}; |
| 2715 | 2730 | ||
| 2716 | if (typeof _original === 'string') { | 2731 | if (typeof _original === 'string') { |
| @@ -13,6 +13,7 @@ import { | |||
| 13 | import { enumIcons } from '../slash-commands/SlashCommandCommonEnumsProvider.js'; | 13 | import { enumIcons } from '../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 14 | import { ValidFlagSymbols } from '../macros/engine/MacroFlags.js'; | 14 | import { ValidFlagSymbols } from '../macros/engine/MacroFlags.js'; |
| 15 | import { MACRO_VARIABLE_SHORTHAND_PATTERN } from '../macros/engine/MacroLexer.js'; | 15 | import { MACRO_VARIABLE_SHORTHAND_PATTERN } from '../macros/engine/MacroLexer.js'; |
| 16 | import { onboardingExperimentalMacroEngine } from '../macros/engine/MacroDiagnostics.js'; | ||
| 16 | 17 | ||
| 17 | /** @typedef {import('../macros/engine/MacroRegistry.js').MacroDefinition} MacroDefinition */ | 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 | // Check for variable shorthand prefix (. or $) | 1101 | // Check for variable shorthand prefix (. or $) |
| 1097 | // These trigger variable expression mode instead of regular macro parsing | 1102 | // These trigger variable expression mode instead of regular macro parsing |
| 1098 | /** @type {'.'|'$'|null} */ | 1103 | /** @type {'.'|'$'|null} */ |
| @@ -1183,6 +1188,8 @@ export function parseMacroContext(macroText, cursorOffset) { | |||
| 1183 | // For invalid trailing chars, none of the typing flags will be true | 1188 | // For invalid trailing chars, none of the typing flags will be true |
| 1184 | const isOperatorComplete = (variableOperator === '++' || variableOperator === '--'); | 1189 | const isOperatorComplete = (variableOperator === '++' || variableOperator === '--'); |
| 1185 | 1190 | ||
| 1191 | void onboardingExperimentalMacroEngine('variable shorthands'); | ||
| 1192 | |||
| 1186 | // Return early for variable shorthand - different structure than regular macros | 1193 | // Return early for variable shorthand - different structure than regular macros |
| 1187 | return { | 1194 | return { |
| 1188 | fullText: macroText, | 1195 | fullText: macroText, |
| @@ -1308,6 +1315,10 @@ export function parseMacroContext(macroText, cursorOffset) { | |||
| 1308 | 1315 | ||
| 1309 | const leftPadding = macroText.match(/^\s+/)?.[0] ?? ''; | 1316 | const leftPadding = macroText.match(/^\s+/)?.[0] ?? ''; |
| 1310 | 1317 | ||
| 1318 | if (leftPadding) { | ||
| 1319 | void onboardingExperimentalMacroEngine('leading whitespace'); | ||
| 1320 | } | ||
| 1321 | |||
| 1311 | // Clean identifier: strip trailing colons (for partial :: typing) | 1322 | // Clean identifier: strip trailing colons (for partial :: typing) |
| 1312 | let cleanIdentifier = identifierOnly.replace(/:+$/, ''); | 1323 | let cleanIdentifier = identifierOnly.replace(/:+$/, ''); |
| 1313 | 1324 | ||
| @@ -3,6 +3,13 @@ | |||
| 3 | /** @typedef {import('chevrotain').ILexingError} ILexingError */ | 3 | /** @typedef {import('chevrotain').ILexingError} ILexingError */ |
| 4 | /** @typedef {import('chevrotain').IRecognitionException} IRecognitionException */ | 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 | * @typedef {Object} MacroErrorContext | 14 | * @typedef {Object} MacroErrorContext |
| 8 | * @property {string} [macroName] | 15 | * @property {string} [macroName] |
| @@ -22,6 +29,46 @@ | |||
| 22 | * @typedef {MacroErrorContext & { message: string, error?: any }} MacroLogOptions | 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 | * Creates an error representing a runtime macro invocation problem (such as | 73 | * Creates an error representing a runtime macro invocation problem (such as |
| 27 | * arity or type mismatches). These errors are intended to be caught by the | 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 | import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js'; | 39 | import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js'; |
| 40 | import { chat_metadata } from '/script.js'; | 40 | import { chat_metadata } from '/script.js'; |
| 41 | import { extension_settings } from '../extensions.js'; | 41 | import { extension_settings } from '../extensions.js'; |
| 42 | import { onboardingExperimentalMacroEngine } from '../macros/engine/MacroDiagnostics.js'; | ||
| 42 | 43 | ||
| 43 | /** @typedef {import('./SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture */ | 44 | /** @typedef {import('./SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture */ |
| 44 | /** @typedef {import('./SlashCommand.js').NamedArguments} NamedArguments */ | 45 | /** @typedef {import('./SlashCommand.js').NamedArguments} NamedArguments */ |
| @@ -618,6 +619,8 @@ export class SlashCommandParser { | |||
| 618 | resultStart = conditionStartInText + macroNameStart; | 619 | resultStart = conditionStartInText + macroNameStart; |
| 619 | } | 620 | } |
| 620 | 621 | ||
| 622 | await onboardingExperimentalMacroEngine('{{if}} macro'); | ||
| 623 | |||
| 621 | const result = new AutoCompleteNameResult( | 624 | const result = new AutoCompleteNameResult( |
| 622 | resultIdentifier, | 625 | resultIdentifier, |
| 623 | resultStart, | 626 | resultStart, |
| @@ -736,6 +739,8 @@ export class SlashCommandParser { | |||
| 736 | scopedMacroName: scopedMacro.name, | 739 | scopedMacroName: scopedMacro.name, |
| 737 | }; | 740 | }; |
| 738 | 741 | ||
| 742 | await onboardingExperimentalMacroEngine('scoped macros'); | ||
| 743 | |||
| 739 | // Only show the scoped macro's details - no list of other macros | 744 | // Only show the scoped macro's details - no list of other macros |
| 740 | // This creates a "details only" view showing the scoped arg being typed | 745 | // This creates a "details only" view showing the scoped arg being typed |
| 741 | const macroDef = macroSystem.registry.getPrimaryMacro(scopedMacro.name); | 746 | const macroDef = macroSystem.registry.getPrimaryMacro(scopedMacro.name); |