@eventop/sdk 1.2.12 → 1.2.14

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 CHANGED
@@ -486,7 +486,13 @@ function buildSystemPrompt(config) {
486
486
  var _f$screen;
487
487
  return (_f$screen = f.screen) === null || _f$screen === void 0 ? void 0 : _f$screen.id;
488
488
  }).map(f => f.screen.id))];
489
- const featureSummary = (config.features || []).map(f => {
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 => {
490
496
  var _f$screen2, _f$flow;
491
497
  const entry = {
492
498
  id: f.id,
@@ -501,15 +507,18 @@ function buildSystemPrompt(config) {
501
507
  entry.note = `This feature has ${f.flow.length} sequential sub-steps. Include ONE step per flow entry.`;
502
508
  }
503
509
  return entry;
504
- });
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)}` : '';
505
513
  return `
506
514
  You are an in-app assistant called "${config.assistantName || 'AI Guide'}" for "${config.appName}".
507
515
  Your ONLY job: guide users step-by-step through tasks using the feature map below.
508
516
 
509
- ${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. Features are screen-specific. The SDK handles navigation — just pick the right features.` : ''}
517
+ ${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. The SDK navigates automatically — just pick the right feature id.` : ''}
518
+
519
+ ${liveSection}
510
520
 
511
- FEATURE MAP (only reference IDs from this list):
512
- ${JSON.stringify(featureSummary, null, 2)}
521
+ ${ghostSection}
513
522
 
514
523
  RESPOND ONLY with this exact JSON — no markdown, no extra text:
515
524
  {
@@ -526,16 +535,26 @@ RESPOND ONLY with this exact JSON — no markdown, no extra text:
526
535
  }
527
536
 
528
537
  RULES:
529
- 1. The step "id" MUST match a feature id from the feature map.
538
+ 1. The step "id" MUST match a feature id from the current page or other page features above.
530
539
  2. Only use selectors and IDs from the feature map. Never invent them.
531
- 3. No matching feature steps: [], explain kindly in message.
532
- 4. position values: top | bottom | left | right | auto only.
533
- 5. Order steps logically. For multi-step flows, order as the user encounters them.
534
- 6. For forms: ALWAYS include a step for the form section or first input BEFORE the
540
+ 3. position values: top | bottom | left | right | auto only.
541
+ 4. Order steps logically. For multi-step flows, order as the user encounters them.
542
+ 5. For forms: ALWAYS include a step for the form section or first input BEFORE the
535
543
  continue/submit button. The button step must always be LAST in its section.
536
- 7. If a feature has a flow, include one step per flow entry using the same feature id —
544
+ 6. If a feature has a flow, include one step per flow entry using the same feature id —
537
545
  the SDK expands them automatically.
538
- 8. Never skip features in a required sequence. Include every step end-to-end.
546
+ 7. Never skip features in a required sequence. Include every step end-to-end.
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.
539
558
  `.trim();
540
559
  }
541
560
 
package/dist/core.js CHANGED
@@ -484,7 +484,13 @@ function buildSystemPrompt(config) {
484
484
  var _f$screen;
485
485
  return (_f$screen = f.screen) === null || _f$screen === void 0 ? void 0 : _f$screen.id;
486
486
  }).map(f => f.screen.id))];
487
- const featureSummary = (config.features || []).map(f => {
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 => {
488
494
  var _f$screen2, _f$flow;
489
495
  const entry = {
490
496
  id: f.id,
@@ -499,15 +505,18 @@ function buildSystemPrompt(config) {
499
505
  entry.note = `This feature has ${f.flow.length} sequential sub-steps. Include ONE step per flow entry.`;
500
506
  }
501
507
  return entry;
502
- });
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)}` : '';
503
511
  return `
504
512
  You are an in-app assistant called "${config.assistantName || 'AI Guide'}" for "${config.appName}".
505
513
  Your ONLY job: guide users step-by-step through tasks using the feature map below.
506
514
 
507
- ${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. Features are screen-specific. The SDK handles navigation — just pick the right features.` : ''}
515
+ ${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. The SDK navigates automatically — just pick the right feature id.` : ''}
516
+
517
+ ${liveSection}
508
518
 
509
- FEATURE MAP (only reference IDs from this list):
510
- ${JSON.stringify(featureSummary, null, 2)}
519
+ ${ghostSection}
511
520
 
512
521
  RESPOND ONLY with this exact JSON — no markdown, no extra text:
513
522
  {
@@ -524,16 +533,26 @@ RESPOND ONLY with this exact JSON — no markdown, no extra text:
524
533
  }
525
534
 
526
535
  RULES:
527
- 1. The step "id" MUST match a feature id from the feature map.
536
+ 1. The step "id" MUST match a feature id from the current page or other page features above.
528
537
  2. Only use selectors and IDs from the feature map. Never invent them.
529
- 3. No matching feature steps: [], explain kindly in message.
530
- 4. position values: top | bottom | left | right | auto only.
531
- 5. Order steps logically. For multi-step flows, order as the user encounters them.
532
- 6. For forms: ALWAYS include a step for the form section or first input BEFORE the
538
+ 3. position values: top | bottom | left | right | auto only.
539
+ 4. Order steps logically. For multi-step flows, order as the user encounters them.
540
+ 5. For forms: ALWAYS include a step for the form section or first input BEFORE the
533
541
  continue/submit button. The button step must always be LAST in its section.
534
- 7. If a feature has a flow, include one step per flow entry using the same feature id —
542
+ 6. If a feature has a flow, include one step per flow entry using the same feature id —
535
543
  the SDK expands them automatically.
536
- 8. Never skip features in a required sequence. Include every step end-to-end.
544
+ 7. Never skip features in a required sequence. Include every step end-to-end.
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.
537
556
  `.trim();
538
557
  }
539
558
 
package/dist/index.cjs CHANGED
@@ -315,18 +315,28 @@ function EventopTarget({
315
315
  navigate,
316
316
  navigateWaitFor,
317
317
  advanceOn,
318
- waitFor,
319
- ...rest
318
+ waitFor
320
319
  }) {
321
320
  const registry = useRegistry();
322
- const ref = react.useRef(null);
321
+ const wrapperRef = react.useRef(null);
323
322
  const dataAttr = `data-evtp-${id}`;
324
323
  const selector = `[${dataAttr}]`;
325
324
  react.useEffect(() => {
325
+ var _wrapperRef$current;
326
326
  if (!id || !name) {
327
327
  console.warn('[Eventop] <EventopTarget> requires id and name props.');
328
328
  return;
329
329
  }
330
+
331
+ // Set the attribute directly on the first real child DOM element.
332
+ // This bypasses React's prop system entirely — works regardless of whether
333
+ // the wrapped component forwards unknown props.
334
+ const firstChild = (_wrapperRef$current = wrapperRef.current) === null || _wrapperRef$current === void 0 ? void 0 : _wrapperRef$current.firstElementChild;
335
+ if (firstChild) {
336
+ firstChild.setAttribute(dataAttr, '');
337
+ } else {
338
+ console.warn(`[Eventop] <EventopTarget id="${id}"> could not find a child DOM element to attach to. ` + `Make sure the wrapped component renders at least one DOM element.`);
339
+ }
330
340
  registry.registerFeature({
331
341
  id,
332
342
  name,
@@ -341,32 +351,24 @@ function EventopTarget({
341
351
  ...advanceOn
342
352
  } : null
343
353
  });
344
- return () => registry.unregisterFeature(id);
345
- }, [id, name, description, route]);
346
- const child = react.Children.only(children);
347
- let wrapped;
348
- try {
349
- wrapped = /*#__PURE__*/react.cloneElement(child, {
350
- [dataAttr]: '',
351
- ref: node => {
352
- ref.current = node;
353
- const originalRef = child.ref;
354
- if (typeof originalRef === 'function') originalRef(node);else if (originalRef && 'current' in originalRef) originalRef.current = node;
355
- }
356
- });
357
- } catch {
358
- wrapped = /*#__PURE__*/jsxRuntime.jsx("span", {
359
- [dataAttr]: '',
360
- ref: ref,
354
+ return () => {
355
+ var _wrapperRef$current2;
356
+ // Clean up the injected attribute on unmount
357
+ const el = (_wrapperRef$current2 = wrapperRef.current) === null || _wrapperRef$current2 === void 0 ? void 0 : _wrapperRef$current2.firstElementChild;
358
+ if (el) el.removeAttribute(dataAttr);
359
+ registry.unregisterFeature(id);
360
+ };
361
+ }, [id, name, description, route]); // eslint-disable-line react-hooks/exhaustive-deps
362
+
363
+ return /*#__PURE__*/jsxRuntime.jsx(EventopFeatureScopeContext.Provider, {
364
+ value: id,
365
+ children: /*#__PURE__*/jsxRuntime.jsx("span", {
366
+ ref: wrapperRef,
361
367
  style: {
362
368
  display: 'contents'
363
369
  },
364
- children: child
365
- });
366
- }
367
- return /*#__PURE__*/jsxRuntime.jsx(EventopFeatureScopeContext.Provider, {
368
- value: id,
369
- children: wrapped
370
+ children: react.Children.only(children)
371
+ })
370
372
  });
371
373
  }
372
374
 
@@ -381,7 +383,7 @@ function EventopStep({
381
383
  const registry = useRegistry();
382
384
  const featureScope = useFeatureScope();
383
385
  const featureId = feature || featureScope;
384
- const ref = react.useRef(null);
386
+ const wrapperRef = react.useRef(null);
385
387
  if (!featureId) {
386
388
  console.warn('[Eventop] <EventopStep> needs either a feature prop or an <EventopTarget> ancestor.');
387
389
  }
@@ -391,7 +393,14 @@ function EventopStep({
391
393
  const dataAttr = `data-evtp-step-${featureId}-${parentStep != null ? `${parentStep}-` : ''}${index}`;
392
394
  const selector = `[${dataAttr}]`;
393
395
  react.useEffect(() => {
396
+ var _wrapperRef$current;
394
397
  if (!featureId || index == null) return;
398
+
399
+ // Inject attribute directly onto the first real child DOM element
400
+ const firstChild = (_wrapperRef$current = wrapperRef.current) === null || _wrapperRef$current === void 0 ? void 0 : _wrapperRef$current.firstElementChild;
401
+ if (firstChild) {
402
+ firstChild.setAttribute(dataAttr, '');
403
+ }
395
404
  registry.registerStep(featureId, index, parentStep ?? null, {
396
405
  selector,
397
406
  waitFor: waitFor || null,
@@ -400,30 +409,21 @@ function EventopStep({
400
409
  ...advanceOn
401
410
  } : null
402
411
  });
403
- return () => registry.unregisterStep(featureId, index, parentStep ?? null);
404
- }, [featureId, index, parentStep]);
405
- const child = react.Children.only(children);
406
- let wrapped;
407
- try {
408
- wrapped = /*#__PURE__*/react.cloneElement(child, {
409
- [dataAttr]: '',
410
- ref: node => {
411
- ref.current = node;
412
- const originalRef = child.ref;
413
- if (typeof originalRef === 'function') originalRef(node);else if (originalRef && 'current' in originalRef) originalRef.current = node;
414
- }
415
- });
416
- } catch {
417
- wrapped = /*#__PURE__*/jsxRuntime.jsx("span", {
418
- [dataAttr]: '',
419
- ref: ref,
420
- style: {
421
- display: 'contents'
422
- },
423
- children: child
424
- });
425
- }
426
- return wrapped;
412
+ return () => {
413
+ var _wrapperRef$current2;
414
+ const el = (_wrapperRef$current2 = wrapperRef.current) === null || _wrapperRef$current2 === void 0 ? void 0 : _wrapperRef$current2.firstElementChild;
415
+ if (el) el.removeAttribute(dataAttr);
416
+ registry.unregisterStep(featureId, index, parentStep ?? null);
417
+ };
418
+ }, [featureId, index, parentStep]); // eslint-disable-line react-hooks/exhaustive-deps
419
+
420
+ return /*#__PURE__*/jsxRuntime.jsx("span", {
421
+ ref: wrapperRef,
422
+ style: {
423
+ display: 'contents'
424
+ },
425
+ children: react.Children.only(children)
426
+ });
427
427
  }
428
428
 
429
429
  // ═══════════════════════════════════════════════════════════════════════════
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useContext, useRef, useCallback, useEffect, Children, cloneElement, useState } from 'react';
1
+ import { createContext, useContext, useRef, useCallback, useEffect, Children, useState } from 'react';
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
 
4
4
  /**
@@ -313,18 +313,28 @@ function EventopTarget({
313
313
  navigate,
314
314
  navigateWaitFor,
315
315
  advanceOn,
316
- waitFor,
317
- ...rest
316
+ waitFor
318
317
  }) {
319
318
  const registry = useRegistry();
320
- const ref = useRef(null);
319
+ const wrapperRef = useRef(null);
321
320
  const dataAttr = `data-evtp-${id}`;
322
321
  const selector = `[${dataAttr}]`;
323
322
  useEffect(() => {
323
+ var _wrapperRef$current;
324
324
  if (!id || !name) {
325
325
  console.warn('[Eventop] <EventopTarget> requires id and name props.');
326
326
  return;
327
327
  }
328
+
329
+ // Set the attribute directly on the first real child DOM element.
330
+ // This bypasses React's prop system entirely — works regardless of whether
331
+ // the wrapped component forwards unknown props.
332
+ const firstChild = (_wrapperRef$current = wrapperRef.current) === null || _wrapperRef$current === void 0 ? void 0 : _wrapperRef$current.firstElementChild;
333
+ if (firstChild) {
334
+ firstChild.setAttribute(dataAttr, '');
335
+ } else {
336
+ console.warn(`[Eventop] <EventopTarget id="${id}"> could not find a child DOM element to attach to. ` + `Make sure the wrapped component renders at least one DOM element.`);
337
+ }
328
338
  registry.registerFeature({
329
339
  id,
330
340
  name,
@@ -339,32 +349,24 @@ function EventopTarget({
339
349
  ...advanceOn
340
350
  } : null
341
351
  });
342
- return () => registry.unregisterFeature(id);
343
- }, [id, name, description, route]);
344
- const child = Children.only(children);
345
- let wrapped;
346
- try {
347
- wrapped = /*#__PURE__*/cloneElement(child, {
348
- [dataAttr]: '',
349
- ref: node => {
350
- ref.current = node;
351
- const originalRef = child.ref;
352
- if (typeof originalRef === 'function') originalRef(node);else if (originalRef && 'current' in originalRef) originalRef.current = node;
353
- }
354
- });
355
- } catch {
356
- wrapped = /*#__PURE__*/jsx("span", {
357
- [dataAttr]: '',
358
- ref: ref,
352
+ return () => {
353
+ var _wrapperRef$current2;
354
+ // Clean up the injected attribute on unmount
355
+ const el = (_wrapperRef$current2 = wrapperRef.current) === null || _wrapperRef$current2 === void 0 ? void 0 : _wrapperRef$current2.firstElementChild;
356
+ if (el) el.removeAttribute(dataAttr);
357
+ registry.unregisterFeature(id);
358
+ };
359
+ }, [id, name, description, route]); // eslint-disable-line react-hooks/exhaustive-deps
360
+
361
+ return /*#__PURE__*/jsx(EventopFeatureScopeContext.Provider, {
362
+ value: id,
363
+ children: /*#__PURE__*/jsx("span", {
364
+ ref: wrapperRef,
359
365
  style: {
360
366
  display: 'contents'
361
367
  },
362
- children: child
363
- });
364
- }
365
- return /*#__PURE__*/jsx(EventopFeatureScopeContext.Provider, {
366
- value: id,
367
- children: wrapped
368
+ children: Children.only(children)
369
+ })
368
370
  });
369
371
  }
370
372
 
@@ -379,7 +381,7 @@ function EventopStep({
379
381
  const registry = useRegistry();
380
382
  const featureScope = useFeatureScope();
381
383
  const featureId = feature || featureScope;
382
- const ref = useRef(null);
384
+ const wrapperRef = useRef(null);
383
385
  if (!featureId) {
384
386
  console.warn('[Eventop] <EventopStep> needs either a feature prop or an <EventopTarget> ancestor.');
385
387
  }
@@ -389,7 +391,14 @@ function EventopStep({
389
391
  const dataAttr = `data-evtp-step-${featureId}-${parentStep != null ? `${parentStep}-` : ''}${index}`;
390
392
  const selector = `[${dataAttr}]`;
391
393
  useEffect(() => {
394
+ var _wrapperRef$current;
392
395
  if (!featureId || index == null) return;
396
+
397
+ // Inject attribute directly onto the first real child DOM element
398
+ const firstChild = (_wrapperRef$current = wrapperRef.current) === null || _wrapperRef$current === void 0 ? void 0 : _wrapperRef$current.firstElementChild;
399
+ if (firstChild) {
400
+ firstChild.setAttribute(dataAttr, '');
401
+ }
393
402
  registry.registerStep(featureId, index, parentStep ?? null, {
394
403
  selector,
395
404
  waitFor: waitFor || null,
@@ -398,30 +407,21 @@ function EventopStep({
398
407
  ...advanceOn
399
408
  } : null
400
409
  });
401
- return () => registry.unregisterStep(featureId, index, parentStep ?? null);
402
- }, [featureId, index, parentStep]);
403
- const child = Children.only(children);
404
- let wrapped;
405
- try {
406
- wrapped = /*#__PURE__*/cloneElement(child, {
407
- [dataAttr]: '',
408
- ref: node => {
409
- ref.current = node;
410
- const originalRef = child.ref;
411
- if (typeof originalRef === 'function') originalRef(node);else if (originalRef && 'current' in originalRef) originalRef.current = node;
412
- }
413
- });
414
- } catch {
415
- wrapped = /*#__PURE__*/jsx("span", {
416
- [dataAttr]: '',
417
- ref: ref,
418
- style: {
419
- display: 'contents'
420
- },
421
- children: child
422
- });
423
- }
424
- return wrapped;
410
+ return () => {
411
+ var _wrapperRef$current2;
412
+ const el = (_wrapperRef$current2 = wrapperRef.current) === null || _wrapperRef$current2 === void 0 ? void 0 : _wrapperRef$current2.firstElementChild;
413
+ if (el) el.removeAttribute(dataAttr);
414
+ registry.unregisterStep(featureId, index, parentStep ?? null);
415
+ };
416
+ }, [featureId, index, parentStep]); // eslint-disable-line react-hooks/exhaustive-deps
417
+
418
+ return /*#__PURE__*/jsx("span", {
419
+ ref: wrapperRef,
420
+ style: {
421
+ display: 'contents'
422
+ },
423
+ children: Children.only(children)
424
+ });
425
425
  }
426
426
 
427
427
  // ═══════════════════════════════════════════════════════════════════════════
@@ -486,7 +486,13 @@ function buildSystemPrompt(config) {
486
486
  var _f$screen;
487
487
  return (_f$screen = f.screen) === null || _f$screen === void 0 ? void 0 : _f$screen.id;
488
488
  }).map(f => f.screen.id))];
489
- const featureSummary = (config.features || []).map(f => {
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 => {
490
496
  var _f$screen2, _f$flow;
491
497
  const entry = {
492
498
  id: f.id,
@@ -501,15 +507,18 @@ function buildSystemPrompt(config) {
501
507
  entry.note = `This feature has ${f.flow.length} sequential sub-steps. Include ONE step per flow entry.`;
502
508
  }
503
509
  return entry;
504
- });
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)}` : '';
505
513
  return `
506
514
  You are an in-app assistant called "${config.assistantName || 'AI Guide'}" for "${config.appName}".
507
515
  Your ONLY job: guide users step-by-step through tasks using the feature map below.
508
516
 
509
- ${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. Features are screen-specific. The SDK handles navigation — just pick the right features.` : ''}
517
+ ${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. The SDK navigates automatically — just pick the right feature id.` : ''}
518
+
519
+ ${liveSection}
510
520
 
511
- FEATURE MAP (only reference IDs from this list):
512
- ${JSON.stringify(featureSummary, null, 2)}
521
+ ${ghostSection}
513
522
 
514
523
  RESPOND ONLY with this exact JSON — no markdown, no extra text:
515
524
  {
@@ -526,16 +535,26 @@ RESPOND ONLY with this exact JSON — no markdown, no extra text:
526
535
  }
527
536
 
528
537
  RULES:
529
- 1. The step "id" MUST match a feature id from the feature map.
538
+ 1. The step "id" MUST match a feature id from the current page or other page features above.
530
539
  2. Only use selectors and IDs from the feature map. Never invent them.
531
- 3. No matching feature steps: [], explain kindly in message.
532
- 4. position values: top | bottom | left | right | auto only.
533
- 5. Order steps logically. For multi-step flows, order as the user encounters them.
534
- 6. For forms: ALWAYS include a step for the form section or first input BEFORE the
540
+ 3. position values: top | bottom | left | right | auto only.
541
+ 4. Order steps logically. For multi-step flows, order as the user encounters them.
542
+ 5. For forms: ALWAYS include a step for the form section or first input BEFORE the
535
543
  continue/submit button. The button step must always be LAST in its section.
536
- 7. If a feature has a flow, include one step per flow entry using the same feature id —
544
+ 6. If a feature has a flow, include one step per flow entry using the same feature id —
537
545
  the SDK expands them automatically.
538
- 8. Never skip features in a required sequence. Include every step end-to-end.
546
+ 7. Never skip features in a required sequence. Include every step end-to-end.
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.
539
558
  `.trim();
540
559
  }
541
560
 
@@ -484,7 +484,13 @@ function buildSystemPrompt(config) {
484
484
  var _f$screen;
485
485
  return (_f$screen = f.screen) === null || _f$screen === void 0 ? void 0 : _f$screen.id;
486
486
  }).map(f => f.screen.id))];
487
- const featureSummary = (config.features || []).map(f => {
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 => {
488
494
  var _f$screen2, _f$flow;
489
495
  const entry = {
490
496
  id: f.id,
@@ -499,15 +505,18 @@ function buildSystemPrompt(config) {
499
505
  entry.note = `This feature has ${f.flow.length} sequential sub-steps. Include ONE step per flow entry.`;
500
506
  }
501
507
  return entry;
502
- });
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)}` : '';
503
511
  return `
504
512
  You are an in-app assistant called "${config.assistantName || 'AI Guide'}" for "${config.appName}".
505
513
  Your ONLY job: guide users step-by-step through tasks using the feature map below.
506
514
 
507
- ${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. Features are screen-specific. The SDK handles navigation — just pick the right features.` : ''}
515
+ ${screens.length > 1 ? `SCREENS: This app has multiple screens: ${screens.join(', ')}. The SDK navigates automatically — just pick the right feature id.` : ''}
516
+
517
+ ${liveSection}
508
518
 
509
- FEATURE MAP (only reference IDs from this list):
510
- ${JSON.stringify(featureSummary, null, 2)}
519
+ ${ghostSection}
511
520
 
512
521
  RESPOND ONLY with this exact JSON — no markdown, no extra text:
513
522
  {
@@ -524,16 +533,26 @@ RESPOND ONLY with this exact JSON — no markdown, no extra text:
524
533
  }
525
534
 
526
535
  RULES:
527
- 1. The step "id" MUST match a feature id from the feature map.
536
+ 1. The step "id" MUST match a feature id from the current page or other page features above.
528
537
  2. Only use selectors and IDs from the feature map. Never invent them.
529
- 3. No matching feature steps: [], explain kindly in message.
530
- 4. position values: top | bottom | left | right | auto only.
531
- 5. Order steps logically. For multi-step flows, order as the user encounters them.
532
- 6. For forms: ALWAYS include a step for the form section or first input BEFORE the
538
+ 3. position values: top | bottom | left | right | auto only.
539
+ 4. Order steps logically. For multi-step flows, order as the user encounters them.
540
+ 5. For forms: ALWAYS include a step for the form section or first input BEFORE the
533
541
  continue/submit button. The button step must always be LAST in its section.
534
- 7. If a feature has a flow, include one step per flow entry using the same feature id —
542
+ 6. If a feature has a flow, include one step per flow entry using the same feature id —
535
543
  the SDK expands them automatically.
536
- 8. Never skip features in a required sequence. Include every step end-to-end.
544
+ 7. Never skip features in a required sequence. Include every step end-to-end.
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.
537
556
  `.trim();
538
557
  }
539
558
 
@@ -315,18 +315,28 @@ function EventopTarget({
315
315
  navigate,
316
316
  navigateWaitFor,
317
317
  advanceOn,
318
- waitFor,
319
- ...rest
318
+ waitFor
320
319
  }) {
321
320
  const registry = useRegistry();
322
- const ref = react.useRef(null);
321
+ const wrapperRef = react.useRef(null);
323
322
  const dataAttr = `data-evtp-${id}`;
324
323
  const selector = `[${dataAttr}]`;
325
324
  react.useEffect(() => {
325
+ var _wrapperRef$current;
326
326
  if (!id || !name) {
327
327
  console.warn('[Eventop] <EventopTarget> requires id and name props.');
328
328
  return;
329
329
  }
330
+
331
+ // Set the attribute directly on the first real child DOM element.
332
+ // This bypasses React's prop system entirely — works regardless of whether
333
+ // the wrapped component forwards unknown props.
334
+ const firstChild = (_wrapperRef$current = wrapperRef.current) === null || _wrapperRef$current === void 0 ? void 0 : _wrapperRef$current.firstElementChild;
335
+ if (firstChild) {
336
+ firstChild.setAttribute(dataAttr, '');
337
+ } else {
338
+ console.warn(`[Eventop] <EventopTarget id="${id}"> could not find a child DOM element to attach to. ` + `Make sure the wrapped component renders at least one DOM element.`);
339
+ }
330
340
  registry.registerFeature({
331
341
  id,
332
342
  name,
@@ -341,32 +351,24 @@ function EventopTarget({
341
351
  ...advanceOn
342
352
  } : null
343
353
  });
344
- return () => registry.unregisterFeature(id);
345
- }, [id, name, description, route]);
346
- const child = react.Children.only(children);
347
- let wrapped;
348
- try {
349
- wrapped = /*#__PURE__*/react.cloneElement(child, {
350
- [dataAttr]: '',
351
- ref: node => {
352
- ref.current = node;
353
- const originalRef = child.ref;
354
- if (typeof originalRef === 'function') originalRef(node);else if (originalRef && 'current' in originalRef) originalRef.current = node;
355
- }
356
- });
357
- } catch {
358
- wrapped = /*#__PURE__*/jsxRuntime.jsx("span", {
359
- [dataAttr]: '',
360
- ref: ref,
354
+ return () => {
355
+ var _wrapperRef$current2;
356
+ // Clean up the injected attribute on unmount
357
+ const el = (_wrapperRef$current2 = wrapperRef.current) === null || _wrapperRef$current2 === void 0 ? void 0 : _wrapperRef$current2.firstElementChild;
358
+ if (el) el.removeAttribute(dataAttr);
359
+ registry.unregisterFeature(id);
360
+ };
361
+ }, [id, name, description, route]); // eslint-disable-line react-hooks/exhaustive-deps
362
+
363
+ return /*#__PURE__*/jsxRuntime.jsx(EventopFeatureScopeContext.Provider, {
364
+ value: id,
365
+ children: /*#__PURE__*/jsxRuntime.jsx("span", {
366
+ ref: wrapperRef,
361
367
  style: {
362
368
  display: 'contents'
363
369
  },
364
- children: child
365
- });
366
- }
367
- return /*#__PURE__*/jsxRuntime.jsx(EventopFeatureScopeContext.Provider, {
368
- value: id,
369
- children: wrapped
370
+ children: react.Children.only(children)
371
+ })
370
372
  });
371
373
  }
372
374
 
@@ -381,7 +383,7 @@ function EventopStep({
381
383
  const registry = useRegistry();
382
384
  const featureScope = useFeatureScope();
383
385
  const featureId = feature || featureScope;
384
- const ref = react.useRef(null);
386
+ const wrapperRef = react.useRef(null);
385
387
  if (!featureId) {
386
388
  console.warn('[Eventop] <EventopStep> needs either a feature prop or an <EventopTarget> ancestor.');
387
389
  }
@@ -391,7 +393,14 @@ function EventopStep({
391
393
  const dataAttr = `data-evtp-step-${featureId}-${parentStep != null ? `${parentStep}-` : ''}${index}`;
392
394
  const selector = `[${dataAttr}]`;
393
395
  react.useEffect(() => {
396
+ var _wrapperRef$current;
394
397
  if (!featureId || index == null) return;
398
+
399
+ // Inject attribute directly onto the first real child DOM element
400
+ const firstChild = (_wrapperRef$current = wrapperRef.current) === null || _wrapperRef$current === void 0 ? void 0 : _wrapperRef$current.firstElementChild;
401
+ if (firstChild) {
402
+ firstChild.setAttribute(dataAttr, '');
403
+ }
395
404
  registry.registerStep(featureId, index, parentStep ?? null, {
396
405
  selector,
397
406
  waitFor: waitFor || null,
@@ -400,30 +409,21 @@ function EventopStep({
400
409
  ...advanceOn
401
410
  } : null
402
411
  });
403
- return () => registry.unregisterStep(featureId, index, parentStep ?? null);
404
- }, [featureId, index, parentStep]);
405
- const child = react.Children.only(children);
406
- let wrapped;
407
- try {
408
- wrapped = /*#__PURE__*/react.cloneElement(child, {
409
- [dataAttr]: '',
410
- ref: node => {
411
- ref.current = node;
412
- const originalRef = child.ref;
413
- if (typeof originalRef === 'function') originalRef(node);else if (originalRef && 'current' in originalRef) originalRef.current = node;
414
- }
415
- });
416
- } catch {
417
- wrapped = /*#__PURE__*/jsxRuntime.jsx("span", {
418
- [dataAttr]: '',
419
- ref: ref,
420
- style: {
421
- display: 'contents'
422
- },
423
- children: child
424
- });
425
- }
426
- return wrapped;
412
+ return () => {
413
+ var _wrapperRef$current2;
414
+ const el = (_wrapperRef$current2 = wrapperRef.current) === null || _wrapperRef$current2 === void 0 ? void 0 : _wrapperRef$current2.firstElementChild;
415
+ if (el) el.removeAttribute(dataAttr);
416
+ registry.unregisterStep(featureId, index, parentStep ?? null);
417
+ };
418
+ }, [featureId, index, parentStep]); // eslint-disable-line react-hooks/exhaustive-deps
419
+
420
+ return /*#__PURE__*/jsxRuntime.jsx("span", {
421
+ ref: wrapperRef,
422
+ style: {
423
+ display: 'contents'
424
+ },
425
+ children: react.Children.only(children)
426
+ });
427
427
  }
428
428
 
429
429
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1,4 +1,4 @@
1
- import { createContext, useContext, useRef, useCallback, useEffect, Children, cloneElement, useState } from 'react';
1
+ import { createContext, useContext, useRef, useCallback, useEffect, Children, useState } from 'react';
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
 
4
4
  /**
@@ -313,18 +313,28 @@ function EventopTarget({
313
313
  navigate,
314
314
  navigateWaitFor,
315
315
  advanceOn,
316
- waitFor,
317
- ...rest
316
+ waitFor
318
317
  }) {
319
318
  const registry = useRegistry();
320
- const ref = useRef(null);
319
+ const wrapperRef = useRef(null);
321
320
  const dataAttr = `data-evtp-${id}`;
322
321
  const selector = `[${dataAttr}]`;
323
322
  useEffect(() => {
323
+ var _wrapperRef$current;
324
324
  if (!id || !name) {
325
325
  console.warn('[Eventop] <EventopTarget> requires id and name props.');
326
326
  return;
327
327
  }
328
+
329
+ // Set the attribute directly on the first real child DOM element.
330
+ // This bypasses React's prop system entirely — works regardless of whether
331
+ // the wrapped component forwards unknown props.
332
+ const firstChild = (_wrapperRef$current = wrapperRef.current) === null || _wrapperRef$current === void 0 ? void 0 : _wrapperRef$current.firstElementChild;
333
+ if (firstChild) {
334
+ firstChild.setAttribute(dataAttr, '');
335
+ } else {
336
+ console.warn(`[Eventop] <EventopTarget id="${id}"> could not find a child DOM element to attach to. ` + `Make sure the wrapped component renders at least one DOM element.`);
337
+ }
328
338
  registry.registerFeature({
329
339
  id,
330
340
  name,
@@ -339,32 +349,24 @@ function EventopTarget({
339
349
  ...advanceOn
340
350
  } : null
341
351
  });
342
- return () => registry.unregisterFeature(id);
343
- }, [id, name, description, route]);
344
- const child = Children.only(children);
345
- let wrapped;
346
- try {
347
- wrapped = /*#__PURE__*/cloneElement(child, {
348
- [dataAttr]: '',
349
- ref: node => {
350
- ref.current = node;
351
- const originalRef = child.ref;
352
- if (typeof originalRef === 'function') originalRef(node);else if (originalRef && 'current' in originalRef) originalRef.current = node;
353
- }
354
- });
355
- } catch {
356
- wrapped = /*#__PURE__*/jsx("span", {
357
- [dataAttr]: '',
358
- ref: ref,
352
+ return () => {
353
+ var _wrapperRef$current2;
354
+ // Clean up the injected attribute on unmount
355
+ const el = (_wrapperRef$current2 = wrapperRef.current) === null || _wrapperRef$current2 === void 0 ? void 0 : _wrapperRef$current2.firstElementChild;
356
+ if (el) el.removeAttribute(dataAttr);
357
+ registry.unregisterFeature(id);
358
+ };
359
+ }, [id, name, description, route]); // eslint-disable-line react-hooks/exhaustive-deps
360
+
361
+ return /*#__PURE__*/jsx(EventopFeatureScopeContext.Provider, {
362
+ value: id,
363
+ children: /*#__PURE__*/jsx("span", {
364
+ ref: wrapperRef,
359
365
  style: {
360
366
  display: 'contents'
361
367
  },
362
- children: child
363
- });
364
- }
365
- return /*#__PURE__*/jsx(EventopFeatureScopeContext.Provider, {
366
- value: id,
367
- children: wrapped
368
+ children: Children.only(children)
369
+ })
368
370
  });
369
371
  }
370
372
 
@@ -379,7 +381,7 @@ function EventopStep({
379
381
  const registry = useRegistry();
380
382
  const featureScope = useFeatureScope();
381
383
  const featureId = feature || featureScope;
382
- const ref = useRef(null);
384
+ const wrapperRef = useRef(null);
383
385
  if (!featureId) {
384
386
  console.warn('[Eventop] <EventopStep> needs either a feature prop or an <EventopTarget> ancestor.');
385
387
  }
@@ -389,7 +391,14 @@ function EventopStep({
389
391
  const dataAttr = `data-evtp-step-${featureId}-${parentStep != null ? `${parentStep}-` : ''}${index}`;
390
392
  const selector = `[${dataAttr}]`;
391
393
  useEffect(() => {
394
+ var _wrapperRef$current;
392
395
  if (!featureId || index == null) return;
396
+
397
+ // Inject attribute directly onto the first real child DOM element
398
+ const firstChild = (_wrapperRef$current = wrapperRef.current) === null || _wrapperRef$current === void 0 ? void 0 : _wrapperRef$current.firstElementChild;
399
+ if (firstChild) {
400
+ firstChild.setAttribute(dataAttr, '');
401
+ }
393
402
  registry.registerStep(featureId, index, parentStep ?? null, {
394
403
  selector,
395
404
  waitFor: waitFor || null,
@@ -398,30 +407,21 @@ function EventopStep({
398
407
  ...advanceOn
399
408
  } : null
400
409
  });
401
- return () => registry.unregisterStep(featureId, index, parentStep ?? null);
402
- }, [featureId, index, parentStep]);
403
- const child = Children.only(children);
404
- let wrapped;
405
- try {
406
- wrapped = /*#__PURE__*/cloneElement(child, {
407
- [dataAttr]: '',
408
- ref: node => {
409
- ref.current = node;
410
- const originalRef = child.ref;
411
- if (typeof originalRef === 'function') originalRef(node);else if (originalRef && 'current' in originalRef) originalRef.current = node;
412
- }
413
- });
414
- } catch {
415
- wrapped = /*#__PURE__*/jsx("span", {
416
- [dataAttr]: '',
417
- ref: ref,
418
- style: {
419
- display: 'contents'
420
- },
421
- children: child
422
- });
423
- }
424
- return wrapped;
410
+ return () => {
411
+ var _wrapperRef$current2;
412
+ const el = (_wrapperRef$current2 = wrapperRef.current) === null || _wrapperRef$current2 === void 0 ? void 0 : _wrapperRef$current2.firstElementChild;
413
+ if (el) el.removeAttribute(dataAttr);
414
+ registry.unregisterStep(featureId, index, parentStep ?? null);
415
+ };
416
+ }, [featureId, index, parentStep]); // eslint-disable-line react-hooks/exhaustive-deps
417
+
418
+ return /*#__PURE__*/jsx("span", {
419
+ ref: wrapperRef,
420
+ style: {
421
+ display: 'contents'
422
+ },
423
+ children: Children.only(children)
424
+ });
425
425
  }
426
426
 
427
427
  // ═══════════════════════════════════════════════════════════════════════════
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventop/sdk",
3
- "version": "1.2.12",
3
+ "version": "1.2.14",
4
4
  "description": "AI-powered guided tours for any web app. Drop-in, themeable, provider-agnostic.",
5
5
  "keywords": [
6
6
  "onboarding",