@eventop/sdk 1.2.14 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core.cjs +169 -33
- package/dist/core.js +169 -33
- package/dist/react/core.cjs +169 -33
- package/dist/react/core.js +169 -33
- package/package.json +1 -1
package/dist/core.cjs
CHANGED
|
@@ -221,6 +221,71 @@ function injectStyles(theme, pos) {
|
|
|
221
221
|
style.textContent = `
|
|
222
222
|
#sai-trigger, #sai-panel { ${buildCSSVars(theme)} }
|
|
223
223
|
|
|
224
|
+
/* ── Ring Light Animation ── */
|
|
225
|
+
@keyframes sai-ring-pulse {
|
|
226
|
+
0% {
|
|
227
|
+
box-shadow:
|
|
228
|
+
0 0 0 0 rgba(var(--sai-accent-rgb), 0.7),
|
|
229
|
+
0 0 0 0 rgba(var(--sai-accent-rgb), 0.5),
|
|
230
|
+
0 0 0 0 rgba(var(--sai-accent-rgb), 0.3);
|
|
231
|
+
}
|
|
232
|
+
50% {
|
|
233
|
+
box-shadow:
|
|
234
|
+
0 0 0 8px rgba(var(--sai-accent-rgb), 0),
|
|
235
|
+
0 0 0 16px rgba(var(--sai-accent-rgb), 0.3),
|
|
236
|
+
0 0 0 24px rgba(var(--sai-accent-rgb), 0);
|
|
237
|
+
}
|
|
238
|
+
100% {
|
|
239
|
+
box-shadow:
|
|
240
|
+
0 0 0 16px rgba(var(--sai-accent-rgb), 0),
|
|
241
|
+
0 0 0 32px rgba(var(--sai-accent-rgb), 0),
|
|
242
|
+
0 0 0 48px rgba(var(--sai-accent-rgb), 0);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@keyframes sai-ring-pulse-hard {
|
|
247
|
+
0% {
|
|
248
|
+
box-shadow:
|
|
249
|
+
inset 0 0 0 2px var(--sai-accent),
|
|
250
|
+
0 0 0 3px var(--sai-accent),
|
|
251
|
+
0 0 20px var(--sai-accent);
|
|
252
|
+
}
|
|
253
|
+
50% {
|
|
254
|
+
box-shadow:
|
|
255
|
+
inset 0 0 0 2px var(--sai-accent),
|
|
256
|
+
0 0 0 8px var(--sai-accent),
|
|
257
|
+
0 0 40px var(--sai-accent);
|
|
258
|
+
}
|
|
259
|
+
100% {
|
|
260
|
+
box-shadow:
|
|
261
|
+
inset 0 0 0 2px var(--sai-accent),
|
|
262
|
+
0 0 0 3px var(--sai-accent),
|
|
263
|
+
0 0 20px var(--sai-accent);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@keyframes sai-element-highlight {
|
|
268
|
+
0% { filter: brightness(1); }
|
|
269
|
+
50% { filter: brightness(1.15); }
|
|
270
|
+
100% { filter: brightness(1); }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* Highlighted element styles */
|
|
274
|
+
.sai-highlighted {
|
|
275
|
+
position: relative !important;
|
|
276
|
+
animation: sai-ring-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
|
|
277
|
+
z-index: 99997 !important;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.sai-highlighted.sai-highlight-hard {
|
|
281
|
+
animation: sai-ring-pulse-hard 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.sai-highlighted.sai-highlight-subtle {
|
|
285
|
+
animation: sai-element-highlight 2s ease-in-out infinite !important;
|
|
286
|
+
filter: drop-shadow(0 0 8px var(--sai-accent)) !important;
|
|
287
|
+
}
|
|
288
|
+
|
|
224
289
|
/* ── Trigger ── */
|
|
225
290
|
#sai-trigger {
|
|
226
291
|
position: fixed; ${triggerCSS};
|
|
@@ -486,13 +551,7 @@ function buildSystemPrompt(config) {
|
|
|
486
551
|
var _f$screen;
|
|
487
552
|
return (_f$screen = f.screen) === null || _f$screen === void 0 ? void 0 : _f$screen.id;
|
|
488
553
|
}).map(f => f.screen.id))];
|
|
489
|
-
|
|
490
|
-
// Split features into live (current page) and ghost (other pages).
|
|
491
|
-
// This lets the AI understand what's immediately available vs reachable
|
|
492
|
-
// via navigation, and craft the right message when something isn't found.
|
|
493
|
-
const liveFeatures = (config.features || []).filter(f => !f._ghost);
|
|
494
|
-
const ghostFeatures = (config.features || []).filter(f => f._ghost);
|
|
495
|
-
const summarise = f => {
|
|
554
|
+
const featureSummary = (config.features || []).map(f => {
|
|
496
555
|
var _f$screen2, _f$flow;
|
|
497
556
|
const entry = {
|
|
498
557
|
id: f.id,
|
|
@@ -507,18 +566,15 @@ function buildSystemPrompt(config) {
|
|
|
507
566
|
entry.note = `This feature has ${f.flow.length} sequential sub-steps. Include ONE step per flow entry.`;
|
|
508
567
|
}
|
|
509
568
|
return entry;
|
|
510
|
-
};
|
|
511
|
-
const liveSection = liveFeatures.length ? `CURRENT PAGE FEATURES (user is here now):\n${JSON.stringify(liveFeatures.map(summarise), null, 2)}` : `CURRENT PAGE FEATURES: none registered yet.`;
|
|
512
|
-
const ghostSection = ghostFeatures.length ? `OTHER PAGE FEATURES (reachable via navigation — SDK handles it automatically):\n${JSON.stringify(ghostFeatures.map(summarise), null, 2)}` : '';
|
|
569
|
+
});
|
|
513
570
|
return `
|
|
514
571
|
You are an in-app assistant called "${config.assistantName || 'AI Guide'}" for "${config.appName}".
|
|
515
572
|
Your ONLY job: guide users step-by-step through tasks using the feature map below.
|
|
516
573
|
|
|
517
|
-
${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. The SDK
|
|
518
|
-
|
|
519
|
-
${liveSection}
|
|
574
|
+
${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. Features are screen-specific. The SDK handles navigation — just pick the right features.` : ''}
|
|
520
575
|
|
|
521
|
-
|
|
576
|
+
FEATURE MAP (only reference IDs from this list):
|
|
577
|
+
${JSON.stringify(featureSummary, null, 2)}
|
|
522
578
|
|
|
523
579
|
RESPOND ONLY with this exact JSON — no markdown, no extra text:
|
|
524
580
|
{
|
|
@@ -535,26 +591,16 @@ RESPOND ONLY with this exact JSON — no markdown, no extra text:
|
|
|
535
591
|
}
|
|
536
592
|
|
|
537
593
|
RULES:
|
|
538
|
-
1. The step "id" MUST match a feature id from the
|
|
594
|
+
1. The step "id" MUST match a feature id from the feature map.
|
|
539
595
|
2. Only use selectors and IDs from the feature map. Never invent them.
|
|
540
|
-
3.
|
|
541
|
-
4.
|
|
542
|
-
5.
|
|
596
|
+
3. No matching feature → steps: [], explain kindly in message.
|
|
597
|
+
4. position values: top | bottom | left | right | auto only.
|
|
598
|
+
5. Order steps logically. For multi-step flows, order as the user encounters them.
|
|
599
|
+
6. For forms: ALWAYS include a step for the form section or first input BEFORE the
|
|
543
600
|
continue/submit button. The button step must always be LAST in its section.
|
|
544
|
-
|
|
601
|
+
7. If a feature has a flow, include one step per flow entry using the same feature id —
|
|
545
602
|
the SDK expands them automatically.
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
WHEN THE USER'S REQUEST DOESN'T MATCH ANY FEATURE:
|
|
549
|
-
- If it partially matches something (e.g. they said "create template" and there's a
|
|
550
|
-
"template-gallery" feature): guide them to the closest matching feature and explain
|
|
551
|
-
what it does. Don't say you can't help.
|
|
552
|
-
- If it matches a feature on another page (ghost): include that feature's steps normally.
|
|
553
|
-
The SDK will navigate there automatically. Do NOT tell the user to navigate manually.
|
|
554
|
-
- If there is genuinely no match anywhere in the feature map: set steps to [] and say
|
|
555
|
-
something like "That doesn't seem to be a feature in ${config.appName} yet. Here's
|
|
556
|
-
what I can help you with:" then list 2-3 relevant features from the map by name.
|
|
557
|
-
Never say you "can only guide through available features" — always offer alternatives.
|
|
603
|
+
8. Never skip features in a required sequence. Include every step end-to-end.
|
|
558
604
|
`.trim();
|
|
559
605
|
}
|
|
560
606
|
|
|
@@ -1025,6 +1071,7 @@ function expandFlowSteps(aiStep, feature) {
|
|
|
1025
1071
|
|
|
1026
1072
|
// Loads Shepherd.js, builds + runs tours, wires up advanceOn listeners,
|
|
1027
1073
|
// progress indicators, pause/resume, and step-level error display.
|
|
1074
|
+
// ENHANCED: Adds animated ring light effect to highlighted elements.
|
|
1028
1075
|
|
|
1029
1076
|
const SHEPHERD_JS = 'https://cdn.jsdelivr.net/npm/shepherd.js@11.2.0/dist/js/shepherd.min.js';
|
|
1030
1077
|
|
|
@@ -1045,6 +1092,61 @@ async function ensureShepherd() {
|
|
|
1045
1092
|
await loadScript(SHEPHERD_JS);
|
|
1046
1093
|
}
|
|
1047
1094
|
|
|
1095
|
+
// ─── Highlight management ────────────────────────────────────────────────────
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Applies the ring light animation to an element.
|
|
1099
|
+
*
|
|
1100
|
+
* @param {HTMLElement} el
|
|
1101
|
+
* @param {string} [style='default'] - 'default' | 'hard' | 'subtle'
|
|
1102
|
+
*/
|
|
1103
|
+
function applyHighlight(el, style = 'default') {
|
|
1104
|
+
if (!el) return;
|
|
1105
|
+
|
|
1106
|
+
// Set CSS variables for accent color parsing in RGB format
|
|
1107
|
+
const accentColor = getComputedStyle(el).getPropertyValue('--sai-accent') || '#e94560';
|
|
1108
|
+
const rgb = hexToRgb(accentColor.trim());
|
|
1109
|
+
if (rgb) {
|
|
1110
|
+
el.style.setProperty('--sai-accent-rgb', `${rgb.r}, ${rgb.g}, ${rgb.b}`);
|
|
1111
|
+
}
|
|
1112
|
+
el.classList.add('sai-highlighted');
|
|
1113
|
+
if (style === 'hard') {
|
|
1114
|
+
el.classList.add('sai-highlight-hard');
|
|
1115
|
+
} else if (style === 'subtle') {
|
|
1116
|
+
el.classList.add('sai-highlight-subtle');
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Scroll into view with extra padding
|
|
1120
|
+
if (el.scrollIntoView) {
|
|
1121
|
+
el.scrollIntoView({
|
|
1122
|
+
behavior: 'smooth',
|
|
1123
|
+
block: 'center'
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Removes the ring light animation from an element.
|
|
1130
|
+
*/
|
|
1131
|
+
function removeHighlight(el) {
|
|
1132
|
+
if (!el) return;
|
|
1133
|
+
el.classList.remove('sai-highlighted', 'sai-highlight-hard', 'sai-highlight-subtle');
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Converts hex color to RGB object.
|
|
1138
|
+
* @param {string} hex
|
|
1139
|
+
* @returns {object|null}
|
|
1140
|
+
*/
|
|
1141
|
+
function hexToRgb(hex) {
|
|
1142
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
1143
|
+
return result ? {
|
|
1144
|
+
r: parseInt(result[1], 16),
|
|
1145
|
+
g: parseInt(result[2], 16),
|
|
1146
|
+
b: parseInt(result[3], 16)
|
|
1147
|
+
} : null;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1048
1150
|
// ─── Feature-map merge ────────────────────────────────────────────────────────
|
|
1049
1151
|
|
|
1050
1152
|
/**
|
|
@@ -1078,6 +1180,7 @@ function mergeWithFeature(step) {
|
|
|
1078
1180
|
* 1. Cross-page navigation (route prop) with automatic waiting
|
|
1079
1181
|
* 2. Legacy screen navigation (screen.navigate)
|
|
1080
1182
|
* 3. Element waiting (waitFor prop)
|
|
1183
|
+
* 4. Ring light animation application
|
|
1081
1184
|
*
|
|
1082
1185
|
* @param {object} step
|
|
1083
1186
|
* @param {number} waitTimeout
|
|
@@ -1101,6 +1204,10 @@ function makeBeforeShowPromise(step, waitTimeout) {
|
|
|
1101
1204
|
});
|
|
1102
1205
|
} catch (_) {/* non-fatal */}
|
|
1103
1206
|
await waitForElement(postNavMerge.selector, waitTimeout);
|
|
1207
|
+
|
|
1208
|
+
// Apply ring light animation to the element
|
|
1209
|
+
const el = document.querySelector(postNavMerge.selector);
|
|
1210
|
+
if (el) applyHighlight(el, step._highlightStyle || 'default');
|
|
1104
1211
|
return;
|
|
1105
1212
|
}
|
|
1106
1213
|
}
|
|
@@ -1110,6 +1217,12 @@ function makeBeforeShowPromise(step, waitTimeout) {
|
|
|
1110
1217
|
if (freshMerged.waitFor) {
|
|
1111
1218
|
await waitForElement(freshMerged.waitFor, waitTimeout);
|
|
1112
1219
|
}
|
|
1220
|
+
|
|
1221
|
+
// Apply ring light animation to the main selector
|
|
1222
|
+
if (freshMerged.selector) {
|
|
1223
|
+
const el = document.querySelector(freshMerged.selector);
|
|
1224
|
+
if (el) applyHighlight(el, step._highlightStyle || 'default');
|
|
1225
|
+
}
|
|
1113
1226
|
})();
|
|
1114
1227
|
}
|
|
1115
1228
|
|
|
@@ -1158,11 +1271,13 @@ function addProgressIndicator(shepherdStep, index, total) {
|
|
|
1158
1271
|
|
|
1159
1272
|
/**
|
|
1160
1273
|
* Starts a Shepherd tour from the given steps array.
|
|
1274
|
+
* ENHANCED: Applies ring light animations to highlighted elements.
|
|
1161
1275
|
*
|
|
1162
1276
|
* @param {Array} steps
|
|
1163
1277
|
* @param {object} [options]
|
|
1164
1278
|
* @param {boolean} [options.showProgress=true]
|
|
1165
1279
|
* @param {number} [options.waitTimeout=8000]
|
|
1280
|
+
* @param {string} [options.highlightStyle='default'] - 'default' | 'hard' | 'subtle'
|
|
1166
1281
|
*/
|
|
1167
1282
|
async function runTour(steps, options = {}) {
|
|
1168
1283
|
var _state$config2;
|
|
@@ -1175,9 +1290,13 @@ async function runTour(steps, options = {}) {
|
|
|
1175
1290
|
if (!(steps !== null && steps !== void 0 && steps.length)) return;
|
|
1176
1291
|
const {
|
|
1177
1292
|
showProgress = true,
|
|
1178
|
-
waitTimeout = 8000
|
|
1293
|
+
waitTimeout = 8000,
|
|
1294
|
+
highlightStyle = 'default'
|
|
1179
1295
|
} = options;
|
|
1180
|
-
const mergedSteps = steps.map(
|
|
1296
|
+
const mergedSteps = steps.map(step => ({
|
|
1297
|
+
...mergeWithFeature(step),
|
|
1298
|
+
_highlightStyle: highlightStyle
|
|
1299
|
+
}));
|
|
1181
1300
|
|
|
1182
1301
|
// Legacy: navigate to the correct screen for the first step
|
|
1183
1302
|
const firstFeature = (_state$config2 = config) === null || _state$config2 === void 0 || (_state$config2 = _state$config2.features) === null || _state$config2 === void 0 ? void 0 : _state$config2.find(f => {
|
|
@@ -1243,12 +1362,24 @@ async function runTour(steps, options = {}) {
|
|
|
1243
1362
|
});
|
|
1244
1363
|
step._shepherdRef = shepherdStep;
|
|
1245
1364
|
shepherdStep._isLast = isLast;
|
|
1365
|
+
|
|
1366
|
+
// Clean up highlight on hide
|
|
1367
|
+
shepherdStep.on('hide', () => {
|
|
1368
|
+
if (step.selector) {
|
|
1369
|
+
const el = document.querySelector(step.selector);
|
|
1370
|
+
if (el) removeHighlight(el);
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1246
1373
|
if (hasAuto) wireAdvanceOn(shepherdStep, step.advanceOn, tour$1);
|
|
1247
1374
|
if (showProgress && expandedSteps.length > 1) {
|
|
1248
1375
|
addProgressIndicator(shepherdStep, i, expandedSteps.length);
|
|
1249
1376
|
}
|
|
1250
1377
|
});
|
|
1251
1378
|
tour$1.on('complete', () => {
|
|
1379
|
+
// Clean up all highlights
|
|
1380
|
+
document.querySelectorAll('.sai-highlighted').forEach(el => {
|
|
1381
|
+
removeHighlight(el);
|
|
1382
|
+
});
|
|
1252
1383
|
runAndClearCleanups();
|
|
1253
1384
|
setPausedSteps(null);
|
|
1254
1385
|
setPausedIndex(0);
|
|
@@ -1259,6 +1390,11 @@ async function runTour(steps, options = {}) {
|
|
|
1259
1390
|
tour$1.on('cancel', () => {
|
|
1260
1391
|
const currentStepEl = tour$1.getCurrentStep();
|
|
1261
1392
|
const currentIdx = currentStepEl ? expandedSteps.findIndex(s => s.id === currentStepEl.id) : 0;
|
|
1393
|
+
|
|
1394
|
+
// Clean up all highlights
|
|
1395
|
+
document.querySelectorAll('.sai-highlighted').forEach(el => {
|
|
1396
|
+
removeHighlight(el);
|
|
1397
|
+
});
|
|
1262
1398
|
runAndClearCleanups();
|
|
1263
1399
|
setPausedSteps(expandedSteps);
|
|
1264
1400
|
setPausedIndex(Math.max(0, currentIdx));
|
package/dist/core.js
CHANGED
|
@@ -219,6 +219,71 @@ function injectStyles(theme, pos) {
|
|
|
219
219
|
style.textContent = `
|
|
220
220
|
#sai-trigger, #sai-panel { ${buildCSSVars(theme)} }
|
|
221
221
|
|
|
222
|
+
/* ── Ring Light Animation ── */
|
|
223
|
+
@keyframes sai-ring-pulse {
|
|
224
|
+
0% {
|
|
225
|
+
box-shadow:
|
|
226
|
+
0 0 0 0 rgba(var(--sai-accent-rgb), 0.7),
|
|
227
|
+
0 0 0 0 rgba(var(--sai-accent-rgb), 0.5),
|
|
228
|
+
0 0 0 0 rgba(var(--sai-accent-rgb), 0.3);
|
|
229
|
+
}
|
|
230
|
+
50% {
|
|
231
|
+
box-shadow:
|
|
232
|
+
0 0 0 8px rgba(var(--sai-accent-rgb), 0),
|
|
233
|
+
0 0 0 16px rgba(var(--sai-accent-rgb), 0.3),
|
|
234
|
+
0 0 0 24px rgba(var(--sai-accent-rgb), 0);
|
|
235
|
+
}
|
|
236
|
+
100% {
|
|
237
|
+
box-shadow:
|
|
238
|
+
0 0 0 16px rgba(var(--sai-accent-rgb), 0),
|
|
239
|
+
0 0 0 32px rgba(var(--sai-accent-rgb), 0),
|
|
240
|
+
0 0 0 48px rgba(var(--sai-accent-rgb), 0);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@keyframes sai-ring-pulse-hard {
|
|
245
|
+
0% {
|
|
246
|
+
box-shadow:
|
|
247
|
+
inset 0 0 0 2px var(--sai-accent),
|
|
248
|
+
0 0 0 3px var(--sai-accent),
|
|
249
|
+
0 0 20px var(--sai-accent);
|
|
250
|
+
}
|
|
251
|
+
50% {
|
|
252
|
+
box-shadow:
|
|
253
|
+
inset 0 0 0 2px var(--sai-accent),
|
|
254
|
+
0 0 0 8px var(--sai-accent),
|
|
255
|
+
0 0 40px var(--sai-accent);
|
|
256
|
+
}
|
|
257
|
+
100% {
|
|
258
|
+
box-shadow:
|
|
259
|
+
inset 0 0 0 2px var(--sai-accent),
|
|
260
|
+
0 0 0 3px var(--sai-accent),
|
|
261
|
+
0 0 20px var(--sai-accent);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@keyframes sai-element-highlight {
|
|
266
|
+
0% { filter: brightness(1); }
|
|
267
|
+
50% { filter: brightness(1.15); }
|
|
268
|
+
100% { filter: brightness(1); }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* Highlighted element styles */
|
|
272
|
+
.sai-highlighted {
|
|
273
|
+
position: relative !important;
|
|
274
|
+
animation: sai-ring-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
|
|
275
|
+
z-index: 99997 !important;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.sai-highlighted.sai-highlight-hard {
|
|
279
|
+
animation: sai-ring-pulse-hard 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.sai-highlighted.sai-highlight-subtle {
|
|
283
|
+
animation: sai-element-highlight 2s ease-in-out infinite !important;
|
|
284
|
+
filter: drop-shadow(0 0 8px var(--sai-accent)) !important;
|
|
285
|
+
}
|
|
286
|
+
|
|
222
287
|
/* ── Trigger ── */
|
|
223
288
|
#sai-trigger {
|
|
224
289
|
position: fixed; ${triggerCSS};
|
|
@@ -484,13 +549,7 @@ function buildSystemPrompt(config) {
|
|
|
484
549
|
var _f$screen;
|
|
485
550
|
return (_f$screen = f.screen) === null || _f$screen === void 0 ? void 0 : _f$screen.id;
|
|
486
551
|
}).map(f => f.screen.id))];
|
|
487
|
-
|
|
488
|
-
// Split features into live (current page) and ghost (other pages).
|
|
489
|
-
// This lets the AI understand what's immediately available vs reachable
|
|
490
|
-
// via navigation, and craft the right message when something isn't found.
|
|
491
|
-
const liveFeatures = (config.features || []).filter(f => !f._ghost);
|
|
492
|
-
const ghostFeatures = (config.features || []).filter(f => f._ghost);
|
|
493
|
-
const summarise = f => {
|
|
552
|
+
const featureSummary = (config.features || []).map(f => {
|
|
494
553
|
var _f$screen2, _f$flow;
|
|
495
554
|
const entry = {
|
|
496
555
|
id: f.id,
|
|
@@ -505,18 +564,15 @@ function buildSystemPrompt(config) {
|
|
|
505
564
|
entry.note = `This feature has ${f.flow.length} sequential sub-steps. Include ONE step per flow entry.`;
|
|
506
565
|
}
|
|
507
566
|
return entry;
|
|
508
|
-
};
|
|
509
|
-
const liveSection = liveFeatures.length ? `CURRENT PAGE FEATURES (user is here now):\n${JSON.stringify(liveFeatures.map(summarise), null, 2)}` : `CURRENT PAGE FEATURES: none registered yet.`;
|
|
510
|
-
const ghostSection = ghostFeatures.length ? `OTHER PAGE FEATURES (reachable via navigation — SDK handles it automatically):\n${JSON.stringify(ghostFeatures.map(summarise), null, 2)}` : '';
|
|
567
|
+
});
|
|
511
568
|
return `
|
|
512
569
|
You are an in-app assistant called "${config.assistantName || 'AI Guide'}" for "${config.appName}".
|
|
513
570
|
Your ONLY job: guide users step-by-step through tasks using the feature map below.
|
|
514
571
|
|
|
515
|
-
${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. The SDK
|
|
516
|
-
|
|
517
|
-
${liveSection}
|
|
572
|
+
${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. Features are screen-specific. The SDK handles navigation — just pick the right features.` : ''}
|
|
518
573
|
|
|
519
|
-
|
|
574
|
+
FEATURE MAP (only reference IDs from this list):
|
|
575
|
+
${JSON.stringify(featureSummary, null, 2)}
|
|
520
576
|
|
|
521
577
|
RESPOND ONLY with this exact JSON — no markdown, no extra text:
|
|
522
578
|
{
|
|
@@ -533,26 +589,16 @@ RESPOND ONLY with this exact JSON — no markdown, no extra text:
|
|
|
533
589
|
}
|
|
534
590
|
|
|
535
591
|
RULES:
|
|
536
|
-
1. The step "id" MUST match a feature id from the
|
|
592
|
+
1. The step "id" MUST match a feature id from the feature map.
|
|
537
593
|
2. Only use selectors and IDs from the feature map. Never invent them.
|
|
538
|
-
3.
|
|
539
|
-
4.
|
|
540
|
-
5.
|
|
594
|
+
3. No matching feature → steps: [], explain kindly in message.
|
|
595
|
+
4. position values: top | bottom | left | right | auto only.
|
|
596
|
+
5. Order steps logically. For multi-step flows, order as the user encounters them.
|
|
597
|
+
6. For forms: ALWAYS include a step for the form section or first input BEFORE the
|
|
541
598
|
continue/submit button. The button step must always be LAST in its section.
|
|
542
|
-
|
|
599
|
+
7. If a feature has a flow, include one step per flow entry using the same feature id —
|
|
543
600
|
the SDK expands them automatically.
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
WHEN THE USER'S REQUEST DOESN'T MATCH ANY FEATURE:
|
|
547
|
-
- If it partially matches something (e.g. they said "create template" and there's a
|
|
548
|
-
"template-gallery" feature): guide them to the closest matching feature and explain
|
|
549
|
-
what it does. Don't say you can't help.
|
|
550
|
-
- If it matches a feature on another page (ghost): include that feature's steps normally.
|
|
551
|
-
The SDK will navigate there automatically. Do NOT tell the user to navigate manually.
|
|
552
|
-
- If there is genuinely no match anywhere in the feature map: set steps to [] and say
|
|
553
|
-
something like "That doesn't seem to be a feature in ${config.appName} yet. Here's
|
|
554
|
-
what I can help you with:" then list 2-3 relevant features from the map by name.
|
|
555
|
-
Never say you "can only guide through available features" — always offer alternatives.
|
|
601
|
+
8. Never skip features in a required sequence. Include every step end-to-end.
|
|
556
602
|
`.trim();
|
|
557
603
|
}
|
|
558
604
|
|
|
@@ -1023,6 +1069,7 @@ function expandFlowSteps(aiStep, feature) {
|
|
|
1023
1069
|
|
|
1024
1070
|
// Loads Shepherd.js, builds + runs tours, wires up advanceOn listeners,
|
|
1025
1071
|
// progress indicators, pause/resume, and step-level error display.
|
|
1072
|
+
// ENHANCED: Adds animated ring light effect to highlighted elements.
|
|
1026
1073
|
|
|
1027
1074
|
const SHEPHERD_JS = 'https://cdn.jsdelivr.net/npm/shepherd.js@11.2.0/dist/js/shepherd.min.js';
|
|
1028
1075
|
|
|
@@ -1043,6 +1090,61 @@ async function ensureShepherd() {
|
|
|
1043
1090
|
await loadScript(SHEPHERD_JS);
|
|
1044
1091
|
}
|
|
1045
1092
|
|
|
1093
|
+
// ─── Highlight management ────────────────────────────────────────────────────
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* Applies the ring light animation to an element.
|
|
1097
|
+
*
|
|
1098
|
+
* @param {HTMLElement} el
|
|
1099
|
+
* @param {string} [style='default'] - 'default' | 'hard' | 'subtle'
|
|
1100
|
+
*/
|
|
1101
|
+
function applyHighlight(el, style = 'default') {
|
|
1102
|
+
if (!el) return;
|
|
1103
|
+
|
|
1104
|
+
// Set CSS variables for accent color parsing in RGB format
|
|
1105
|
+
const accentColor = getComputedStyle(el).getPropertyValue('--sai-accent') || '#e94560';
|
|
1106
|
+
const rgb = hexToRgb(accentColor.trim());
|
|
1107
|
+
if (rgb) {
|
|
1108
|
+
el.style.setProperty('--sai-accent-rgb', `${rgb.r}, ${rgb.g}, ${rgb.b}`);
|
|
1109
|
+
}
|
|
1110
|
+
el.classList.add('sai-highlighted');
|
|
1111
|
+
if (style === 'hard') {
|
|
1112
|
+
el.classList.add('sai-highlight-hard');
|
|
1113
|
+
} else if (style === 'subtle') {
|
|
1114
|
+
el.classList.add('sai-highlight-subtle');
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Scroll into view with extra padding
|
|
1118
|
+
if (el.scrollIntoView) {
|
|
1119
|
+
el.scrollIntoView({
|
|
1120
|
+
behavior: 'smooth',
|
|
1121
|
+
block: 'center'
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Removes the ring light animation from an element.
|
|
1128
|
+
*/
|
|
1129
|
+
function removeHighlight(el) {
|
|
1130
|
+
if (!el) return;
|
|
1131
|
+
el.classList.remove('sai-highlighted', 'sai-highlight-hard', 'sai-highlight-subtle');
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* Converts hex color to RGB object.
|
|
1136
|
+
* @param {string} hex
|
|
1137
|
+
* @returns {object|null}
|
|
1138
|
+
*/
|
|
1139
|
+
function hexToRgb(hex) {
|
|
1140
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
1141
|
+
return result ? {
|
|
1142
|
+
r: parseInt(result[1], 16),
|
|
1143
|
+
g: parseInt(result[2], 16),
|
|
1144
|
+
b: parseInt(result[3], 16)
|
|
1145
|
+
} : null;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1046
1148
|
// ─── Feature-map merge ────────────────────────────────────────────────────────
|
|
1047
1149
|
|
|
1048
1150
|
/**
|
|
@@ -1076,6 +1178,7 @@ function mergeWithFeature(step) {
|
|
|
1076
1178
|
* 1. Cross-page navigation (route prop) with automatic waiting
|
|
1077
1179
|
* 2. Legacy screen navigation (screen.navigate)
|
|
1078
1180
|
* 3. Element waiting (waitFor prop)
|
|
1181
|
+
* 4. Ring light animation application
|
|
1079
1182
|
*
|
|
1080
1183
|
* @param {object} step
|
|
1081
1184
|
* @param {number} waitTimeout
|
|
@@ -1099,6 +1202,10 @@ function makeBeforeShowPromise(step, waitTimeout) {
|
|
|
1099
1202
|
});
|
|
1100
1203
|
} catch (_) {/* non-fatal */}
|
|
1101
1204
|
await waitForElement(postNavMerge.selector, waitTimeout);
|
|
1205
|
+
|
|
1206
|
+
// Apply ring light animation to the element
|
|
1207
|
+
const el = document.querySelector(postNavMerge.selector);
|
|
1208
|
+
if (el) applyHighlight(el, step._highlightStyle || 'default');
|
|
1102
1209
|
return;
|
|
1103
1210
|
}
|
|
1104
1211
|
}
|
|
@@ -1108,6 +1215,12 @@ function makeBeforeShowPromise(step, waitTimeout) {
|
|
|
1108
1215
|
if (freshMerged.waitFor) {
|
|
1109
1216
|
await waitForElement(freshMerged.waitFor, waitTimeout);
|
|
1110
1217
|
}
|
|
1218
|
+
|
|
1219
|
+
// Apply ring light animation to the main selector
|
|
1220
|
+
if (freshMerged.selector) {
|
|
1221
|
+
const el = document.querySelector(freshMerged.selector);
|
|
1222
|
+
if (el) applyHighlight(el, step._highlightStyle || 'default');
|
|
1223
|
+
}
|
|
1111
1224
|
})();
|
|
1112
1225
|
}
|
|
1113
1226
|
|
|
@@ -1156,11 +1269,13 @@ function addProgressIndicator(shepherdStep, index, total) {
|
|
|
1156
1269
|
|
|
1157
1270
|
/**
|
|
1158
1271
|
* Starts a Shepherd tour from the given steps array.
|
|
1272
|
+
* ENHANCED: Applies ring light animations to highlighted elements.
|
|
1159
1273
|
*
|
|
1160
1274
|
* @param {Array} steps
|
|
1161
1275
|
* @param {object} [options]
|
|
1162
1276
|
* @param {boolean} [options.showProgress=true]
|
|
1163
1277
|
* @param {number} [options.waitTimeout=8000]
|
|
1278
|
+
* @param {string} [options.highlightStyle='default'] - 'default' | 'hard' | 'subtle'
|
|
1164
1279
|
*/
|
|
1165
1280
|
async function runTour(steps, options = {}) {
|
|
1166
1281
|
var _state$config2;
|
|
@@ -1173,9 +1288,13 @@ async function runTour(steps, options = {}) {
|
|
|
1173
1288
|
if (!(steps !== null && steps !== void 0 && steps.length)) return;
|
|
1174
1289
|
const {
|
|
1175
1290
|
showProgress = true,
|
|
1176
|
-
waitTimeout = 8000
|
|
1291
|
+
waitTimeout = 8000,
|
|
1292
|
+
highlightStyle = 'default'
|
|
1177
1293
|
} = options;
|
|
1178
|
-
const mergedSteps = steps.map(
|
|
1294
|
+
const mergedSteps = steps.map(step => ({
|
|
1295
|
+
...mergeWithFeature(step),
|
|
1296
|
+
_highlightStyle: highlightStyle
|
|
1297
|
+
}));
|
|
1179
1298
|
|
|
1180
1299
|
// Legacy: navigate to the correct screen for the first step
|
|
1181
1300
|
const firstFeature = (_state$config2 = config) === null || _state$config2 === void 0 || (_state$config2 = _state$config2.features) === null || _state$config2 === void 0 ? void 0 : _state$config2.find(f => {
|
|
@@ -1241,12 +1360,24 @@ async function runTour(steps, options = {}) {
|
|
|
1241
1360
|
});
|
|
1242
1361
|
step._shepherdRef = shepherdStep;
|
|
1243
1362
|
shepherdStep._isLast = isLast;
|
|
1363
|
+
|
|
1364
|
+
// Clean up highlight on hide
|
|
1365
|
+
shepherdStep.on('hide', () => {
|
|
1366
|
+
if (step.selector) {
|
|
1367
|
+
const el = document.querySelector(step.selector);
|
|
1368
|
+
if (el) removeHighlight(el);
|
|
1369
|
+
}
|
|
1370
|
+
});
|
|
1244
1371
|
if (hasAuto) wireAdvanceOn(shepherdStep, step.advanceOn, tour$1);
|
|
1245
1372
|
if (showProgress && expandedSteps.length > 1) {
|
|
1246
1373
|
addProgressIndicator(shepherdStep, i, expandedSteps.length);
|
|
1247
1374
|
}
|
|
1248
1375
|
});
|
|
1249
1376
|
tour$1.on('complete', () => {
|
|
1377
|
+
// Clean up all highlights
|
|
1378
|
+
document.querySelectorAll('.sai-highlighted').forEach(el => {
|
|
1379
|
+
removeHighlight(el);
|
|
1380
|
+
});
|
|
1250
1381
|
runAndClearCleanups();
|
|
1251
1382
|
setPausedSteps(null);
|
|
1252
1383
|
setPausedIndex(0);
|
|
@@ -1257,6 +1388,11 @@ async function runTour(steps, options = {}) {
|
|
|
1257
1388
|
tour$1.on('cancel', () => {
|
|
1258
1389
|
const currentStepEl = tour$1.getCurrentStep();
|
|
1259
1390
|
const currentIdx = currentStepEl ? expandedSteps.findIndex(s => s.id === currentStepEl.id) : 0;
|
|
1391
|
+
|
|
1392
|
+
// Clean up all highlights
|
|
1393
|
+
document.querySelectorAll('.sai-highlighted').forEach(el => {
|
|
1394
|
+
removeHighlight(el);
|
|
1395
|
+
});
|
|
1260
1396
|
runAndClearCleanups();
|
|
1261
1397
|
setPausedSteps(expandedSteps);
|
|
1262
1398
|
setPausedIndex(Math.max(0, currentIdx));
|
package/dist/react/core.cjs
CHANGED
|
@@ -221,6 +221,71 @@ function injectStyles(theme, pos) {
|
|
|
221
221
|
style.textContent = `
|
|
222
222
|
#sai-trigger, #sai-panel { ${buildCSSVars(theme)} }
|
|
223
223
|
|
|
224
|
+
/* ── Ring Light Animation ── */
|
|
225
|
+
@keyframes sai-ring-pulse {
|
|
226
|
+
0% {
|
|
227
|
+
box-shadow:
|
|
228
|
+
0 0 0 0 rgba(var(--sai-accent-rgb), 0.7),
|
|
229
|
+
0 0 0 0 rgba(var(--sai-accent-rgb), 0.5),
|
|
230
|
+
0 0 0 0 rgba(var(--sai-accent-rgb), 0.3);
|
|
231
|
+
}
|
|
232
|
+
50% {
|
|
233
|
+
box-shadow:
|
|
234
|
+
0 0 0 8px rgba(var(--sai-accent-rgb), 0),
|
|
235
|
+
0 0 0 16px rgba(var(--sai-accent-rgb), 0.3),
|
|
236
|
+
0 0 0 24px rgba(var(--sai-accent-rgb), 0);
|
|
237
|
+
}
|
|
238
|
+
100% {
|
|
239
|
+
box-shadow:
|
|
240
|
+
0 0 0 16px rgba(var(--sai-accent-rgb), 0),
|
|
241
|
+
0 0 0 32px rgba(var(--sai-accent-rgb), 0),
|
|
242
|
+
0 0 0 48px rgba(var(--sai-accent-rgb), 0);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@keyframes sai-ring-pulse-hard {
|
|
247
|
+
0% {
|
|
248
|
+
box-shadow:
|
|
249
|
+
inset 0 0 0 2px var(--sai-accent),
|
|
250
|
+
0 0 0 3px var(--sai-accent),
|
|
251
|
+
0 0 20px var(--sai-accent);
|
|
252
|
+
}
|
|
253
|
+
50% {
|
|
254
|
+
box-shadow:
|
|
255
|
+
inset 0 0 0 2px var(--sai-accent),
|
|
256
|
+
0 0 0 8px var(--sai-accent),
|
|
257
|
+
0 0 40px var(--sai-accent);
|
|
258
|
+
}
|
|
259
|
+
100% {
|
|
260
|
+
box-shadow:
|
|
261
|
+
inset 0 0 0 2px var(--sai-accent),
|
|
262
|
+
0 0 0 3px var(--sai-accent),
|
|
263
|
+
0 0 20px var(--sai-accent);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@keyframes sai-element-highlight {
|
|
268
|
+
0% { filter: brightness(1); }
|
|
269
|
+
50% { filter: brightness(1.15); }
|
|
270
|
+
100% { filter: brightness(1); }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* Highlighted element styles */
|
|
274
|
+
.sai-highlighted {
|
|
275
|
+
position: relative !important;
|
|
276
|
+
animation: sai-ring-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
|
|
277
|
+
z-index: 99997 !important;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.sai-highlighted.sai-highlight-hard {
|
|
281
|
+
animation: sai-ring-pulse-hard 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.sai-highlighted.sai-highlight-subtle {
|
|
285
|
+
animation: sai-element-highlight 2s ease-in-out infinite !important;
|
|
286
|
+
filter: drop-shadow(0 0 8px var(--sai-accent)) !important;
|
|
287
|
+
}
|
|
288
|
+
|
|
224
289
|
/* ── Trigger ── */
|
|
225
290
|
#sai-trigger {
|
|
226
291
|
position: fixed; ${triggerCSS};
|
|
@@ -486,13 +551,7 @@ function buildSystemPrompt(config) {
|
|
|
486
551
|
var _f$screen;
|
|
487
552
|
return (_f$screen = f.screen) === null || _f$screen === void 0 ? void 0 : _f$screen.id;
|
|
488
553
|
}).map(f => f.screen.id))];
|
|
489
|
-
|
|
490
|
-
// Split features into live (current page) and ghost (other pages).
|
|
491
|
-
// This lets the AI understand what's immediately available vs reachable
|
|
492
|
-
// via navigation, and craft the right message when something isn't found.
|
|
493
|
-
const liveFeatures = (config.features || []).filter(f => !f._ghost);
|
|
494
|
-
const ghostFeatures = (config.features || []).filter(f => f._ghost);
|
|
495
|
-
const summarise = f => {
|
|
554
|
+
const featureSummary = (config.features || []).map(f => {
|
|
496
555
|
var _f$screen2, _f$flow;
|
|
497
556
|
const entry = {
|
|
498
557
|
id: f.id,
|
|
@@ -507,18 +566,15 @@ function buildSystemPrompt(config) {
|
|
|
507
566
|
entry.note = `This feature has ${f.flow.length} sequential sub-steps. Include ONE step per flow entry.`;
|
|
508
567
|
}
|
|
509
568
|
return entry;
|
|
510
|
-
};
|
|
511
|
-
const liveSection = liveFeatures.length ? `CURRENT PAGE FEATURES (user is here now):\n${JSON.stringify(liveFeatures.map(summarise), null, 2)}` : `CURRENT PAGE FEATURES: none registered yet.`;
|
|
512
|
-
const ghostSection = ghostFeatures.length ? `OTHER PAGE FEATURES (reachable via navigation — SDK handles it automatically):\n${JSON.stringify(ghostFeatures.map(summarise), null, 2)}` : '';
|
|
569
|
+
});
|
|
513
570
|
return `
|
|
514
571
|
You are an in-app assistant called "${config.assistantName || 'AI Guide'}" for "${config.appName}".
|
|
515
572
|
Your ONLY job: guide users step-by-step through tasks using the feature map below.
|
|
516
573
|
|
|
517
|
-
${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. The SDK
|
|
518
|
-
|
|
519
|
-
${liveSection}
|
|
574
|
+
${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. Features are screen-specific. The SDK handles navigation — just pick the right features.` : ''}
|
|
520
575
|
|
|
521
|
-
|
|
576
|
+
FEATURE MAP (only reference IDs from this list):
|
|
577
|
+
${JSON.stringify(featureSummary, null, 2)}
|
|
522
578
|
|
|
523
579
|
RESPOND ONLY with this exact JSON — no markdown, no extra text:
|
|
524
580
|
{
|
|
@@ -535,26 +591,16 @@ RESPOND ONLY with this exact JSON — no markdown, no extra text:
|
|
|
535
591
|
}
|
|
536
592
|
|
|
537
593
|
RULES:
|
|
538
|
-
1. The step "id" MUST match a feature id from the
|
|
594
|
+
1. The step "id" MUST match a feature id from the feature map.
|
|
539
595
|
2. Only use selectors and IDs from the feature map. Never invent them.
|
|
540
|
-
3.
|
|
541
|
-
4.
|
|
542
|
-
5.
|
|
596
|
+
3. No matching feature → steps: [], explain kindly in message.
|
|
597
|
+
4. position values: top | bottom | left | right | auto only.
|
|
598
|
+
5. Order steps logically. For multi-step flows, order as the user encounters them.
|
|
599
|
+
6. For forms: ALWAYS include a step for the form section or first input BEFORE the
|
|
543
600
|
continue/submit button. The button step must always be LAST in its section.
|
|
544
|
-
|
|
601
|
+
7. If a feature has a flow, include one step per flow entry using the same feature id —
|
|
545
602
|
the SDK expands them automatically.
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
WHEN THE USER'S REQUEST DOESN'T MATCH ANY FEATURE:
|
|
549
|
-
- If it partially matches something (e.g. they said "create template" and there's a
|
|
550
|
-
"template-gallery" feature): guide them to the closest matching feature and explain
|
|
551
|
-
what it does. Don't say you can't help.
|
|
552
|
-
- If it matches a feature on another page (ghost): include that feature's steps normally.
|
|
553
|
-
The SDK will navigate there automatically. Do NOT tell the user to navigate manually.
|
|
554
|
-
- If there is genuinely no match anywhere in the feature map: set steps to [] and say
|
|
555
|
-
something like "That doesn't seem to be a feature in ${config.appName} yet. Here's
|
|
556
|
-
what I can help you with:" then list 2-3 relevant features from the map by name.
|
|
557
|
-
Never say you "can only guide through available features" — always offer alternatives.
|
|
603
|
+
8. Never skip features in a required sequence. Include every step end-to-end.
|
|
558
604
|
`.trim();
|
|
559
605
|
}
|
|
560
606
|
|
|
@@ -1025,6 +1071,7 @@ function expandFlowSteps(aiStep, feature) {
|
|
|
1025
1071
|
|
|
1026
1072
|
// Loads Shepherd.js, builds + runs tours, wires up advanceOn listeners,
|
|
1027
1073
|
// progress indicators, pause/resume, and step-level error display.
|
|
1074
|
+
// ENHANCED: Adds animated ring light effect to highlighted elements.
|
|
1028
1075
|
|
|
1029
1076
|
const SHEPHERD_JS = 'https://cdn.jsdelivr.net/npm/shepherd.js@11.2.0/dist/js/shepherd.min.js';
|
|
1030
1077
|
|
|
@@ -1045,6 +1092,61 @@ async function ensureShepherd() {
|
|
|
1045
1092
|
await loadScript(SHEPHERD_JS);
|
|
1046
1093
|
}
|
|
1047
1094
|
|
|
1095
|
+
// ─── Highlight management ────────────────────────────────────────────────────
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Applies the ring light animation to an element.
|
|
1099
|
+
*
|
|
1100
|
+
* @param {HTMLElement} el
|
|
1101
|
+
* @param {string} [style='default'] - 'default' | 'hard' | 'subtle'
|
|
1102
|
+
*/
|
|
1103
|
+
function applyHighlight(el, style = 'default') {
|
|
1104
|
+
if (!el) return;
|
|
1105
|
+
|
|
1106
|
+
// Set CSS variables for accent color parsing in RGB format
|
|
1107
|
+
const accentColor = getComputedStyle(el).getPropertyValue('--sai-accent') || '#e94560';
|
|
1108
|
+
const rgb = hexToRgb(accentColor.trim());
|
|
1109
|
+
if (rgb) {
|
|
1110
|
+
el.style.setProperty('--sai-accent-rgb', `${rgb.r}, ${rgb.g}, ${rgb.b}`);
|
|
1111
|
+
}
|
|
1112
|
+
el.classList.add('sai-highlighted');
|
|
1113
|
+
if (style === 'hard') {
|
|
1114
|
+
el.classList.add('sai-highlight-hard');
|
|
1115
|
+
} else if (style === 'subtle') {
|
|
1116
|
+
el.classList.add('sai-highlight-subtle');
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Scroll into view with extra padding
|
|
1120
|
+
if (el.scrollIntoView) {
|
|
1121
|
+
el.scrollIntoView({
|
|
1122
|
+
behavior: 'smooth',
|
|
1123
|
+
block: 'center'
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Removes the ring light animation from an element.
|
|
1130
|
+
*/
|
|
1131
|
+
function removeHighlight(el) {
|
|
1132
|
+
if (!el) return;
|
|
1133
|
+
el.classList.remove('sai-highlighted', 'sai-highlight-hard', 'sai-highlight-subtle');
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Converts hex color to RGB object.
|
|
1138
|
+
* @param {string} hex
|
|
1139
|
+
* @returns {object|null}
|
|
1140
|
+
*/
|
|
1141
|
+
function hexToRgb(hex) {
|
|
1142
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
1143
|
+
return result ? {
|
|
1144
|
+
r: parseInt(result[1], 16),
|
|
1145
|
+
g: parseInt(result[2], 16),
|
|
1146
|
+
b: parseInt(result[3], 16)
|
|
1147
|
+
} : null;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1048
1150
|
// ─── Feature-map merge ────────────────────────────────────────────────────────
|
|
1049
1151
|
|
|
1050
1152
|
/**
|
|
@@ -1078,6 +1180,7 @@ function mergeWithFeature(step) {
|
|
|
1078
1180
|
* 1. Cross-page navigation (route prop) with automatic waiting
|
|
1079
1181
|
* 2. Legacy screen navigation (screen.navigate)
|
|
1080
1182
|
* 3. Element waiting (waitFor prop)
|
|
1183
|
+
* 4. Ring light animation application
|
|
1081
1184
|
*
|
|
1082
1185
|
* @param {object} step
|
|
1083
1186
|
* @param {number} waitTimeout
|
|
@@ -1101,6 +1204,10 @@ function makeBeforeShowPromise(step, waitTimeout) {
|
|
|
1101
1204
|
});
|
|
1102
1205
|
} catch (_) {/* non-fatal */}
|
|
1103
1206
|
await waitForElement(postNavMerge.selector, waitTimeout);
|
|
1207
|
+
|
|
1208
|
+
// Apply ring light animation to the element
|
|
1209
|
+
const el = document.querySelector(postNavMerge.selector);
|
|
1210
|
+
if (el) applyHighlight(el, step._highlightStyle || 'default');
|
|
1104
1211
|
return;
|
|
1105
1212
|
}
|
|
1106
1213
|
}
|
|
@@ -1110,6 +1217,12 @@ function makeBeforeShowPromise(step, waitTimeout) {
|
|
|
1110
1217
|
if (freshMerged.waitFor) {
|
|
1111
1218
|
await waitForElement(freshMerged.waitFor, waitTimeout);
|
|
1112
1219
|
}
|
|
1220
|
+
|
|
1221
|
+
// Apply ring light animation to the main selector
|
|
1222
|
+
if (freshMerged.selector) {
|
|
1223
|
+
const el = document.querySelector(freshMerged.selector);
|
|
1224
|
+
if (el) applyHighlight(el, step._highlightStyle || 'default');
|
|
1225
|
+
}
|
|
1113
1226
|
})();
|
|
1114
1227
|
}
|
|
1115
1228
|
|
|
@@ -1158,11 +1271,13 @@ function addProgressIndicator(shepherdStep, index, total) {
|
|
|
1158
1271
|
|
|
1159
1272
|
/**
|
|
1160
1273
|
* Starts a Shepherd tour from the given steps array.
|
|
1274
|
+
* ENHANCED: Applies ring light animations to highlighted elements.
|
|
1161
1275
|
*
|
|
1162
1276
|
* @param {Array} steps
|
|
1163
1277
|
* @param {object} [options]
|
|
1164
1278
|
* @param {boolean} [options.showProgress=true]
|
|
1165
1279
|
* @param {number} [options.waitTimeout=8000]
|
|
1280
|
+
* @param {string} [options.highlightStyle='default'] - 'default' | 'hard' | 'subtle'
|
|
1166
1281
|
*/
|
|
1167
1282
|
async function runTour(steps, options = {}) {
|
|
1168
1283
|
var _state$config2;
|
|
@@ -1175,9 +1290,13 @@ async function runTour(steps, options = {}) {
|
|
|
1175
1290
|
if (!(steps !== null && steps !== void 0 && steps.length)) return;
|
|
1176
1291
|
const {
|
|
1177
1292
|
showProgress = true,
|
|
1178
|
-
waitTimeout = 8000
|
|
1293
|
+
waitTimeout = 8000,
|
|
1294
|
+
highlightStyle = 'default'
|
|
1179
1295
|
} = options;
|
|
1180
|
-
const mergedSteps = steps.map(
|
|
1296
|
+
const mergedSteps = steps.map(step => ({
|
|
1297
|
+
...mergeWithFeature(step),
|
|
1298
|
+
_highlightStyle: highlightStyle
|
|
1299
|
+
}));
|
|
1181
1300
|
|
|
1182
1301
|
// Legacy: navigate to the correct screen for the first step
|
|
1183
1302
|
const firstFeature = (_state$config2 = config) === null || _state$config2 === void 0 || (_state$config2 = _state$config2.features) === null || _state$config2 === void 0 ? void 0 : _state$config2.find(f => {
|
|
@@ -1243,12 +1362,24 @@ async function runTour(steps, options = {}) {
|
|
|
1243
1362
|
});
|
|
1244
1363
|
step._shepherdRef = shepherdStep;
|
|
1245
1364
|
shepherdStep._isLast = isLast;
|
|
1365
|
+
|
|
1366
|
+
// Clean up highlight on hide
|
|
1367
|
+
shepherdStep.on('hide', () => {
|
|
1368
|
+
if (step.selector) {
|
|
1369
|
+
const el = document.querySelector(step.selector);
|
|
1370
|
+
if (el) removeHighlight(el);
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1246
1373
|
if (hasAuto) wireAdvanceOn(shepherdStep, step.advanceOn, tour$1);
|
|
1247
1374
|
if (showProgress && expandedSteps.length > 1) {
|
|
1248
1375
|
addProgressIndicator(shepherdStep, i, expandedSteps.length);
|
|
1249
1376
|
}
|
|
1250
1377
|
});
|
|
1251
1378
|
tour$1.on('complete', () => {
|
|
1379
|
+
// Clean up all highlights
|
|
1380
|
+
document.querySelectorAll('.sai-highlighted').forEach(el => {
|
|
1381
|
+
removeHighlight(el);
|
|
1382
|
+
});
|
|
1252
1383
|
runAndClearCleanups();
|
|
1253
1384
|
setPausedSteps(null);
|
|
1254
1385
|
setPausedIndex(0);
|
|
@@ -1259,6 +1390,11 @@ async function runTour(steps, options = {}) {
|
|
|
1259
1390
|
tour$1.on('cancel', () => {
|
|
1260
1391
|
const currentStepEl = tour$1.getCurrentStep();
|
|
1261
1392
|
const currentIdx = currentStepEl ? expandedSteps.findIndex(s => s.id === currentStepEl.id) : 0;
|
|
1393
|
+
|
|
1394
|
+
// Clean up all highlights
|
|
1395
|
+
document.querySelectorAll('.sai-highlighted').forEach(el => {
|
|
1396
|
+
removeHighlight(el);
|
|
1397
|
+
});
|
|
1262
1398
|
runAndClearCleanups();
|
|
1263
1399
|
setPausedSteps(expandedSteps);
|
|
1264
1400
|
setPausedIndex(Math.max(0, currentIdx));
|
package/dist/react/core.js
CHANGED
|
@@ -219,6 +219,71 @@ function injectStyles(theme, pos) {
|
|
|
219
219
|
style.textContent = `
|
|
220
220
|
#sai-trigger, #sai-panel { ${buildCSSVars(theme)} }
|
|
221
221
|
|
|
222
|
+
/* ── Ring Light Animation ── */
|
|
223
|
+
@keyframes sai-ring-pulse {
|
|
224
|
+
0% {
|
|
225
|
+
box-shadow:
|
|
226
|
+
0 0 0 0 rgba(var(--sai-accent-rgb), 0.7),
|
|
227
|
+
0 0 0 0 rgba(var(--sai-accent-rgb), 0.5),
|
|
228
|
+
0 0 0 0 rgba(var(--sai-accent-rgb), 0.3);
|
|
229
|
+
}
|
|
230
|
+
50% {
|
|
231
|
+
box-shadow:
|
|
232
|
+
0 0 0 8px rgba(var(--sai-accent-rgb), 0),
|
|
233
|
+
0 0 0 16px rgba(var(--sai-accent-rgb), 0.3),
|
|
234
|
+
0 0 0 24px rgba(var(--sai-accent-rgb), 0);
|
|
235
|
+
}
|
|
236
|
+
100% {
|
|
237
|
+
box-shadow:
|
|
238
|
+
0 0 0 16px rgba(var(--sai-accent-rgb), 0),
|
|
239
|
+
0 0 0 32px rgba(var(--sai-accent-rgb), 0),
|
|
240
|
+
0 0 0 48px rgba(var(--sai-accent-rgb), 0);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@keyframes sai-ring-pulse-hard {
|
|
245
|
+
0% {
|
|
246
|
+
box-shadow:
|
|
247
|
+
inset 0 0 0 2px var(--sai-accent),
|
|
248
|
+
0 0 0 3px var(--sai-accent),
|
|
249
|
+
0 0 20px var(--sai-accent);
|
|
250
|
+
}
|
|
251
|
+
50% {
|
|
252
|
+
box-shadow:
|
|
253
|
+
inset 0 0 0 2px var(--sai-accent),
|
|
254
|
+
0 0 0 8px var(--sai-accent),
|
|
255
|
+
0 0 40px var(--sai-accent);
|
|
256
|
+
}
|
|
257
|
+
100% {
|
|
258
|
+
box-shadow:
|
|
259
|
+
inset 0 0 0 2px var(--sai-accent),
|
|
260
|
+
0 0 0 3px var(--sai-accent),
|
|
261
|
+
0 0 20px var(--sai-accent);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@keyframes sai-element-highlight {
|
|
266
|
+
0% { filter: brightness(1); }
|
|
267
|
+
50% { filter: brightness(1.15); }
|
|
268
|
+
100% { filter: brightness(1); }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* Highlighted element styles */
|
|
272
|
+
.sai-highlighted {
|
|
273
|
+
position: relative !important;
|
|
274
|
+
animation: sai-ring-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
|
|
275
|
+
z-index: 99997 !important;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.sai-highlighted.sai-highlight-hard {
|
|
279
|
+
animation: sai-ring-pulse-hard 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.sai-highlighted.sai-highlight-subtle {
|
|
283
|
+
animation: sai-element-highlight 2s ease-in-out infinite !important;
|
|
284
|
+
filter: drop-shadow(0 0 8px var(--sai-accent)) !important;
|
|
285
|
+
}
|
|
286
|
+
|
|
222
287
|
/* ── Trigger ── */
|
|
223
288
|
#sai-trigger {
|
|
224
289
|
position: fixed; ${triggerCSS};
|
|
@@ -484,13 +549,7 @@ function buildSystemPrompt(config) {
|
|
|
484
549
|
var _f$screen;
|
|
485
550
|
return (_f$screen = f.screen) === null || _f$screen === void 0 ? void 0 : _f$screen.id;
|
|
486
551
|
}).map(f => f.screen.id))];
|
|
487
|
-
|
|
488
|
-
// Split features into live (current page) and ghost (other pages).
|
|
489
|
-
// This lets the AI understand what's immediately available vs reachable
|
|
490
|
-
// via navigation, and craft the right message when something isn't found.
|
|
491
|
-
const liveFeatures = (config.features || []).filter(f => !f._ghost);
|
|
492
|
-
const ghostFeatures = (config.features || []).filter(f => f._ghost);
|
|
493
|
-
const summarise = f => {
|
|
552
|
+
const featureSummary = (config.features || []).map(f => {
|
|
494
553
|
var _f$screen2, _f$flow;
|
|
495
554
|
const entry = {
|
|
496
555
|
id: f.id,
|
|
@@ -505,18 +564,15 @@ function buildSystemPrompt(config) {
|
|
|
505
564
|
entry.note = `This feature has ${f.flow.length} sequential sub-steps. Include ONE step per flow entry.`;
|
|
506
565
|
}
|
|
507
566
|
return entry;
|
|
508
|
-
};
|
|
509
|
-
const liveSection = liveFeatures.length ? `CURRENT PAGE FEATURES (user is here now):\n${JSON.stringify(liveFeatures.map(summarise), null, 2)}` : `CURRENT PAGE FEATURES: none registered yet.`;
|
|
510
|
-
const ghostSection = ghostFeatures.length ? `OTHER PAGE FEATURES (reachable via navigation — SDK handles it automatically):\n${JSON.stringify(ghostFeatures.map(summarise), null, 2)}` : '';
|
|
567
|
+
});
|
|
511
568
|
return `
|
|
512
569
|
You are an in-app assistant called "${config.assistantName || 'AI Guide'}" for "${config.appName}".
|
|
513
570
|
Your ONLY job: guide users step-by-step through tasks using the feature map below.
|
|
514
571
|
|
|
515
|
-
${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. The SDK
|
|
516
|
-
|
|
517
|
-
${liveSection}
|
|
572
|
+
${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. Features are screen-specific. The SDK handles navigation — just pick the right features.` : ''}
|
|
518
573
|
|
|
519
|
-
|
|
574
|
+
FEATURE MAP (only reference IDs from this list):
|
|
575
|
+
${JSON.stringify(featureSummary, null, 2)}
|
|
520
576
|
|
|
521
577
|
RESPOND ONLY with this exact JSON — no markdown, no extra text:
|
|
522
578
|
{
|
|
@@ -533,26 +589,16 @@ RESPOND ONLY with this exact JSON — no markdown, no extra text:
|
|
|
533
589
|
}
|
|
534
590
|
|
|
535
591
|
RULES:
|
|
536
|
-
1. The step "id" MUST match a feature id from the
|
|
592
|
+
1. The step "id" MUST match a feature id from the feature map.
|
|
537
593
|
2. Only use selectors and IDs from the feature map. Never invent them.
|
|
538
|
-
3.
|
|
539
|
-
4.
|
|
540
|
-
5.
|
|
594
|
+
3. No matching feature → steps: [], explain kindly in message.
|
|
595
|
+
4. position values: top | bottom | left | right | auto only.
|
|
596
|
+
5. Order steps logically. For multi-step flows, order as the user encounters them.
|
|
597
|
+
6. For forms: ALWAYS include a step for the form section or first input BEFORE the
|
|
541
598
|
continue/submit button. The button step must always be LAST in its section.
|
|
542
|
-
|
|
599
|
+
7. If a feature has a flow, include one step per flow entry using the same feature id —
|
|
543
600
|
the SDK expands them automatically.
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
WHEN THE USER'S REQUEST DOESN'T MATCH ANY FEATURE:
|
|
547
|
-
- If it partially matches something (e.g. they said "create template" and there's a
|
|
548
|
-
"template-gallery" feature): guide them to the closest matching feature and explain
|
|
549
|
-
what it does. Don't say you can't help.
|
|
550
|
-
- If it matches a feature on another page (ghost): include that feature's steps normally.
|
|
551
|
-
The SDK will navigate there automatically. Do NOT tell the user to navigate manually.
|
|
552
|
-
- If there is genuinely no match anywhere in the feature map: set steps to [] and say
|
|
553
|
-
something like "That doesn't seem to be a feature in ${config.appName} yet. Here's
|
|
554
|
-
what I can help you with:" then list 2-3 relevant features from the map by name.
|
|
555
|
-
Never say you "can only guide through available features" — always offer alternatives.
|
|
601
|
+
8. Never skip features in a required sequence. Include every step end-to-end.
|
|
556
602
|
`.trim();
|
|
557
603
|
}
|
|
558
604
|
|
|
@@ -1023,6 +1069,7 @@ function expandFlowSteps(aiStep, feature) {
|
|
|
1023
1069
|
|
|
1024
1070
|
// Loads Shepherd.js, builds + runs tours, wires up advanceOn listeners,
|
|
1025
1071
|
// progress indicators, pause/resume, and step-level error display.
|
|
1072
|
+
// ENHANCED: Adds animated ring light effect to highlighted elements.
|
|
1026
1073
|
|
|
1027
1074
|
const SHEPHERD_JS = 'https://cdn.jsdelivr.net/npm/shepherd.js@11.2.0/dist/js/shepherd.min.js';
|
|
1028
1075
|
|
|
@@ -1043,6 +1090,61 @@ async function ensureShepherd() {
|
|
|
1043
1090
|
await loadScript(SHEPHERD_JS);
|
|
1044
1091
|
}
|
|
1045
1092
|
|
|
1093
|
+
// ─── Highlight management ────────────────────────────────────────────────────
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* Applies the ring light animation to an element.
|
|
1097
|
+
*
|
|
1098
|
+
* @param {HTMLElement} el
|
|
1099
|
+
* @param {string} [style='default'] - 'default' | 'hard' | 'subtle'
|
|
1100
|
+
*/
|
|
1101
|
+
function applyHighlight(el, style = 'default') {
|
|
1102
|
+
if (!el) return;
|
|
1103
|
+
|
|
1104
|
+
// Set CSS variables for accent color parsing in RGB format
|
|
1105
|
+
const accentColor = getComputedStyle(el).getPropertyValue('--sai-accent') || '#e94560';
|
|
1106
|
+
const rgb = hexToRgb(accentColor.trim());
|
|
1107
|
+
if (rgb) {
|
|
1108
|
+
el.style.setProperty('--sai-accent-rgb', `${rgb.r}, ${rgb.g}, ${rgb.b}`);
|
|
1109
|
+
}
|
|
1110
|
+
el.classList.add('sai-highlighted');
|
|
1111
|
+
if (style === 'hard') {
|
|
1112
|
+
el.classList.add('sai-highlight-hard');
|
|
1113
|
+
} else if (style === 'subtle') {
|
|
1114
|
+
el.classList.add('sai-highlight-subtle');
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Scroll into view with extra padding
|
|
1118
|
+
if (el.scrollIntoView) {
|
|
1119
|
+
el.scrollIntoView({
|
|
1120
|
+
behavior: 'smooth',
|
|
1121
|
+
block: 'center'
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Removes the ring light animation from an element.
|
|
1128
|
+
*/
|
|
1129
|
+
function removeHighlight(el) {
|
|
1130
|
+
if (!el) return;
|
|
1131
|
+
el.classList.remove('sai-highlighted', 'sai-highlight-hard', 'sai-highlight-subtle');
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* Converts hex color to RGB object.
|
|
1136
|
+
* @param {string} hex
|
|
1137
|
+
* @returns {object|null}
|
|
1138
|
+
*/
|
|
1139
|
+
function hexToRgb(hex) {
|
|
1140
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
1141
|
+
return result ? {
|
|
1142
|
+
r: parseInt(result[1], 16),
|
|
1143
|
+
g: parseInt(result[2], 16),
|
|
1144
|
+
b: parseInt(result[3], 16)
|
|
1145
|
+
} : null;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1046
1148
|
// ─── Feature-map merge ────────────────────────────────────────────────────────
|
|
1047
1149
|
|
|
1048
1150
|
/**
|
|
@@ -1076,6 +1178,7 @@ function mergeWithFeature(step) {
|
|
|
1076
1178
|
* 1. Cross-page navigation (route prop) with automatic waiting
|
|
1077
1179
|
* 2. Legacy screen navigation (screen.navigate)
|
|
1078
1180
|
* 3. Element waiting (waitFor prop)
|
|
1181
|
+
* 4. Ring light animation application
|
|
1079
1182
|
*
|
|
1080
1183
|
* @param {object} step
|
|
1081
1184
|
* @param {number} waitTimeout
|
|
@@ -1099,6 +1202,10 @@ function makeBeforeShowPromise(step, waitTimeout) {
|
|
|
1099
1202
|
});
|
|
1100
1203
|
} catch (_) {/* non-fatal */}
|
|
1101
1204
|
await waitForElement(postNavMerge.selector, waitTimeout);
|
|
1205
|
+
|
|
1206
|
+
// Apply ring light animation to the element
|
|
1207
|
+
const el = document.querySelector(postNavMerge.selector);
|
|
1208
|
+
if (el) applyHighlight(el, step._highlightStyle || 'default');
|
|
1102
1209
|
return;
|
|
1103
1210
|
}
|
|
1104
1211
|
}
|
|
@@ -1108,6 +1215,12 @@ function makeBeforeShowPromise(step, waitTimeout) {
|
|
|
1108
1215
|
if (freshMerged.waitFor) {
|
|
1109
1216
|
await waitForElement(freshMerged.waitFor, waitTimeout);
|
|
1110
1217
|
}
|
|
1218
|
+
|
|
1219
|
+
// Apply ring light animation to the main selector
|
|
1220
|
+
if (freshMerged.selector) {
|
|
1221
|
+
const el = document.querySelector(freshMerged.selector);
|
|
1222
|
+
if (el) applyHighlight(el, step._highlightStyle || 'default');
|
|
1223
|
+
}
|
|
1111
1224
|
})();
|
|
1112
1225
|
}
|
|
1113
1226
|
|
|
@@ -1156,11 +1269,13 @@ function addProgressIndicator(shepherdStep, index, total) {
|
|
|
1156
1269
|
|
|
1157
1270
|
/**
|
|
1158
1271
|
* Starts a Shepherd tour from the given steps array.
|
|
1272
|
+
* ENHANCED: Applies ring light animations to highlighted elements.
|
|
1159
1273
|
*
|
|
1160
1274
|
* @param {Array} steps
|
|
1161
1275
|
* @param {object} [options]
|
|
1162
1276
|
* @param {boolean} [options.showProgress=true]
|
|
1163
1277
|
* @param {number} [options.waitTimeout=8000]
|
|
1278
|
+
* @param {string} [options.highlightStyle='default'] - 'default' | 'hard' | 'subtle'
|
|
1164
1279
|
*/
|
|
1165
1280
|
async function runTour(steps, options = {}) {
|
|
1166
1281
|
var _state$config2;
|
|
@@ -1173,9 +1288,13 @@ async function runTour(steps, options = {}) {
|
|
|
1173
1288
|
if (!(steps !== null && steps !== void 0 && steps.length)) return;
|
|
1174
1289
|
const {
|
|
1175
1290
|
showProgress = true,
|
|
1176
|
-
waitTimeout = 8000
|
|
1291
|
+
waitTimeout = 8000,
|
|
1292
|
+
highlightStyle = 'default'
|
|
1177
1293
|
} = options;
|
|
1178
|
-
const mergedSteps = steps.map(
|
|
1294
|
+
const mergedSteps = steps.map(step => ({
|
|
1295
|
+
...mergeWithFeature(step),
|
|
1296
|
+
_highlightStyle: highlightStyle
|
|
1297
|
+
}));
|
|
1179
1298
|
|
|
1180
1299
|
// Legacy: navigate to the correct screen for the first step
|
|
1181
1300
|
const firstFeature = (_state$config2 = config) === null || _state$config2 === void 0 || (_state$config2 = _state$config2.features) === null || _state$config2 === void 0 ? void 0 : _state$config2.find(f => {
|
|
@@ -1241,12 +1360,24 @@ async function runTour(steps, options = {}) {
|
|
|
1241
1360
|
});
|
|
1242
1361
|
step._shepherdRef = shepherdStep;
|
|
1243
1362
|
shepherdStep._isLast = isLast;
|
|
1363
|
+
|
|
1364
|
+
// Clean up highlight on hide
|
|
1365
|
+
shepherdStep.on('hide', () => {
|
|
1366
|
+
if (step.selector) {
|
|
1367
|
+
const el = document.querySelector(step.selector);
|
|
1368
|
+
if (el) removeHighlight(el);
|
|
1369
|
+
}
|
|
1370
|
+
});
|
|
1244
1371
|
if (hasAuto) wireAdvanceOn(shepherdStep, step.advanceOn, tour$1);
|
|
1245
1372
|
if (showProgress && expandedSteps.length > 1) {
|
|
1246
1373
|
addProgressIndicator(shepherdStep, i, expandedSteps.length);
|
|
1247
1374
|
}
|
|
1248
1375
|
});
|
|
1249
1376
|
tour$1.on('complete', () => {
|
|
1377
|
+
// Clean up all highlights
|
|
1378
|
+
document.querySelectorAll('.sai-highlighted').forEach(el => {
|
|
1379
|
+
removeHighlight(el);
|
|
1380
|
+
});
|
|
1250
1381
|
runAndClearCleanups();
|
|
1251
1382
|
setPausedSteps(null);
|
|
1252
1383
|
setPausedIndex(0);
|
|
@@ -1257,6 +1388,11 @@ async function runTour(steps, options = {}) {
|
|
|
1257
1388
|
tour$1.on('cancel', () => {
|
|
1258
1389
|
const currentStepEl = tour$1.getCurrentStep();
|
|
1259
1390
|
const currentIdx = currentStepEl ? expandedSteps.findIndex(s => s.id === currentStepEl.id) : 0;
|
|
1391
|
+
|
|
1392
|
+
// Clean up all highlights
|
|
1393
|
+
document.querySelectorAll('.sai-highlighted').forEach(el => {
|
|
1394
|
+
removeHighlight(el);
|
|
1395
|
+
});
|
|
1260
1396
|
runAndClearCleanups();
|
|
1261
1397
|
setPausedSteps(expandedSteps);
|
|
1262
1398
|
setPausedIndex(Math.max(0, currentIdx));
|