@modelnex/sdk 0.5.25 → 0.5.27

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/index.d.mts CHANGED
@@ -133,7 +133,7 @@ interface TourStep {
133
133
  /** Optional onboarding-specific step metadata */
134
134
  onboarding?: OnboardingStepMetadata;
135
135
  }
136
- type TourTrigger = 'first_visit' | 'feature_launch' | 'feature_unlocked' | 'feature_unlock' | 'new_feature' | 'manual';
136
+ type TourTrigger = 'first_visit' | 'return_visit' | 'feature_launch' | 'feature_unlocked' | 'feature_unlock' | 'new_feature' | 'manual';
137
137
  type TourStartPolicy = 'immediate_start' | 'prompt_only' | 'manual_only';
138
138
  type TourNotificationType = 'bubble_card' | 'modal';
139
139
  interface TourVoiceConfig {
@@ -200,6 +200,12 @@ interface UserProfile {
200
200
  type: string;
201
201
  /** Whether this is a new user — triggers first_visit tours */
202
202
  isNewUser?: boolean;
203
+ /** Optional feature facts used for feature-triggered tours */
204
+ features?: string[];
205
+ /** Backward-compatible facts bag for feature-triggered tours */
206
+ tourFacts?: {
207
+ features?: string[];
208
+ };
203
209
  /** User ID for per-user completion state */
204
210
  userId?: string;
205
211
  }
package/dist/index.d.ts CHANGED
@@ -133,7 +133,7 @@ interface TourStep {
133
133
  /** Optional onboarding-specific step metadata */
134
134
  onboarding?: OnboardingStepMetadata;
135
135
  }
136
- type TourTrigger = 'first_visit' | 'feature_launch' | 'feature_unlocked' | 'feature_unlock' | 'new_feature' | 'manual';
136
+ type TourTrigger = 'first_visit' | 'return_visit' | 'feature_launch' | 'feature_unlocked' | 'feature_unlock' | 'new_feature' | 'manual';
137
137
  type TourStartPolicy = 'immediate_start' | 'prompt_only' | 'manual_only';
138
138
  type TourNotificationType = 'bubble_card' | 'modal';
139
139
  interface TourVoiceConfig {
@@ -200,6 +200,12 @@ interface UserProfile {
200
200
  type: string;
201
201
  /** Whether this is a new user — triggers first_visit tours */
202
202
  isNewUser?: boolean;
203
+ /** Optional feature facts used for feature-triggered tours */
204
+ features?: string[];
205
+ /** Backward-compatible facts bag for feature-triggered tours */
206
+ tourFacts?: {
207
+ features?: string[];
208
+ };
203
209
  /** User ID for per-user completion state */
204
210
  userId?: string;
205
211
  }
package/dist/index.js CHANGED
@@ -2667,6 +2667,9 @@ function normalizeTrigger(trigger) {
2667
2667
  if (trigger === "feature_launch" || trigger === "feature_unlocked" || trigger === "feature_unlock" || trigger === "new_feature") {
2668
2668
  return "feature_launch";
2669
2669
  }
2670
+ if (trigger === "return_visit") {
2671
+ return "return_visit";
2672
+ }
2670
2673
  return trigger ?? "first_visit";
2671
2674
  }
2672
2675
  function getStartPolicy(tour) {
@@ -2675,10 +2678,22 @@ function getStartPolicy(tour) {
2675
2678
  function getNotificationType(tour) {
2676
2679
  return tour.notificationType ?? "bubble_card";
2677
2680
  }
2681
+ function getUserProfileFeatures(userProfile) {
2682
+ const directFeatures = Array.isArray(userProfile.features) ? userProfile.features : [];
2683
+ const nestedFeatures = Array.isArray(userProfile.tourFacts?.features) ? userProfile.tourFacts.features : [];
2684
+ return [...new Set([...directFeatures, ...nestedFeatures].filter((value) => Boolean(value)))];
2685
+ }
2678
2686
  function isTourEligible(tour, userProfile) {
2679
2687
  switch (normalizeTrigger(tour.trigger)) {
2680
2688
  case "first_visit":
2681
2689
  return !!userProfile.isNewUser;
2690
+ case "return_visit":
2691
+ return userProfile.isNewUser === false;
2692
+ case "feature_launch": {
2693
+ const featureKey = tour.featureKey?.trim();
2694
+ if (!featureKey) return false;
2695
+ return getUserProfileFeatures(userProfile).includes(featureKey);
2696
+ }
2682
2697
  case "manual":
2683
2698
  return false;
2684
2699
  default:
@@ -8119,7 +8134,7 @@ function getRecordingDraftStatusMessage(phase, experienceType) {
8119
8134
 
8120
8135
  // src/utils/tour-listening.ts
8121
8136
  function canStartTourListening(state) {
8122
- const hasLiveOrPendingPlayback = state.isTourActive || state.isOnboardingActive || state.hasPendingTour || state.hasPendingOnboarding;
8137
+ const hasLiveOrPendingPlayback = state.isTourActive || state.isOnboardingActive || state.hasPendingTour || state.hasPendingOnboarding || Boolean(state.startingExperienceType);
8123
8138
  return hasLiveOrPendingPlayback && !state.sttActive && state.sttSupported;
8124
8139
  }
8125
8140
  function resolveTourListeningExperience(preferredExperience, state) {
@@ -8196,6 +8211,58 @@ function buildTranscriptPreviewLines(transcript, options = {}) {
8196
8211
  return lines.slice(-maxLines);
8197
8212
  }
8198
8213
 
8214
+ // src/utils/pendingPromptUi.ts
8215
+ function isPendingReviewPrompt(pendingPrompt) {
8216
+ return Boolean(pendingPrompt?.options?.reviewMode);
8217
+ }
8218
+ function shouldRenderPendingPromptInline({
8219
+ pendingPrompt,
8220
+ isPlaybackActive,
8221
+ recordingMode
8222
+ }) {
8223
+ return Boolean(
8224
+ pendingPrompt && !isPlaybackActive && !recordingMode && !isPendingReviewPrompt(pendingPrompt)
8225
+ );
8226
+ }
8227
+ function shouldRenderPendingPromptModal({
8228
+ pendingPrompt,
8229
+ isPlaybackActive,
8230
+ recordingMode,
8231
+ pendingNotificationType
8232
+ }) {
8233
+ return Boolean(
8234
+ pendingPrompt && !isPlaybackActive && !recordingMode && isPendingReviewPrompt(pendingPrompt) && pendingNotificationType === "modal"
8235
+ );
8236
+ }
8237
+ function shouldAutoExpandForPendingPrompt({
8238
+ pendingPrompt,
8239
+ isPlaybackActive,
8240
+ recordingMode,
8241
+ pendingNotificationType
8242
+ }) {
8243
+ if (!pendingPrompt || isPlaybackActive || recordingMode) return false;
8244
+ return pendingNotificationType === "bubble_card" || shouldRenderPendingPromptInline({
8245
+ pendingPrompt,
8246
+ isPlaybackActive,
8247
+ recordingMode
8248
+ });
8249
+ }
8250
+ function getPendingPromptTitle(pendingPrompt) {
8251
+ return `I found a ${pendingPrompt.experienceType === "onboarding" ? "workflow" : "tour"} called "${pendingPrompt.tour.name}". Would you like to start it now?`;
8252
+ }
8253
+ function getPendingPromptReason(pendingPrompt) {
8254
+ if (pendingPrompt.tour.trigger === "first_visit") {
8255
+ return "This was surfaced because the current user matches the configured first-visit trigger.";
8256
+ }
8257
+ if (pendingPrompt.tour.trigger === "return_visit") {
8258
+ return "This was surfaced because the current user matches the configured return-visit trigger.";
8259
+ }
8260
+ if (pendingPrompt.tour.featureKey) {
8261
+ return `This was surfaced because the "${pendingPrompt.tour.featureKey}" trigger condition matched.`;
8262
+ }
8263
+ return "This was surfaced because the configured trigger condition matched.";
8264
+ }
8265
+
8199
8266
  // src/utils/floatingLiveTranscript.ts
8200
8267
  var floatingTranscriptEl = null;
8201
8268
  var liveTranscriptSuppressed = false;
@@ -8915,7 +8982,20 @@ function ModelNexChatBubble({
8915
8982
  }
8916
8983
  }, [devMode, handleAutoTag, ctx?.extractedElements.length, window.location.pathname]);
8917
8984
  const onboardingReviewToggle = getReviewModeToggleConfig(onboardingPlayback.playbackState);
8985
+ const pendingPrompt = playbackController.pendingPrompt;
8918
8986
  const pendingNotificationType = (onboardingPlayback.pendingTour || tourPlayback.pendingTour)?.notificationType ?? "bubble_card";
8987
+ const isPlaybackActive = tourPlayback.isActive || onboardingPlayback.isActive;
8988
+ const renderPendingPromptInline = shouldRenderPendingPromptInline({
8989
+ pendingPrompt,
8990
+ isPlaybackActive,
8991
+ recordingMode
8992
+ });
8993
+ const renderPendingPromptModal = shouldRenderPendingPromptModal({
8994
+ pendingPrompt,
8995
+ isPlaybackActive,
8996
+ recordingMode,
8997
+ pendingNotificationType
8998
+ });
8919
8999
  (0, import_react18.useEffect)(() => {
8920
9000
  setHydrated(true);
8921
9001
  try {
@@ -8982,10 +9062,15 @@ function ModelNexChatBubble({
8982
9062
  }
8983
9063
  }, [tourPlayback.isActive, onboardingPlayback.isActive]);
8984
9064
  (0, import_react18.useEffect)(() => {
8985
- if ((onboardingPlayback.pendingTour || tourPlayback.pendingTour) && !recordingMode && pendingNotificationType === "bubble_card") {
9065
+ if (shouldAutoExpandForPendingPrompt({
9066
+ pendingPrompt,
9067
+ isPlaybackActive,
9068
+ recordingMode,
9069
+ pendingNotificationType
9070
+ })) {
8986
9071
  setExpandedState(true);
8987
9072
  }
8988
- }, [onboardingPlayback.pendingTour, tourPlayback.pendingTour, recordingMode, pendingNotificationType, setExpandedState]);
9073
+ }, [isPlaybackActive, pendingNotificationType, pendingPrompt, recordingMode, setExpandedState]);
8989
9074
  const preferredListeningExperienceRef = (0, import_react18.useRef)(null);
8990
9075
  const playbackVoiceRoutingRef = (0, import_react18.useRef)({
8991
9076
  activeExperienceType,
@@ -9033,6 +9118,7 @@ function ModelNexChatBubble({
9033
9118
  isOnboardingActive: onboardingPlayback.isActive,
9034
9119
  hasPendingTour: Boolean(tourPlayback.pendingTour),
9035
9120
  hasPendingOnboarding: Boolean(onboardingPlayback.pendingTour),
9121
+ startingExperienceType,
9036
9122
  sttActive: sttActiveRef.current,
9037
9123
  sttSupported: voice.sttSupported
9038
9124
  };
@@ -9078,19 +9164,20 @@ function ModelNexChatBubble({
9078
9164
  tourPlayback.pendingTour,
9079
9165
  onboardingPlayback.isActive,
9080
9166
  onboardingPlayback.pendingTour,
9167
+ startingExperienceType,
9081
9168
  voice,
9082
9169
  handleVoiceTourInput,
9083
9170
  updateTourListenReady,
9084
9171
  updateTourSttError
9085
9172
  ]);
9086
9173
  (0, import_react18.useEffect)(() => {
9087
- const isPlaybackActive = isTourListeningSessionActive({
9174
+ const isPlaybackActive2 = isTourListeningSessionActive({
9088
9175
  isTourActive: tourPlayback.isActive,
9089
9176
  isOnboardingActive: onboardingPlayback.isActive,
9090
9177
  startingExperienceType
9091
9178
  });
9092
- const becameActive = isPlaybackActive && !previousTourActiveRef.current;
9093
- previousTourActiveRef.current = isPlaybackActive;
9179
+ const becameActive = isPlaybackActive2 && !previousTourActiveRef.current;
9180
+ previousTourActiveRef.current = isPlaybackActive2;
9094
9181
  if (becameActive) {
9095
9182
  try {
9096
9183
  sessionStorage.setItem(TOUR_MINIMIZED_STORAGE_KEY, "true");
@@ -9099,7 +9186,7 @@ function ModelNexChatBubble({
9099
9186
  setExpandedState(false, { rememberTourMinimize: true });
9100
9187
  updateTourSttError(null);
9101
9188
  }
9102
- if (!isPlaybackActive) {
9189
+ if (!isPlaybackActive2) {
9103
9190
  try {
9104
9191
  sessionStorage.removeItem(TOUR_MINIMIZED_STORAGE_KEY);
9105
9192
  } catch {
@@ -9126,12 +9213,12 @@ function ModelNexChatBubble({
9126
9213
  }
9127
9214
  }, [recordingMode, setExpandedState]);
9128
9215
  (0, import_react18.useEffect)(() => {
9129
- const isPlaybackActive = isTourListeningSessionActive({
9216
+ const isPlaybackActive2 = isTourListeningSessionActive({
9130
9217
  isTourActive: tourPlayback.isActive,
9131
9218
  isOnboardingActive: onboardingPlayback.isActive,
9132
9219
  startingExperienceType
9133
9220
  });
9134
- if (!isPlaybackActive) {
9221
+ if (!isPlaybackActive2) {
9135
9222
  updateTourListenReady(false);
9136
9223
  setTourLiveTranscript("");
9137
9224
  hideFloatingLiveTranscript();
@@ -9140,12 +9227,12 @@ function ModelNexChatBubble({
9140
9227
  updateTourListenReady(Boolean(voice.isListening && sttActiveRef.current));
9141
9228
  }, [tourPlayback.isActive, onboardingPlayback.isActive, voice.isListening, startingExperienceType, updateTourListenReady]);
9142
9229
  (0, import_react18.useEffect)(() => {
9143
- const isPlaybackActive = isTourListeningSessionActive({
9230
+ const isPlaybackActive2 = isTourListeningSessionActive({
9144
9231
  isTourActive: tourPlayback.isActive,
9145
9232
  isOnboardingActive: onboardingPlayback.isActive,
9146
9233
  startingExperienceType
9147
9234
  });
9148
- if (!isPlaybackActive && sttActiveRef.current) {
9235
+ if (!isPlaybackActive2 && sttActiveRef.current) {
9149
9236
  sttActiveRef.current = false;
9150
9237
  updateTourListenReady(false);
9151
9238
  updateTourSttError(null);
@@ -9155,12 +9242,12 @@ function ModelNexChatBubble({
9155
9242
  }
9156
9243
  }, [tourPlayback.isActive, onboardingPlayback.isActive, voice, startingExperienceType, updateTourListenReady, updateTourSttError]);
9157
9244
  (0, import_react18.useEffect)(() => {
9158
- const isPlaybackActive = isTourListeningSessionActive({
9245
+ const isPlaybackActive2 = isTourListeningSessionActive({
9159
9246
  isTourActive: tourPlayback.isActive,
9160
9247
  isOnboardingActive: onboardingPlayback.isActive,
9161
9248
  startingExperienceType
9162
9249
  });
9163
- if (!isPlaybackActive || tourListenReady || !voice.sttSupported || tourSttError === "not-allowed") {
9250
+ if (!isPlaybackActive2 || tourListenReady || !voice.sttSupported || tourSttError === "not-allowed") {
9164
9251
  return;
9165
9252
  }
9166
9253
  const enableTourListeningFromGesture = (event) => {
@@ -9367,8 +9454,8 @@ function ModelNexChatBubble({
9367
9454
  return (0, import_react_dom.createPortal)(
9368
9455
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: containerStyle, className, "data-modelnex-internal": "true", onMouseDown: stopEventPropagation, onClick: stopEventPropagation, children: [
9369
9456
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: GLOBAL_STYLES }),
9370
- !(tourPlayback.isActive || onboardingPlayback.isActive) && (tourPlayback.pendingTour || onboardingPlayback.pendingTour) && pendingNotificationType === "modal" && !recordingMode && (() => {
9371
- const pt = onboardingPlayback.pendingTour || tourPlayback.pendingTour;
9457
+ renderPendingPromptModal && (() => {
9458
+ const pt = pendingPrompt?.tour;
9372
9459
  const mc = pt?.presentation?.modalConfig || {};
9373
9460
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
9374
9461
  "div",
@@ -9430,8 +9517,8 @@ function ModelNexChatBubble({
9430
9517
  "button",
9431
9518
  {
9432
9519
  onClick: () => {
9433
- const preferredExperience = onboardingPlayback.pendingTour ? "onboarding" : "tour";
9434
- if (onboardingPlayback.pendingTour) {
9520
+ const preferredExperience = pendingPrompt?.experienceType === "onboarding" ? "onboarding" : "tour";
9521
+ if (preferredExperience === "onboarding") {
9435
9522
  onboardingPlayback.acceptPendingTour();
9436
9523
  } else {
9437
9524
  tourPlayback.acceptPendingTour();
@@ -9594,24 +9681,28 @@ function ModelNexChatBubble({
9594
9681
  children: "Microphone access is blocked. Use the input box below, or allow microphone permission and tap the mic again."
9595
9682
  }
9596
9683
  ),
9597
- !(tourPlayback.isActive || onboardingPlayback.isActive) && (tourPlayback.pendingTour || onboardingPlayback.pendingTour) && pendingNotificationType === "bubble_card" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
9684
+ renderPendingPromptInline && pendingPrompt && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { display: "flex", justifyContent: "flex-start" }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
9598
9685
  "div",
9599
9686
  {
9600
9687
  style: {
9601
- margin: "12px",
9602
- padding: "14px",
9603
- borderRadius: "12px",
9604
- border: "1px solid rgba(59,130,246,0.18)",
9605
- background: "linear-gradient(135deg, rgba(59,130,246,0.08) 0%, rgba(59,130,246,0.03) 100%)"
9688
+ maxWidth: "85%",
9689
+ padding: "12px 16px",
9690
+ borderRadius: "var(--modelnex-radius-inner, 16px)",
9691
+ borderBottomLeftRadius: 4,
9692
+ fontSize: "14px",
9693
+ lineHeight: 1.55,
9694
+ background: "var(--modelnex-bg, #fff)",
9695
+ color: "var(--modelnex-fg, #18181b)",
9696
+ border: "1px solid var(--modelnex-border, #e4e4e7)",
9697
+ boxShadow: "0 2px 4px rgba(0,0,0,0.02)"
9606
9698
  },
9607
9699
  children: [
9608
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "13px", fontWeight: 600, color: "var(--modelnex-fg, #18181b)", marginBottom: "6px" }, children: onboardingPlayback.pendingTour ? "Suggested workflow" : "Suggested tour" }),
9609
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "14px", color: "var(--modelnex-fg, #27272a)", marginBottom: "6px" }, children: (onboardingPlayback.pendingTour || tourPlayback.pendingTour)?.name }),
9610
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "12px", color: "#52525b", lineHeight: 1.45, marginBottom: "12px" }, children: (onboardingPlayback.pendingTour || tourPlayback.pendingTour)?.trigger === "first_visit" ? "Start a short walkthrough for this user now?" : (onboardingPlayback.pendingTour || tourPlayback.pendingTour)?.featureKey ? `A walkthrough is available for ${(onboardingPlayback.pendingTour || tourPlayback.pendingTour)?.featureKey}.` : "A walkthrough is available for this user." }),
9700
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontWeight: 500, marginBottom: "8px" }, children: getPendingPromptTitle(pendingPrompt) }),
9701
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: "12px", color: "#52525b", marginBottom: "12px" }, children: getPendingPromptReason(pendingPrompt) }),
9611
9702
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
9612
9703
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("button", { className: "btn btn-primary btn-sm", onClick: () => {
9613
- const preferredExperience = onboardingPlayback.pendingTour ? "onboarding" : "tour";
9614
- if (onboardingPlayback.pendingTour) {
9704
+ const preferredExperience = pendingPrompt.experienceType === "onboarding" ? "onboarding" : "tour";
9705
+ if (preferredExperience === "onboarding") {
9615
9706
  onboardingPlayback.acceptPendingTour();
9616
9707
  } else {
9617
9708
  tourPlayback.acceptPendingTour();
@@ -9619,13 +9710,13 @@ function ModelNexChatBubble({
9619
9710
  startTourListening(preferredExperience);
9620
9711
  }, children: [
9621
9712
  "Start ",
9622
- onboardingPlayback.pendingTour ? "workflow" : "tour"
9713
+ pendingPrompt.experienceType === "onboarding" ? "workflow" : "tour"
9623
9714
  ] }),
9624
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "btn btn-secondary btn-sm", onClick: onboardingPlayback.pendingTour ? onboardingPlayback.dismissPendingTour : tourPlayback.dismissPendingTour, children: "Not now" })
9715
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "btn btn-secondary btn-sm", onClick: pendingPrompt.experienceType === "onboarding" ? onboardingPlayback.dismissPendingTour : tourPlayback.dismissPendingTour, children: "Not now" })
9625
9716
  ] })
9626
9717
  ]
9627
9718
  }
9628
- ),
9719
+ ) }),
9629
9720
  tourPlayback.isActive && tourPlayback.isReviewMode && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
9630
9721
  "div",
9631
9722
  {
@@ -10204,7 +10295,7 @@ function ModelNexChatBubble({
10204
10295
  )
10205
10296
  ] })
10206
10297
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
10207
- messages.length === 0 && !loading && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
10298
+ messages.length === 0 && !loading && !renderPendingPromptInline && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
10208
10299
  "div",
10209
10300
  {
10210
10301
  style: {
package/dist/index.mjs CHANGED
@@ -2457,6 +2457,9 @@ function normalizeTrigger(trigger) {
2457
2457
  if (trigger === "feature_launch" || trigger === "feature_unlocked" || trigger === "feature_unlock" || trigger === "new_feature") {
2458
2458
  return "feature_launch";
2459
2459
  }
2460
+ if (trigger === "return_visit") {
2461
+ return "return_visit";
2462
+ }
2460
2463
  return trigger ?? "first_visit";
2461
2464
  }
2462
2465
  function getStartPolicy(tour) {
@@ -2465,10 +2468,22 @@ function getStartPolicy(tour) {
2465
2468
  function getNotificationType(tour) {
2466
2469
  return tour.notificationType ?? "bubble_card";
2467
2470
  }
2471
+ function getUserProfileFeatures(userProfile) {
2472
+ const directFeatures = Array.isArray(userProfile.features) ? userProfile.features : [];
2473
+ const nestedFeatures = Array.isArray(userProfile.tourFacts?.features) ? userProfile.tourFacts.features : [];
2474
+ return [...new Set([...directFeatures, ...nestedFeatures].filter((value) => Boolean(value)))];
2475
+ }
2468
2476
  function isTourEligible(tour, userProfile) {
2469
2477
  switch (normalizeTrigger(tour.trigger)) {
2470
2478
  case "first_visit":
2471
2479
  return !!userProfile.isNewUser;
2480
+ case "return_visit":
2481
+ return userProfile.isNewUser === false;
2482
+ case "feature_launch": {
2483
+ const featureKey = tour.featureKey?.trim();
2484
+ if (!featureKey) return false;
2485
+ return getUserProfileFeatures(userProfile).includes(featureKey);
2486
+ }
2472
2487
  case "manual":
2473
2488
  return false;
2474
2489
  default:
@@ -7908,7 +7923,7 @@ function getRecordingDraftStatusMessage(phase, experienceType) {
7908
7923
 
7909
7924
  // src/utils/tour-listening.ts
7910
7925
  function canStartTourListening(state) {
7911
- const hasLiveOrPendingPlayback = state.isTourActive || state.isOnboardingActive || state.hasPendingTour || state.hasPendingOnboarding;
7926
+ const hasLiveOrPendingPlayback = state.isTourActive || state.isOnboardingActive || state.hasPendingTour || state.hasPendingOnboarding || Boolean(state.startingExperienceType);
7912
7927
  return hasLiveOrPendingPlayback && !state.sttActive && state.sttSupported;
7913
7928
  }
7914
7929
  function resolveTourListeningExperience(preferredExperience, state) {
@@ -7985,6 +8000,58 @@ function buildTranscriptPreviewLines(transcript, options = {}) {
7985
8000
  return lines.slice(-maxLines);
7986
8001
  }
7987
8002
 
8003
+ // src/utils/pendingPromptUi.ts
8004
+ function isPendingReviewPrompt(pendingPrompt) {
8005
+ return Boolean(pendingPrompt?.options?.reviewMode);
8006
+ }
8007
+ function shouldRenderPendingPromptInline({
8008
+ pendingPrompt,
8009
+ isPlaybackActive,
8010
+ recordingMode
8011
+ }) {
8012
+ return Boolean(
8013
+ pendingPrompt && !isPlaybackActive && !recordingMode && !isPendingReviewPrompt(pendingPrompt)
8014
+ );
8015
+ }
8016
+ function shouldRenderPendingPromptModal({
8017
+ pendingPrompt,
8018
+ isPlaybackActive,
8019
+ recordingMode,
8020
+ pendingNotificationType
8021
+ }) {
8022
+ return Boolean(
8023
+ pendingPrompt && !isPlaybackActive && !recordingMode && isPendingReviewPrompt(pendingPrompt) && pendingNotificationType === "modal"
8024
+ );
8025
+ }
8026
+ function shouldAutoExpandForPendingPrompt({
8027
+ pendingPrompt,
8028
+ isPlaybackActive,
8029
+ recordingMode,
8030
+ pendingNotificationType
8031
+ }) {
8032
+ if (!pendingPrompt || isPlaybackActive || recordingMode) return false;
8033
+ return pendingNotificationType === "bubble_card" || shouldRenderPendingPromptInline({
8034
+ pendingPrompt,
8035
+ isPlaybackActive,
8036
+ recordingMode
8037
+ });
8038
+ }
8039
+ function getPendingPromptTitle(pendingPrompt) {
8040
+ return `I found a ${pendingPrompt.experienceType === "onboarding" ? "workflow" : "tour"} called "${pendingPrompt.tour.name}". Would you like to start it now?`;
8041
+ }
8042
+ function getPendingPromptReason(pendingPrompt) {
8043
+ if (pendingPrompt.tour.trigger === "first_visit") {
8044
+ return "This was surfaced because the current user matches the configured first-visit trigger.";
8045
+ }
8046
+ if (pendingPrompt.tour.trigger === "return_visit") {
8047
+ return "This was surfaced because the current user matches the configured return-visit trigger.";
8048
+ }
8049
+ if (pendingPrompt.tour.featureKey) {
8050
+ return `This was surfaced because the "${pendingPrompt.tour.featureKey}" trigger condition matched.`;
8051
+ }
8052
+ return "This was surfaced because the configured trigger condition matched.";
8053
+ }
8054
+
7988
8055
  // src/utils/floatingLiveTranscript.ts
7989
8056
  var floatingTranscriptEl = null;
7990
8057
  var liveTranscriptSuppressed = false;
@@ -8704,7 +8771,20 @@ function ModelNexChatBubble({
8704
8771
  }
8705
8772
  }, [devMode, handleAutoTag, ctx?.extractedElements.length, window.location.pathname]);
8706
8773
  const onboardingReviewToggle = getReviewModeToggleConfig(onboardingPlayback.playbackState);
8774
+ const pendingPrompt = playbackController.pendingPrompt;
8707
8775
  const pendingNotificationType = (onboardingPlayback.pendingTour || tourPlayback.pendingTour)?.notificationType ?? "bubble_card";
8776
+ const isPlaybackActive = tourPlayback.isActive || onboardingPlayback.isActive;
8777
+ const renderPendingPromptInline = shouldRenderPendingPromptInline({
8778
+ pendingPrompt,
8779
+ isPlaybackActive,
8780
+ recordingMode
8781
+ });
8782
+ const renderPendingPromptModal = shouldRenderPendingPromptModal({
8783
+ pendingPrompt,
8784
+ isPlaybackActive,
8785
+ recordingMode,
8786
+ pendingNotificationType
8787
+ });
8708
8788
  useEffect17(() => {
8709
8789
  setHydrated(true);
8710
8790
  try {
@@ -8771,10 +8851,15 @@ function ModelNexChatBubble({
8771
8851
  }
8772
8852
  }, [tourPlayback.isActive, onboardingPlayback.isActive]);
8773
8853
  useEffect17(() => {
8774
- if ((onboardingPlayback.pendingTour || tourPlayback.pendingTour) && !recordingMode && pendingNotificationType === "bubble_card") {
8854
+ if (shouldAutoExpandForPendingPrompt({
8855
+ pendingPrompt,
8856
+ isPlaybackActive,
8857
+ recordingMode,
8858
+ pendingNotificationType
8859
+ })) {
8775
8860
  setExpandedState(true);
8776
8861
  }
8777
- }, [onboardingPlayback.pendingTour, tourPlayback.pendingTour, recordingMode, pendingNotificationType, setExpandedState]);
8862
+ }, [isPlaybackActive, pendingNotificationType, pendingPrompt, recordingMode, setExpandedState]);
8778
8863
  const preferredListeningExperienceRef = useRef13(null);
8779
8864
  const playbackVoiceRoutingRef = useRef13({
8780
8865
  activeExperienceType,
@@ -8822,6 +8907,7 @@ function ModelNexChatBubble({
8822
8907
  isOnboardingActive: onboardingPlayback.isActive,
8823
8908
  hasPendingTour: Boolean(tourPlayback.pendingTour),
8824
8909
  hasPendingOnboarding: Boolean(onboardingPlayback.pendingTour),
8910
+ startingExperienceType,
8825
8911
  sttActive: sttActiveRef.current,
8826
8912
  sttSupported: voice.sttSupported
8827
8913
  };
@@ -8867,19 +8953,20 @@ function ModelNexChatBubble({
8867
8953
  tourPlayback.pendingTour,
8868
8954
  onboardingPlayback.isActive,
8869
8955
  onboardingPlayback.pendingTour,
8956
+ startingExperienceType,
8870
8957
  voice,
8871
8958
  handleVoiceTourInput,
8872
8959
  updateTourListenReady,
8873
8960
  updateTourSttError
8874
8961
  ]);
8875
8962
  useEffect17(() => {
8876
- const isPlaybackActive = isTourListeningSessionActive({
8963
+ const isPlaybackActive2 = isTourListeningSessionActive({
8877
8964
  isTourActive: tourPlayback.isActive,
8878
8965
  isOnboardingActive: onboardingPlayback.isActive,
8879
8966
  startingExperienceType
8880
8967
  });
8881
- const becameActive = isPlaybackActive && !previousTourActiveRef.current;
8882
- previousTourActiveRef.current = isPlaybackActive;
8968
+ const becameActive = isPlaybackActive2 && !previousTourActiveRef.current;
8969
+ previousTourActiveRef.current = isPlaybackActive2;
8883
8970
  if (becameActive) {
8884
8971
  try {
8885
8972
  sessionStorage.setItem(TOUR_MINIMIZED_STORAGE_KEY, "true");
@@ -8888,7 +8975,7 @@ function ModelNexChatBubble({
8888
8975
  setExpandedState(false, { rememberTourMinimize: true });
8889
8976
  updateTourSttError(null);
8890
8977
  }
8891
- if (!isPlaybackActive) {
8978
+ if (!isPlaybackActive2) {
8892
8979
  try {
8893
8980
  sessionStorage.removeItem(TOUR_MINIMIZED_STORAGE_KEY);
8894
8981
  } catch {
@@ -8915,12 +9002,12 @@ function ModelNexChatBubble({
8915
9002
  }
8916
9003
  }, [recordingMode, setExpandedState]);
8917
9004
  useEffect17(() => {
8918
- const isPlaybackActive = isTourListeningSessionActive({
9005
+ const isPlaybackActive2 = isTourListeningSessionActive({
8919
9006
  isTourActive: tourPlayback.isActive,
8920
9007
  isOnboardingActive: onboardingPlayback.isActive,
8921
9008
  startingExperienceType
8922
9009
  });
8923
- if (!isPlaybackActive) {
9010
+ if (!isPlaybackActive2) {
8924
9011
  updateTourListenReady(false);
8925
9012
  setTourLiveTranscript("");
8926
9013
  hideFloatingLiveTranscript();
@@ -8929,12 +9016,12 @@ function ModelNexChatBubble({
8929
9016
  updateTourListenReady(Boolean(voice.isListening && sttActiveRef.current));
8930
9017
  }, [tourPlayback.isActive, onboardingPlayback.isActive, voice.isListening, startingExperienceType, updateTourListenReady]);
8931
9018
  useEffect17(() => {
8932
- const isPlaybackActive = isTourListeningSessionActive({
9019
+ const isPlaybackActive2 = isTourListeningSessionActive({
8933
9020
  isTourActive: tourPlayback.isActive,
8934
9021
  isOnboardingActive: onboardingPlayback.isActive,
8935
9022
  startingExperienceType
8936
9023
  });
8937
- if (!isPlaybackActive && sttActiveRef.current) {
9024
+ if (!isPlaybackActive2 && sttActiveRef.current) {
8938
9025
  sttActiveRef.current = false;
8939
9026
  updateTourListenReady(false);
8940
9027
  updateTourSttError(null);
@@ -8944,12 +9031,12 @@ function ModelNexChatBubble({
8944
9031
  }
8945
9032
  }, [tourPlayback.isActive, onboardingPlayback.isActive, voice, startingExperienceType, updateTourListenReady, updateTourSttError]);
8946
9033
  useEffect17(() => {
8947
- const isPlaybackActive = isTourListeningSessionActive({
9034
+ const isPlaybackActive2 = isTourListeningSessionActive({
8948
9035
  isTourActive: tourPlayback.isActive,
8949
9036
  isOnboardingActive: onboardingPlayback.isActive,
8950
9037
  startingExperienceType
8951
9038
  });
8952
- if (!isPlaybackActive || tourListenReady || !voice.sttSupported || tourSttError === "not-allowed") {
9039
+ if (!isPlaybackActive2 || tourListenReady || !voice.sttSupported || tourSttError === "not-allowed") {
8953
9040
  return;
8954
9041
  }
8955
9042
  const enableTourListeningFromGesture = (event) => {
@@ -9156,8 +9243,8 @@ function ModelNexChatBubble({
9156
9243
  return createPortal(
9157
9244
  /* @__PURE__ */ jsxs3("div", { style: containerStyle, className, "data-modelnex-internal": "true", onMouseDown: stopEventPropagation, onClick: stopEventPropagation, children: [
9158
9245
  /* @__PURE__ */ jsx4("style", { children: GLOBAL_STYLES }),
9159
- !(tourPlayback.isActive || onboardingPlayback.isActive) && (tourPlayback.pendingTour || onboardingPlayback.pendingTour) && pendingNotificationType === "modal" && !recordingMode && (() => {
9160
- const pt = onboardingPlayback.pendingTour || tourPlayback.pendingTour;
9246
+ renderPendingPromptModal && (() => {
9247
+ const pt = pendingPrompt?.tour;
9161
9248
  const mc = pt?.presentation?.modalConfig || {};
9162
9249
  return /* @__PURE__ */ jsx4(
9163
9250
  "div",
@@ -9219,8 +9306,8 @@ function ModelNexChatBubble({
9219
9306
  "button",
9220
9307
  {
9221
9308
  onClick: () => {
9222
- const preferredExperience = onboardingPlayback.pendingTour ? "onboarding" : "tour";
9223
- if (onboardingPlayback.pendingTour) {
9309
+ const preferredExperience = pendingPrompt?.experienceType === "onboarding" ? "onboarding" : "tour";
9310
+ if (preferredExperience === "onboarding") {
9224
9311
  onboardingPlayback.acceptPendingTour();
9225
9312
  } else {
9226
9313
  tourPlayback.acceptPendingTour();
@@ -9383,24 +9470,28 @@ function ModelNexChatBubble({
9383
9470
  children: "Microphone access is blocked. Use the input box below, or allow microphone permission and tap the mic again."
9384
9471
  }
9385
9472
  ),
9386
- !(tourPlayback.isActive || onboardingPlayback.isActive) && (tourPlayback.pendingTour || onboardingPlayback.pendingTour) && pendingNotificationType === "bubble_card" && /* @__PURE__ */ jsxs3(
9473
+ renderPendingPromptInline && pendingPrompt && /* @__PURE__ */ jsx4("div", { style: { display: "flex", justifyContent: "flex-start" }, children: /* @__PURE__ */ jsxs3(
9387
9474
  "div",
9388
9475
  {
9389
9476
  style: {
9390
- margin: "12px",
9391
- padding: "14px",
9392
- borderRadius: "12px",
9393
- border: "1px solid rgba(59,130,246,0.18)",
9394
- background: "linear-gradient(135deg, rgba(59,130,246,0.08) 0%, rgba(59,130,246,0.03) 100%)"
9477
+ maxWidth: "85%",
9478
+ padding: "12px 16px",
9479
+ borderRadius: "var(--modelnex-radius-inner, 16px)",
9480
+ borderBottomLeftRadius: 4,
9481
+ fontSize: "14px",
9482
+ lineHeight: 1.55,
9483
+ background: "var(--modelnex-bg, #fff)",
9484
+ color: "var(--modelnex-fg, #18181b)",
9485
+ border: "1px solid var(--modelnex-border, #e4e4e7)",
9486
+ boxShadow: "0 2px 4px rgba(0,0,0,0.02)"
9395
9487
  },
9396
9488
  children: [
9397
- /* @__PURE__ */ jsx4("div", { style: { fontSize: "13px", fontWeight: 600, color: "var(--modelnex-fg, #18181b)", marginBottom: "6px" }, children: onboardingPlayback.pendingTour ? "Suggested workflow" : "Suggested tour" }),
9398
- /* @__PURE__ */ jsx4("div", { style: { fontSize: "14px", color: "var(--modelnex-fg, #27272a)", marginBottom: "6px" }, children: (onboardingPlayback.pendingTour || tourPlayback.pendingTour)?.name }),
9399
- /* @__PURE__ */ jsx4("div", { style: { fontSize: "12px", color: "#52525b", lineHeight: 1.45, marginBottom: "12px" }, children: (onboardingPlayback.pendingTour || tourPlayback.pendingTour)?.trigger === "first_visit" ? "Start a short walkthrough for this user now?" : (onboardingPlayback.pendingTour || tourPlayback.pendingTour)?.featureKey ? `A walkthrough is available for ${(onboardingPlayback.pendingTour || tourPlayback.pendingTour)?.featureKey}.` : "A walkthrough is available for this user." }),
9489
+ /* @__PURE__ */ jsx4("div", { style: { fontWeight: 500, marginBottom: "8px" }, children: getPendingPromptTitle(pendingPrompt) }),
9490
+ /* @__PURE__ */ jsx4("div", { style: { fontSize: "12px", color: "#52525b", marginBottom: "12px" }, children: getPendingPromptReason(pendingPrompt) }),
9400
9491
  /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: "8px" }, children: [
9401
9492
  /* @__PURE__ */ jsxs3("button", { className: "btn btn-primary btn-sm", onClick: () => {
9402
- const preferredExperience = onboardingPlayback.pendingTour ? "onboarding" : "tour";
9403
- if (onboardingPlayback.pendingTour) {
9493
+ const preferredExperience = pendingPrompt.experienceType === "onboarding" ? "onboarding" : "tour";
9494
+ if (preferredExperience === "onboarding") {
9404
9495
  onboardingPlayback.acceptPendingTour();
9405
9496
  } else {
9406
9497
  tourPlayback.acceptPendingTour();
@@ -9408,13 +9499,13 @@ function ModelNexChatBubble({
9408
9499
  startTourListening(preferredExperience);
9409
9500
  }, children: [
9410
9501
  "Start ",
9411
- onboardingPlayback.pendingTour ? "workflow" : "tour"
9502
+ pendingPrompt.experienceType === "onboarding" ? "workflow" : "tour"
9412
9503
  ] }),
9413
- /* @__PURE__ */ jsx4("button", { className: "btn btn-secondary btn-sm", onClick: onboardingPlayback.pendingTour ? onboardingPlayback.dismissPendingTour : tourPlayback.dismissPendingTour, children: "Not now" })
9504
+ /* @__PURE__ */ jsx4("button", { className: "btn btn-secondary btn-sm", onClick: pendingPrompt.experienceType === "onboarding" ? onboardingPlayback.dismissPendingTour : tourPlayback.dismissPendingTour, children: "Not now" })
9414
9505
  ] })
9415
9506
  ]
9416
9507
  }
9417
- ),
9508
+ ) }),
9418
9509
  tourPlayback.isActive && tourPlayback.isReviewMode && /* @__PURE__ */ jsxs3(
9419
9510
  "div",
9420
9511
  {
@@ -9993,7 +10084,7 @@ function ModelNexChatBubble({
9993
10084
  )
9994
10085
  ] })
9995
10086
  ] }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
9996
- messages.length === 0 && !loading && /* @__PURE__ */ jsx4(
10087
+ messages.length === 0 && !loading && !renderPendingPromptInline && /* @__PURE__ */ jsx4(
9997
10088
  "div",
9998
10089
  {
9999
10090
  style: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelnex/sdk",
3
- "version": "0.5.25",
3
+ "version": "0.5.27",
4
4
  "description": "React SDK for natural language control of web apps via AI agents",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",