@qualified-salesforce/web 0.0.1-oidc-bootstrap → 0.1.1

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.
Files changed (98) hide show
  1. package/CAPABILITY-REFERENCE.md +41 -0
  2. package/CHANGELOG.md +84 -0
  3. package/README.md +65 -0
  4. package/constants.generated.js +30 -2
  5. package/css/widget/multimodal_v2/index.split-css.panda.css +265 -47
  6. package/package.json +18 -4
  7. package/packs/widget/api/docking/viewport_lock.js +93 -2
  8. package/packs/widget/esm/index.d.ts +134 -0
  9. package/packs/widget/esm/index.js +18 -2
  10. package/packs/widget/esm/qualified.css +265 -47
  11. package/src/graphql-types.js +74 -1
  12. package/src/url.js +4 -1
  13. package/vendor/actioncable/connection.js +40 -3
  14. package/vendor/actioncable/connection_monitor.js +8 -0
  15. package/vendor/actioncable/consumer.js +7 -1
  16. package/vendor/actioncable/index.js +5 -2
  17. package/vendor/actioncable/sse_socket.js +262 -0
  18. package/widget/analytics.js +8 -0
  19. package/widget/api_handler/base_api_handler.js +24 -0
  20. package/widget/booker_outcome_tracker.js +58 -20
  21. package/widget/capabilities.js +190 -0
  22. package/widget/channel.js +38 -6
  23. package/widget/constants.generated.js +2 -1
  24. package/widget/multimodal_v2/components/AppLayout.js +1 -1
  25. package/widget/multimodal_v2/components/app/Foreground.js +10 -9
  26. package/widget/multimodal_v2/components/app/Input.js +8 -3
  27. package/widget/multimodal_v2/components/app/LightboxExperience.js +17 -3
  28. package/widget/multimodal_v2/components/app/Main.js +87 -31
  29. package/widget/multimodal_v2/components/app/MessengerButton.js +18 -1
  30. package/widget/multimodal_v2/components/chat/ChatInputForm.js +8 -6
  31. package/widget/multimodal_v2/components/chat/timeline/CalendarEventPickRenderer.js +2 -2
  32. package/widget/multimodal_v2/components/chat/timeline/PresentDeckRenderer.js +346 -170
  33. package/widget/multimodal_v2/components/chat/timeline/SystemRenderer.js +4 -7
  34. package/widget/multimodal_v2/components/decks/index.js +506 -52
  35. package/widget/multimodal_v2/components/helpers/content_click_payloads.js +67 -1
  36. package/widget/multimodal_v2/components/lightbox_gather/index.js +4 -4
  37. package/widget/multimodal_v2/components/meetings/MeetingBookerEntry.js +8 -1
  38. package/widget/multimodal_v2/components/meetings/MeetingOfferEmailStep.js +1 -1
  39. package/widget/multimodal_v2/components/meetings/MeetingSummaryRail.js +1 -1
  40. package/widget/multimodal_v2/components/meetings/MeetingTimeSelector.js +1 -1
  41. package/widget/multimodal_v2/components/multimodal/Controls.js +11 -3
  42. package/widget/multimodal_v2/components/multimodal/useMultimodal.js +19 -7
  43. package/widget/multimodal_v2/components/presentations/index.js +61 -0
  44. package/widget/multimodal_v2/constants.js +1 -1
  45. package/widget/multimodal_v2/images/icons.js +17 -1
  46. package/widget/multimodal_v2/index.panda.js +2 -2
  47. package/widget/multimodal_v2/locales/cs.js +8 -1
  48. package/widget/multimodal_v2/locales/da.js +8 -1
  49. package/widget/multimodal_v2/locales/de.js +8 -1
  50. package/widget/multimodal_v2/locales/en.js +8 -1
  51. package/widget/multimodal_v2/locales/en_AU.js +8 -1
  52. package/widget/multimodal_v2/locales/en_GB.js +8 -1
  53. package/widget/multimodal_v2/locales/es.js +8 -1
  54. package/widget/multimodal_v2/locales/es_LA.js +8 -1
  55. package/widget/multimodal_v2/locales/fi.js +8 -1
  56. package/widget/multimodal_v2/locales/fr.js +8 -1
  57. package/widget/multimodal_v2/locales/fr_CA.js +8 -1
  58. package/widget/multimodal_v2/locales/id.js +8 -1
  59. package/widget/multimodal_v2/locales/it.js +8 -1
  60. package/widget/multimodal_v2/locales/ja.js +8 -1
  61. package/widget/multimodal_v2/locales/ko.js +8 -1
  62. package/widget/multimodal_v2/locales/nb.js +8 -1
  63. package/widget/multimodal_v2/locales/nl.js +8 -1
  64. package/widget/multimodal_v2/locales/pl.js +8 -1
  65. package/widget/multimodal_v2/locales/pt_BR.js +8 -1
  66. package/widget/multimodal_v2/locales/ru.js +8 -1
  67. package/widget/multimodal_v2/locales/sv.js +8 -1
  68. package/widget/multimodal_v2/locales/th.js +8 -1
  69. package/widget/multimodal_v2/locales/tr.js +8 -1
  70. package/widget/multimodal_v2/locales/vi.js +8 -1
  71. package/widget/multimodal_v2/locales/zh_CN.js +8 -1
  72. package/widget/multimodal_v2/locales/zh_TW.js +8 -1
  73. package/widget/multimodal_v2/multimodal.js +5 -1
  74. package/widget/multimodal_v2/multimodal_channel.js +13 -8
  75. package/widget/multimodal_v2/signals/messages_signal.js +4 -0
  76. package/widget/multimodal_v2/stores/ai_conversation_store.js +6 -2
  77. package/widget/multimodal_v2/stores/artifact_store.js +352 -27
  78. package/widget/multimodal_v2/stores/call_store.js +40 -1
  79. package/widget/multimodal_v2/stores/call_ui_adapter.js +1 -1
  80. package/widget/multimodal_v2/stores/decks_store.js +405 -39
  81. package/widget/multimodal_v2/stores/meeting_store.js +15 -10
  82. package/widget/multimodal_v2/stores/presentation_ga_adapter.js +64 -0
  83. package/widget/multimodal_v2/stores/presentation_telemetry_store.js +556 -0
  84. package/widget/multimodal_v2/stores/presentations_store.js +1 -0
  85. package/widget/multimodal_v2/stores/root_store.js +157 -268
  86. package/widget/multimodal_v2/stores/screen_share_store.js +41 -21
  87. package/widget/multimodal_v2/stores/translation_store.js +9 -0
  88. package/widget/multimodal_v2/stores/ui_store.js +7 -7
  89. package/widget/multimodal_v2/stores/website_navigation_store.js +3 -1
  90. package/widget/multimodal_v2/text_turn_response_tracker.js +419 -0
  91. package/widget/multimodal_v2/types.js +21 -1
  92. package/widget/multimodal_v2/utils/locale.js +2 -1
  93. package/widget/multimodal_v2/utils/signals.js +35 -4
  94. package/widget/pardot_visitor.js +7 -1
  95. package/widget/server-logger.js +97 -2
  96. package/widget/tracker.js +69 -21
  97. package/widget/widget/page_mirror_factory.js +9 -1
  98. package/widget/widget/widget.js +29 -4
@@ -57,6 +57,7 @@ class PresentationsStore {
57
57
  presentation_id: pres.presentationId,
58
58
  from_seconds: Math.round(this.currentTimeSeconds.value)
59
59
  });
60
+ this.rootStore.presentationTelemetryStore.onDemoInterrupted();
60
61
  this.stopped.value = true;
61
62
  };
62
63
  this.rootStore = rootStore;
@@ -5,30 +5,32 @@ import { BoundedSet } from "../../../src/bounded-set.js";
5
5
  import { isDebugMode } from "../../../src/smart_website/utils.js";
6
6
  import { snakify } from "../../../src/utils.js";
7
7
  import { BookerIntentTracker } from "../../booker_intent_tracker.js";
8
- import { BOOKER_PATHWAY_ORIGIN, BOOKER_SURFACE } from "../../booker_outcome_tracker.js";
8
+ import { reportBookerLoadFailed, BOOKER_PATHWAY_ORIGIN, bookerPathwayOrigin, BOOKER_SURFACE } from "../../booker_outcome_tracker.js";
9
+ import { isCapabilityAllowed, messageCapability } from "../../capabilities.js";
9
10
  import { serverLogger } from "../../server-logger.js";
10
11
  import "../../telemetry/index.js";
11
12
  import { getSentryClient } from "../../widget/sentry.js";
12
13
  import { CAN_OFFER_MEETING_BOOKER_EVENT_TYPE } from "../constants.js";
13
14
  import { createActiveConversationSignal } from "../signals/active_conversation_signal.js";
14
15
  import { createMessagesSignal, isStreamableMessage, isAssistantMessage, isEventMessage } from "../signals/messages_signal.js";
16
+ import { TextTurnResponseTracker } from "../text_turn_response_tracker.js";
15
17
  import { $fromImport } from "../utils/signals.js";
16
18
  import { ArtifactStore } from "./artifact_store.js";
17
19
  import { getCallUiAdapter } from "./call_ui_adapter.js";
18
20
  import { DockingStore } from "./docking_store.js";
21
+ import { PresentationTelemetryStore } from "./presentation_telemetry_store.js";
19
22
  import { TranslationStore } from "./translation_store.js";
20
23
  import { UIStore } from "./ui_store.js";
21
24
  import { BaseStore } from "./base_store.js";
22
25
  import "../../utils/native_globals.js";
26
+ import { ConversationMode as types_ConversationMode, LoadingState as types_LoadingState, MaximizedState as types_MaximizedState, DisplayMode as types_DisplayMode, InputState as types_InputState } from "../types.js";
23
27
  import { validateEmail } from "../../../src/validation.js.js";
24
- import { ConversationMode as types_ConversationMode, LoadingState as types_LoadingState, DisplayMode as types_DisplayMode, MaximizedState as types_MaximizedState, InputState as types_InputState } from "../types.js";
25
28
  import { buildCustEvChatViewPayload, buildCustEvContentImpressionPayload } from "../components/helpers/content_click_payloads.js";
26
29
  import { esm_browser_v4 } from "../../../js/4101.js";
30
+ import { PresentationGaAdapter } from "./presentation_ga_adapter.js";
27
31
  import { metrics } from "../../telemetry/metrics.js";
28
32
 
29
33
  /* provided dependency */ var __nativeDocumentRemoveEventListener = __webpack_require__(68034).LA;
30
- /* provided dependency */ var root_store_setTimeout = __webpack_require__(68034).pS;
31
- /* provided dependency */ var root_store_clearTimeout = __webpack_require__(68034).Kj;
32
34
  /* provided dependency */ var __nativeDocumentAddEventListener = __webpack_require__(68034).Qj;
33
35
  function _extends() {
34
36
  _extends = Object.assign || function(target) {
@@ -79,6 +81,10 @@ function _object_without_properties_loose(source, excluded) {
79
81
 
80
82
 
81
83
 
84
+
85
+
86
+
87
+
82
88
 
83
89
 
84
90
 
@@ -212,132 +218,25 @@ class RootStore extends BaseStore {
212
218
  (_this_callStore_value = this.callStore.value) == null ? void 0 : _this_callStore_value.dispose();
213
219
  (_this_waitingStore_value = this.waitingStore.value) == null ? void 0 : _this_waitingStore_value.dispose();
214
220
  (_this_websiteNavigationStore_value = this.websiteNavigationStore.value) == null ? void 0 : _this_websiteNavigationStore_value.dispose();
221
+ this.presentationGaAdapter.dispose();
222
+ this.presentationTelemetryStore.dispose();
215
223
  (_this_presentationsStore_value = this.presentationsStore.value) == null ? void 0 : _this_presentationsStore_value.dispose();
216
224
  (_this_decksStore_value = this.decksStore.value) == null ? void 0 : _this_decksStore_value.dispose();
217
225
  (_this_screenShareStore_value = this.screenShareStore.value) == null ? void 0 : _this_screenShareStore_value.dispose();
218
- this.clearResponseTimeout();
219
- this.flushHeldTimeout();
220
- this.turnToken++;
226
+ this.textTurnResponseTracker.dispose();
221
227
  if (typeof document !== "undefined") {
222
228
  __nativeDocumentRemoveEventListener("visibilitychange", this.handleTurnVisibilityChange);
223
229
  }
224
230
  }
225
- startTextTurn(messageClientId) {
226
- // Before any per-turn state resets: the held metric belongs to the turn that
227
- // just timed out and reads `turnCause` when it fires.
228
- this.flushHeldTimeout();
229
- this.lastVisitorMessageSentAt = performance.now();
230
- this.turnMessageClientId = messageClientId;
231
- this.turnCounterEmitted = false;
232
- this.turnReplyHandled = false;
233
- this.turnObscured = this.isDocumentHidden();
234
- this.turnCallActive = this.callInProgress.peek();
235
- this.turnCause = null;
236
- this.turnHasToolCall = false;
237
- const token = ++this.turnToken;
238
- this.clearResponseTimeout();
239
- this.responseTimeoutId = root_store_setTimeout(()=>{
240
- this.responseTimeoutId = null;
241
- if (token !== this.turnToken || this.turnCounterEmitted) return;
242
- // Treat the turn as obscured if the tab was hidden at any point, not just
243
- // now: backgrounded tabs throttle setTimeout, so the timer often fires only
244
- // after the visitor returns (document.hidden already false). Checking the
245
- // whole wait keeps that throttling delay out of the timeout (unavailable) count.
246
- const obscured = this.turnObscured || this.isDocumentHidden();
247
- // The turn is counted from this moment, whenever the metric actually
248
- // leaves: a reply or a `textResponseEnded` arriving later must not talk us
249
- // out of a timeout the visitor has already experienced.
250
- this.turnCounterEmitted = true;
251
- if (obscured) {
252
- metrics.emitIncrement("multimodal_text_response", {
253
- status: "backgrounded",
254
- call_active: this.turnCallActiveTag
255
- });
256
- return;
257
- }
258
- // Before the breadcrumb: nothing between latching the outcome and arming the
259
- // emission, so a counted timeout cannot be lost to a failure in between.
260
- this.holdTimeoutForCause();
261
- serverLogger.serverLog({
262
- type: "log",
263
- message: "Multimodal text reply did not arrive within response budget"
264
- });
265
- }, (/* inlined export .RESPONSE_BUDGET_MS */25000));
266
- return token;
267
- }
268
- get turnCallActiveTag() {
269
- return this.turnCallActive ? "true" : "false";
270
- }
271
- /**
272
- * Holds a decided timeout's metric back briefly so the server's cause report
273
- * can land on it (see CAUSE_REPORT_GRACE_MS). Only the `cause` tag is
274
- * outstanding — the turn is already counted — so this delays an emission, it
275
- * never changes an outcome.
276
- *
277
- * Ends early when the report arrives, and is flushed rather than dropped if the
278
- * turn is superseded or the store torn down first, so a held timeout can't go
279
- * missing from the SLI.
280
- */ holdTimeoutForCause() {
281
- // Flush rather than clear: a hold here would already have been counted, and
282
- // dropping it would delete a failure from the SLI. Unreachable today (one
283
- // budget timer per turn, and starting a turn flushes), enforced here so it
284
- // stays that way.
285
- this.flushHeldTimeout();
286
- // Captured now: a turn starting during the window resets the live value.
287
- const callActive = this.turnCallActiveTag;
288
- this.heldTimeoutFlush = ()=>{
289
- var _this_turnCause;
290
- this.clearTimeoutHold();
291
- metrics.emitIncrement("multimodal_text_response", {
292
- status: "timeout",
293
- call_active: callActive,
294
- // `unknown` rather than omitted: every timeout carries a cause, so a
295
- // dashboard split by cause always adds up to the timeout total. A growing
296
- // `unknown` slice means the report is not reaching us.
297
- cause: (_this_turnCause = this.turnCause) != null ? _this_turnCause : "unknown"
298
- });
299
- };
300
- this.timeoutHoldId = root_store_setTimeout(this.heldTimeoutFlush, (/* inlined export .CAUSE_REPORT_GRACE_MS */1500));
301
- }
302
- // No-op unless a timeout is being held. Self-clearing, so it is safe to call
303
- // from every path that might race the window.
304
- flushHeldTimeout() {
305
- this.heldTimeoutFlush == null ? void 0 : this.heldTimeoutFlush.call(this);
306
- }
307
- clearTimeoutHold() {
308
- if (this.timeoutHoldId !== null) {
309
- root_store_clearTimeout(this.timeoutHoldId);
310
- this.timeoutHoldId = null;
311
- }
312
- this.heldTimeoutFlush = null;
313
- }
314
- clearResponseTimeout() {
315
- if (this.responseTimeoutId !== null) {
316
- root_store_clearTimeout(this.responseTimeoutId);
317
- this.responseTimeoutId = null;
318
- }
319
- }
320
231
  isDocumentHidden() {
321
232
  return typeof document !== "undefined" && document.hidden;
322
233
  }
323
- handleTextTurnSendResult(result, turnToken) {
324
- if (!(result == null ? void 0 : result.error)) return;
325
- if (turnToken !== this.turnToken || this.turnCounterEmitted) return;
326
- this.clearResponseTimeout();
327
- metrics.emitIncrement("multimodal_text_response", {
328
- status: "send_failed",
329
- call_active: this.turnCallActiveTag
330
- });
331
- // A failed send has no reply coming, so close the turn entirely (no latency).
332
- this.turnCounterEmitted = true;
333
- this.turnReplyHandled = true;
334
- this.turnToken++;
335
- }
336
- static isTextResponseEndedReason(reason) {
337
- return RootStore.TEXT_RESPONSE_ENDED_REASONS.includes(reason);
338
- }
339
- static isTurnCause(cause) {
340
- return RootStore.TURN_CAUSES.includes(cause);
234
+ recordArtifactVisibilityTransition(previousState, state, message, presentationEndReason) {
235
+ if (presentationEndReason === void 0) presentationEndReason = "dismissed";
236
+ if (previousState !== types_MaximizedState.ARTIFACT || state === types_MaximizedState.ARTIFACT) return;
237
+ if ((message == null ? void 0 : message.type) === "present_content" || (message == null ? void 0 : message.type) === "present_deck" || (message == null ? void 0 : message.type) === "present_video") {
238
+ this.presentationTelemetryStore.onArtifactHidden(message.id, presentationEndReason);
239
+ }
341
240
  }
342
241
  unresolvedAiSdrEmailRequest() {
343
242
  var _this_activeConversation_value;
@@ -437,7 +336,7 @@ class RootStore extends BaseStore {
437
336
  log: (params)=>serverLogger.serverLog(params)
438
337
  }), // Translation store is directly instantiated (not lazy-loaded) because
439
338
  // translations are needed immediately when the app renders
440
- this.translationStore = new TranslationStore(this), this.dockingStore = new DockingStore(this), this.artifactStore = new ArtifactStore(this), // Mutable State
339
+ this.translationStore = new TranslationStore(this), this.dockingStore = new DockingStore(this), this.presentationTelemetryStore = new PresentationTelemetryStore(this), this.artifactStore = new ArtifactStore(this), // Mutable State
441
340
  this.isVisitorBlocked = signals_core_module_d(false), /** Piper form join ID for this page view; same for all custEv_* events on the page. */ this.formJoinIDUnique = signals_core_module_d(null), /** Tracks fired custEv_contentImpression IDs; bounded to prevent unbounded growth. */ this.impressedMessageIds = new BoundedSet(RootStore.IMPRESSED_MESSAGE_IDS_MAX_SIZE), this.agentsAvailable = signals_core_module_d(false), this.companyName = signals_core_module_d(), this.disableTitleFlash = signals_core_module_d(false), this.messengerEventListener = null, this.callInProgress = signals_core_module_w(()=>{
442
341
  var _this_callStore_value;
443
342
  return ((_this_callStore_value = this.callStore.value) == null ? void 0 : _this_callStore_value.dailyCall.value) != null;
@@ -445,20 +344,9 @@ class RootStore extends BaseStore {
445
344
  // via the multimodal voice greeting setting). When true, the initial greeting
446
345
  // can be multiple sentences and varies per team, which affects greeting-sensitive
447
346
  // timing metrics. Used to tag those metrics so dashboards can filter them out.
448
- this.hasCustomVoiceGreeting = false, this.pounceTrackingLoadStarted = false, this.lastVisitorMessageSentAt = null, this.turnCounterEmitted = false, // Separate from the counter: lets a late reply still record latency after the
449
- // timer resolved availability.
450
- this.turnReplyHandled = false, this.responseTimeoutId = null, // A timeout that has been counted but whose metric is still waiting on the
451
- // server's cause report (see holdTimeoutForCause).
452
- this.timeoutHoldId = null, this.heldTimeoutFlush = null, // Dedups a late reply against a turn the timer already resolved.
453
- this.turnToken = 0, // clientId of the message that opened the current turn; used to ignore a
454
- // stale `textResponseEnded` event from a turn we've already superseded.
455
- this.turnMessageClientId = null, // Latest cause the server reported for the outstanding turn, if any. Read only
456
- // when the turn times out.
457
- this.turnCause = null, // Whether the agent reached for a tool on this turn, which costs the call plus
458
- // another model round trip to use what it returned. Reported as it happens, so a
459
- // turn that never finishes is still on the right side of the split.
460
- this.turnHasToolCall = false, // Tab hidden mid-turn → bucket as `backgrounded` (not a false `timeout`) and skip its latency.
461
- this.turnObscured = false, this.turnCallActive = false, this.conversations = signals_core_module_d([]), this.messages = createMessagesSignal(), this.pounceCandidates = signals_core_module_d(null), this.isVideoLoading = signals_core_module_d(false), this.isBlurredVideoActive = signals_core_module_d(false), /**
347
+ this.hasCustomVoiceGreeting = false, this.pounceTrackingLoadStarted = false, this.textTurnResponseTracker = new TextTurnResponseTracker({
348
+ isCurrentlyHidden: ()=>this.isDocumentHidden()
349
+ }), this.conversations = signals_core_module_d([]), this.messages = createMessagesSignal(), this.pounceCandidates = signals_core_module_d(null), this.isVideoLoading = signals_core_module_d(false), this.isBlurredVideoActive = signals_core_module_d(false), /**
462
350
  * Which provider's track Coffee has committed to as the avatar video for this call.
463
351
  * Null until Coffee reports one (an older Coffee never will), which callers treat as
464
352
  * "fall back to the per-team flag".
@@ -634,9 +522,7 @@ class RootStore extends BaseStore {
634
522
  data
635
523
  });
636
524
  }, this.handleTurnVisibilityChange = ()=>{
637
- if (this.isDocumentHidden() && this.lastVisitorMessageSentAt !== null && !this.turnReplyHandled) {
638
- this.turnObscured = true;
639
- }
525
+ if (this.isDocumentHidden()) this.textTurnResponseTracker.onHidden();
640
526
  }, // Actions — delegated to UIStore
641
527
  this.setUIState = (uiState)=>{
642
528
  this.uiStore.setUIState(uiState);
@@ -652,11 +538,25 @@ class RootStore extends BaseStore {
652
538
  this.uiStore.toggleVideoMode(mode);
653
539
  }, this.setMaximizedState = (state)=>{
654
540
  this.uiStore.setMaximizedState(state);
541
+ }, this.endPresentationAndSetMaximizedState = (state, reason)=>{
542
+ if (reason === void 0) reason = "dismissed";
543
+ const previousState = this.maximizedState.peek();
544
+ const artifactMessage = this.artifact.message.peek();
545
+ this.uiStore.setMaximizedState(state);
546
+ this.recordArtifactVisibilityTransition(previousState, this.maximizedState.peek(), artifactMessage, reason);
655
547
  }, this.clearMaximizedState = (param)=>{
656
548
  let { when } = param === void 0 ? {} : param;
657
549
  this.uiStore.clearMaximizedState({
658
550
  when
659
551
  });
552
+ }, this.endPresentationAndClearMaximizedState = (param)=>{
553
+ let { when, reason } = param === void 0 ? {} : param;
554
+ const previousState = this.maximizedState.peek();
555
+ const artifactMessage = this.artifact.message.peek();
556
+ this.uiStore.clearMaximizedState({
557
+ when
558
+ });
559
+ this.recordArtifactVisibilityTransition(previousState, this.maximizedState.peek(), artifactMessage, reason);
660
560
  }, this.sendTypingEvent = (content)=>{
661
561
  this.channel.perform("update_visitor_typing", {
662
562
  content
@@ -718,62 +618,16 @@ class RootStore extends BaseStore {
718
618
  if (payload.type === "text") {
719
619
  this.recordPrivacyDisclosureConsent(id);
720
620
  }
721
- // Known limitation: we track a single outstanding turn (first-message-wins).
722
- // If a turn times out and the visitor sends again before its late reply lands,
723
- // that reply is attributed to the newer turn (replies carry no link to the
724
- // visitor message they answer, so we can't match them by anything but timing).
725
- // Rare (needs a >budget reply plus a resend in the gap) and self-correcting in
726
- // aggregate, so we accept it rather than track per-message turns.
727
- const startedTurnToken = isVisitorAction && (this.lastVisitorMessageSentAt === null || this.turnCounterEmitted) ? this.startTextTurn(id) : null;
621
+ const textTurnHandle = isVisitorAction ? this.textTurnResponseTracker.begin({
622
+ clientId: id,
623
+ callActive: this.callInProgress.peek(),
624
+ initiallyHidden: this.isDocumentHidden()
625
+ }) : null;
728
626
  const result = await this.channel.perform("send_message", snakify(message));
729
- if (startedTurnToken !== null) {
730
- this.handleTextTurnSendResult(result, startedTurnToken);
627
+ if (textTurnHandle !== null) {
628
+ this.textTurnResponseTracker.handleSendResult(textTurnHandle, result);
731
629
  }
732
630
  return result;
733
- }, /**
734
- * Backend closed a turn with no reply on purpose (guardrail block, rep
735
- * takeover, blank output, ...). Close the turn under a distinct status so the
736
- * budget timer doesn't fire a false `timeout` and dashboards can exclude it
737
- * from the success rate. Guarded on `turnCounterEmitted` so a turn already
738
- * resolved by a real reply (or the timer) is left untouched.
739
- */ this.handleTextResponseEnded = (payload)=>{
740
- var _ref;
741
- if (this.lastVisitorMessageSentAt === null || this.turnCounterEmitted) return;
742
- // Ignore a late event from a turn we've already superseded: it must belong
743
- // to the message that opened the current outstanding turn.
744
- if ((payload == null ? void 0 : payload.clientId) !== this.turnMessageClientId) return;
745
- const reason = (_ref = payload == null ? void 0 : payload.reason) != null ? _ref : "";
746
- const status = RootStore.isTextResponseEndedReason(reason) ? reason : "suppressed";
747
- this.clearResponseTimeout();
748
- metrics.emitIncrement("multimodal_text_response", _extends({
749
- status,
750
- call_active: this.turnCallActiveTag
751
- }, this.turnCause ? {
752
- cause: this.turnCause
753
- } : {}));
754
- // No reply is coming, so close the turn entirely (no latency recorded).
755
- this.turnCounterEmitted = true;
756
- this.turnReplyHandled = true;
757
- this.turnToken++;
758
- }, /**
759
- * The server telling us what this turn is spending its time on, sent when the
760
- * response budget expires (and again afterwards for turns that finished slow).
761
- * Purely an annotation: it never resolves or ends a turn, it only supplies the
762
- * `cause` tag the timeout would otherwise lack.
763
- */ this.handleTextTurnDiagnostics = (payload)=>{
764
- var _ref;
765
- if ((payload == null ? void 0 : payload.clientId) !== this.turnMessageClientId) return;
766
- if ((payload == null ? void 0 : payload.hasToolCall) === true) this.turnHasToolCall = true;
767
- // A tool report carries no cause and arrives mid-turn, so it must not release a
768
- // held timeout: that would emit the metric before the cause it is waiting for.
769
- const cause = (_ref = payload == null ? void 0 : payload.cause) != null ? _ref : "";
770
- if (!RootStore.isTurnCause(cause)) return;
771
- // Preserve the first concrete explanation for this client turn; `unknown`
772
- // is a placeholder and may be replaced when a real cause arrives.
773
- this.turnCause = this.turnCause && this.turnCause !== "unknown" ? this.turnCause : cause;
774
- // This report is what a held timeout was waiting for; no reason to sit out
775
- // the rest of the window.
776
- this.flushHeldTimeout();
777
631
  }, this.sendVisitorText = async (text, extra)=>{
778
632
  this.sendTextError.value = null;
779
633
  if (this.featureFlags.piperXCookieConsent && this.needsCookieConsent.value) {
@@ -859,8 +713,6 @@ class RootStore extends BaseStore {
859
713
  signals_core_module_r(()=>{
860
714
  this.showCookieConsentModal.value = false;
861
715
  this.pendingMessage.value = null;
862
- // Clearing the pending call also discards any stashed trigger context, so a
863
- // later regular call is never mistaken for a presentation start.
864
716
  this.pendingCallStart.value = null;
865
717
  this.setDisplayMode(types_DisplayMode.CLOSED);
866
718
  });
@@ -919,7 +771,7 @@ class RootStore extends BaseStore {
919
771
  }, /** Hands an incoming message to the overlay. See ArtifactStore#queueArtifactMessage. */ this.queueArtifactMessage = (message, options)=>{
920
772
  if (options === void 0) options = {};
921
773
  return this.artifactStore.queueArtifactMessage(message, options);
922
- }, /** Re-opens an artifact the visitor asked for from the timeline. */ this.presentArtifactMessage = (message)=>this.artifactStore.presentArtifactMessage(message), this.revisitDeckSlide = (message, slideIndex, pageTitle)=>this.artifactStore.revisitDeckSlide(message, slideIndex, pageTitle), this.clearArtifact = ()=>{
774
+ }, /** Re-opens an artifact the visitor asked for from the timeline. */ this.presentArtifactMessage = (message)=>this.artifactStore.presentArtifactMessage(message), this.revisitDeckSlide = (message, slideIndex, pageTitle)=>this.artifactStore.revisitDeckSlide(message, slideIndex, pageTitle), /** Opens a deck for the visitor to step through at their own pace. */ this.presentSelfGuidedDeck = (message)=>this.artifactStore.presentSelfGuidedDeck(message), this.clearArtifact = ()=>{
923
775
  this.artifactStore.clearArtifact();
924
776
  }, this.clearInputMessage = (param)=>{
925
777
  let { when } = param === void 0 ? {} : param;
@@ -946,7 +798,7 @@ class RootStore extends BaseStore {
946
798
  }
947
799
  }, this.startCall = async (startCallContext, displayState)=>{
948
800
  if (displayState === void 0) displayState = "open";
949
- var _this_pounceTrackingStore_value, _this_callStore_value;
801
+ var _this_pounceTrackingStore_value;
950
802
  if (this.conversationMode.text.peek()) {
951
803
  return;
952
804
  }
@@ -966,6 +818,7 @@ class RootStore extends BaseStore {
966
818
  this.showCookieConsentModal.value = true;
967
819
  return;
968
820
  }
821
+ this.presentationTelemetryStore.recordPendingTrigger(startCallContext != null ? startCallContext : null);
969
822
  this.callInitiatedAt = performance.now();
970
823
  this.messengerEventListener == null ? void 0 : this.messengerEventListener.call(this, "callRequested");
971
824
  (_this_pounceTrackingStore_value = this.pounceTrackingStore.value) == null ? void 0 : _this_pounceTrackingStore_value.notifyCallRequested();
@@ -979,14 +832,41 @@ class RootStore extends BaseStore {
979
832
  });
980
833
  this.applyStartCallDisplayState(displayState);
981
834
  });
982
- const callStore = this.callStore.value;
983
- if (!callStore) {
984
- await this.initializeVoiceAndVideo();
835
+ // Both ways into a deck presentation come through here — the deck's timeline
836
+ // card and the self-guided viewer's switch to voice — so the deck is put into
837
+ // its starting state once, here, rather than at either call site.
838
+ //
839
+ // Only for `open`, and after the display state has been applied: the other two
840
+ // positions have no overlay to show it in. `minimized` runs the call as
841
+ // picture-in-picture and has just cleared the overlay to get there, and
842
+ // `maximized` gives the overlay's slot to the call UI itself.
843
+ //
844
+ // Awaited: its watch has to be installed before the lifecycle below moves, or
845
+ // a start that fails first is never seen to have begun. It cannot hold us up.
846
+ if (displayState === "open") {
847
+ await this.artifactStore.startDeckPresentation(startCallContext == null ? void 0 : startCallContext.trigger);
985
848
  }
986
- const dailyCall = (_this_callStore_value = this.callStore.value) == null ? void 0 : _this_callStore_value.dailyCall.value;
987
- if (!dailyCall) {
988
- var _this_callStore_value1;
989
- (_this_callStore_value1 = this.callStore.value) == null ? void 0 : _this_callStore_value1.aiConversationStore.startConversation(false, startCallContext != null ? startCallContext : null);
849
+ try {
850
+ var _this_callStore_value;
851
+ const callStore = this.callStore.value;
852
+ if (!callStore) {
853
+ await this.initializeVoiceAndVideo();
854
+ }
855
+ const dailyCall = (_this_callStore_value = this.callStore.value) == null ? void 0 : _this_callStore_value.dailyCall.value;
856
+ if (!dailyCall) {
857
+ var _this_callStore_value1;
858
+ (_this_callStore_value1 = this.callStore.value) == null ? void 0 : _this_callStore_value1.aiConversationStore.startConversation(false, startCallContext != null ? startCallContext : null);
859
+ }
860
+ } catch (error) {
861
+ // The one way a start can end without the watch seeing it end: the call
862
+ // chunk failing to load leaves `isVideoLoading` raised by
863
+ // #initializeVoiceAndVideo's first line and nothing behind it to lower it,
864
+ // so the watch has seen a start begin that never finishes and the deadline
865
+ // stays unarmed behind a `connecting` that stays true. Every other failure
866
+ // brings the flag back down and is torn down by the watch itself. Rethrown
867
+ // so a caller that does look at this still sees what happened.
868
+ this.artifactStore.endDeckPresentation();
869
+ throw error;
990
870
  }
991
871
  }, /**
992
872
  * Position the widget for a call that is about to start.
@@ -997,7 +877,7 @@ class RootStore extends BaseStore {
997
877
  * closed, so it has to be cleared as well.
998
878
  */ this.applyStartCallDisplayState = (displayState)=>{
999
879
  if (displayState === "minimized") {
1000
- this.clearMaximizedState();
880
+ this.endPresentationAndClearMaximizedState();
1001
881
  this.setDisplayMode(types_DisplayMode.CLOSED);
1002
882
  return;
1003
883
  }
@@ -1008,7 +888,7 @@ class RootStore extends BaseStore {
1008
888
  // Maximizing centers the multimodal UI, which collides with the custom entry
1009
889
  // points voice-only setups render, so it is video-only.
1010
890
  if (displayState === "maximized" && this.conversationMode.video.peek()) {
1011
- this.setMaximizedState(types_MaximizedState.MULTIMODAL);
891
+ this.endPresentationAndSetMaximizedState(types_MaximizedState.MULTIMODAL);
1012
892
  }
1013
893
  }, this.resetToStartingPosition = ()=>{
1014
894
  this.uiStore.resetToStartingPosition();
@@ -1086,11 +966,10 @@ class RootStore extends BaseStore {
1086
966
  * - SIDEBAR → CLOSED
1087
967
  */ this.cancelCallOrConnection = async (param)=>{
1088
968
  let { closeWidget = true } = param === void 0 ? {} : param;
1089
- // Reset maximized state when ending call
1090
- this.clearMaximizedState();
1091
969
  // Handle debug mode - reset the scenario store
1092
970
  const debugStore = this.debugScenarioStore.value;
1093
971
  if (this.isDebugMultimodal.value && debugStore) {
972
+ this.endPresentationAndClearMaximizedState();
1094
973
  debugStore.reset();
1095
974
  if (closeWidget) {
1096
975
  this.closeWidget();
@@ -1101,15 +980,20 @@ class RootStore extends BaseStore {
1101
980
  const callStore = this.callStore.value;
1102
981
  const aiConversationStore = callStore == null ? void 0 : callStore.aiConversationStore;
1103
982
  const dailyCall = callStore == null ? void 0 : callStore.dailyCall.value;
983
+ const isCallBoundary = Boolean(aiConversationStore == null ? void 0 : aiConversationStore.conversation.value) || Boolean(dailyCall) || this.isVideoLoading.peek();
984
+ const presentationEndReason = isCallBoundary ? "call_ended" : "dismissed";
1104
985
  // Always reset the loading state first to prevent any pending async operations
1105
986
  // from continuing (e.g., tavusStore.handleStartResponse checks this flag)
1106
987
  this.isVideoLoading.value = false;
1107
988
  this.isBlurredVideoActive.value = false;
1108
989
  this.activeAvatarProvider.value = null;
1109
990
  this.setTypingIndicator(false);
1110
- // End the tavus conversation if one exists
1111
- if (aiConversationStore == null ? void 0 : aiConversationStore.conversation.value) {
1112
- await aiConversationStore.endConversation();
991
+ this.endPresentationAndClearMaximizedState({
992
+ reason: presentationEndReason
993
+ });
994
+ // The backend does not echo callEnded for visitor-initiated ends.
995
+ if (callStore && isCallBoundary) {
996
+ await callStore.endCall();
1113
997
  }
1114
998
  // Leave any active Daily call
1115
999
  if (dailyCall) {
@@ -1156,8 +1040,8 @@ class RootStore extends BaseStore {
1156
1040
  }),
1157
1041
  visitorUnblocked: ()=>this.isVisitorBlocked.value = false,
1158
1042
  botProcessEnded: ()=>this.handleBotProcessEnded(),
1159
- textResponseEnded: (payload)=>this.handleTextResponseEnded(payload),
1160
- textTurnDiagnostics: (payload)=>this.handleTextTurnDiagnostics(payload),
1043
+ textResponseEnded: (payload)=>this.textTurnResponseTracker.handleResponseEnded(payload),
1044
+ textTurnDiagnostics: (payload)=>this.textTurnResponseTracker.handleDiagnostic(payload),
1161
1045
  toolCallStarted: (payload)=>{
1162
1046
  var _ref;
1163
1047
  this.activeToolCall.value = (_ref = payload == null ? void 0 : payload.toolName) != null ? _ref : null;
@@ -1194,7 +1078,7 @@ class RootStore extends BaseStore {
1194
1078
  this.elementConversationTextMessage.value = null;
1195
1079
  this.closeLightbox();
1196
1080
  });
1197
- this.meetingStore.load();
1081
+ this.loadBookerForElementOffer(offerMsg);
1198
1082
  } else {
1199
1083
  signals_core_module_r(()=>{
1200
1084
  this.openMessengerForElementConversation();
@@ -1315,7 +1199,41 @@ class RootStore extends BaseStore {
1315
1199
  this.elementConversationTextMessage.value = null;
1316
1200
  this.closeLightbox();
1317
1201
  });
1318
- this.meetingStore.load();
1202
+ this.loadBookerForElementOffer(offerMessage);
1203
+ }, /**
1204
+ * Loads the booker for an element (form/CTA) offer already on screen.
1205
+ *
1206
+ * Channel handlers cannot await, so the rejection needs catching here or it
1207
+ * reaches the host page. Worth reporting because this surface has no card to
1208
+ * fall back to: the lightbox gates its meeting phase on the store being
1209
+ * ready, so a failed import just leaves the visitor on the spinner.
1210
+ */ this.loadBookerForElementOffer = (offer)=>{
1211
+ this.meetingStore.load().catch((error)=>{
1212
+ var _getSentryClient;
1213
+ (_getSentryClient = getSentryClient({
1214
+ project: "multimodal"
1215
+ })) == null ? void 0 : _getSentryClient.captureException(error);
1216
+ this.reportBookerLoadFailure(offer, "element");
1217
+ });
1218
+ }, /**
1219
+ * Records an offer whose booker chunk never arrived as a `failed` outcome.
1220
+ *
1221
+ * The watchdog that normally resolves this SLI lives in `meetingStore`, inside
1222
+ * the chunk that did not load, so the offer would go unrecorded.
1223
+ *
1224
+ * @param conversationType The caller's pathway, which each knows statically.
1225
+ * Not read from `elementConversationOffer` here: this runs in an async
1226
+ * catch, by which point that signal may be cleared or hold another offer,
1227
+ * either of which mislabels the offer being reported.
1228
+ */ this.reportBookerLoadFailure = (offer, conversationType)=>{
1229
+ reportBookerLoadFailed({
1230
+ surface: BOOKER_SURFACE.MULTIMODAL,
1231
+ offerMessageId: offer.id,
1232
+ pathwayOrigin: bookerPathwayOrigin(conversationType, offer.source),
1233
+ conversationType,
1234
+ // No `observeBookerOutcome` hand-off: it acts on `shown` and ignores the rest.
1235
+ log: (params)=>serverLogger.serverLog(params)
1236
+ });
1319
1237
  }, this.handleElementConversationTextMessage = (payload)=>{
1320
1238
  var _ref, _ref1, _ref2;
1321
1239
  const title = (_ref = payload == null ? void 0 : payload.title) != null ? _ref : "";
@@ -1475,6 +1393,10 @@ class RootStore extends BaseStore {
1475
1393
  if (stateUpdate.hasCustomVoiceGreeting !== undefined) {
1476
1394
  this.hasCustomVoiceGreeting = stateUpdate.hasCustomVoiceGreeting;
1477
1395
  }
1396
+ // Conversation#end! broadcasts this singular field rather than `conversations`.
1397
+ if (stateUpdate.activeConversation === null) {
1398
+ this.presentationTelemetryStore.onConversationEnded();
1399
+ }
1478
1400
  if (stateUpdate.conversations) {
1479
1401
  stateUpdate.conversations.forEach((conversation)=>{
1480
1402
  this.updateConversation(conversation);
@@ -1514,44 +1436,10 @@ class RootStore extends BaseStore {
1514
1436
  metrics.emitTiming("multimodal_first_pounce_ms", deltaMs);
1515
1437
  this.pounceResponseStartTimestamp = null;
1516
1438
  }
1517
- if (isAssistantMessage(incomingMessage) && !incomingMessage.isPounce && this.lastVisitorMessageSentAt !== null && !this.turnReplyHandled) {
1518
- this.clearResponseTimeout();
1519
- const deltaMs = performance.now() - this.lastVisitorMessageSentAt;
1520
- const withinBudget = deltaMs <= (/* inlined export .RESPONSE_BUDGET_MS */25000);
1521
- const obscured = this.turnObscured || this.isDocumentHidden();
1522
- // Within budget the wait is short enough to trust regardless of visibility;
1523
- // over budget we only trust the timing if the tab stayed foreground,
1524
- // since a hidden tab can throttle delivery.
1525
- if (withinBudget || !obscured) {
1526
- metrics.emitTiming("multimodal_text_response_ms", deltaMs, {
1527
- call_active: this.turnCallActiveTag,
1528
- has_tool_call: this.turnHasToolCall ? "true" : "false"
1529
- });
1530
- }
1531
- if (!this.turnCounterEmitted) {
1532
- var _this_turnCause;
1533
- const status = withinBudget ? "success" : obscured ? "backgrounded" : "timeout";
1534
- metrics.emitIncrement("multimodal_text_response", _extends({
1535
- status,
1536
- call_active: this.turnCallActiveTag
1537
- }, status === "timeout" ? {
1538
- cause: (_this_turnCause = this.turnCause) != null ? _this_turnCause : "unknown"
1539
- } : {}));
1540
- this.turnCounterEmitted = true;
1541
- } else {
1542
- // The 25s timer already fired and counted this turn as a timeout.
1543
- // Log the late reply so we can distinguish "AI responded slowly" from
1544
- // "reply never arrived" when investigating timeout events.
1545
- serverLogger.serverLog({
1546
- type: "log",
1547
- message: "Multimodal text reply arrived after response budget (late)",
1548
- extra: {
1549
- context: "elapsed_ms:" + Math.round(deltaMs)
1550
- }
1551
- });
1552
- }
1553
- this.turnReplyHandled = true;
1554
- this.turnToken++;
1439
+ if (isAssistantMessage(incomingMessage) && !incomingMessage.isPounce) {
1440
+ this.textTurnResponseTracker.handleAssistantReply({
1441
+ currentlyHidden: this.isDocumentHidden()
1442
+ });
1555
1443
  }
1556
1444
  this.addMessage(incomingMessage, clientMessageIndex);
1557
1445
  if (isAssistantMessage(incomingMessage)) {
@@ -1581,6 +1469,11 @@ class RootStore extends BaseStore {
1581
1469
  }
1582
1470
  }, this.handleIncomingMessage = (incomingMessage, param)=>{
1583
1471
  let { wasHiddenByPlaceholder = false } = param === void 0 ? {} : param;
1472
+ // Capability boundary (QAI-5862): drop a message routed to a capability the consumer denied at
1473
+ // setup(). Gating at this single dispatch also blocks the second path navigate_content takes
1474
+ // into the nav store (via the artifact queue). No-op for the hosted widget (allow-all).
1475
+ const capability = messageCapability(incomingMessage.type);
1476
+ if (capability && !isCapabilityAllowed(capability)) return;
1584
1477
  switch(incomingMessage.type){
1585
1478
  case "calendar_event_offer":
1586
1479
  // Closes the pre-offer SLI here, at message *receipt*, which is where the
@@ -1868,6 +1761,19 @@ class RootStore extends BaseStore {
1868
1761
  // Pounce tracking is non-critical - fail silently
1869
1762
  });
1870
1763
  };
1764
+ this.presentationGaAdapter = new PresentationGaAdapter({
1765
+ telemetry: this.presentationTelemetryStore,
1766
+ isEnabled: ()=>{
1767
+ var _this_config_featureFlags, _this_config;
1768
+ return !!((_this_config = this.config) == null ? void 0 : (_this_config_featureFlags = _this_config.featureFlags) == null ? void 0 : _this_config_featureFlags.piperWebTrackingEvents);
1769
+ },
1770
+ conversationId: ()=>this.piperConversationId,
1771
+ formJoinIDUnique: ()=>{
1772
+ var _this_formJoinIDUnique_value;
1773
+ return (_this_formJoinIDUnique_value = this.formJoinIDUnique.value) != null ? _this_formJoinIDUnique_value : "";
1774
+ },
1775
+ emit: (event)=>this.channel.emitPiperWebTrackingEvent(event)
1776
+ });
1871
1777
  // WS message handlers
1872
1778
  for (const [event, handler] of Object.entries(this.messageHandlers)){
1873
1779
  this.channel.on(event, handler);
@@ -1889,23 +1795,6 @@ RootStore.IMPRESSED_MESSAGE_IDS_MAX_SIZE = 100;
1889
1795
  * placeholder, so the visitor can read the transcript text that just filled it
1890
1796
  * before the overlay covers the page.
1891
1797
  */ RootStore.MEETING_BOOKER_PLACEHOLDER_DELAY_MS = 1000;
1892
- // Kept in sync with MultimodalAgentWorker#broadcast_text_response_ended.
1893
- RootStore.TEXT_RESPONSE_ENDED_REASONS = [
1894
- "suppressed",
1895
- "blank_output"
1896
- ];
1897
- // Kept in sync with AiConversations::TurnCause::CAUSES.
1898
- RootStore.TURN_CAUSES = [
1899
- "queue_wait",
1900
- "setup",
1901
- "llm_slow",
1902
- "agent_loop",
1903
- "tool_slow",
1904
- "side_llm_wait",
1905
- "generation_failed",
1906
- "output_moderation_retry",
1907
- "unknown"
1908
- ];
1909
1798
  const RootStoreContext = Q(null);
1910
1799
  /**
1911
1800
  * Please avoid using this hook as much as possible!!!