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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CAPABILITY-REFERENCE.md +44 -0
  2. package/CHANGELOG.md +75 -0
  3. package/constants.generated.js +30 -2
  4. package/css/widget/multimodal_v2/index.split-css.panda.css +265 -47
  5. package/package.json +17 -3
  6. package/packs/widget/api/docking/viewport_lock.js +93 -2
  7. package/packs/widget/esm/index.d.ts +134 -0
  8. package/packs/widget/esm/index.js +18 -2
  9. package/packs/widget/esm/qualified.css +265 -47
  10. package/src/graphql-types.js +74 -1
  11. package/src/url.js +4 -1
  12. package/vendor/actioncable/connection.js +40 -3
  13. package/vendor/actioncable/connection_monitor.js +8 -0
  14. package/vendor/actioncable/consumer.js +7 -1
  15. package/vendor/actioncable/index.js +5 -2
  16. package/vendor/actioncable/sse_socket.js +262 -0
  17. package/widget/analytics.js +8 -0
  18. package/widget/api_handler/base_api_handler.js +24 -0
  19. package/widget/booker_outcome_tracker.js +58 -20
  20. package/widget/capabilities.js +190 -0
  21. package/widget/channel.js +38 -6
  22. package/widget/constants.generated.js +2 -1
  23. package/widget/multimodal_v2/components/AppLayout.js +1 -1
  24. package/widget/multimodal_v2/components/app/Foreground.js +10 -9
  25. package/widget/multimodal_v2/components/app/Input.js +8 -3
  26. package/widget/multimodal_v2/components/app/LightboxExperience.js +17 -3
  27. package/widget/multimodal_v2/components/app/Main.js +87 -31
  28. package/widget/multimodal_v2/components/app/MessengerButton.js +18 -1
  29. package/widget/multimodal_v2/components/chat/ChatInputForm.js +8 -6
  30. package/widget/multimodal_v2/components/chat/timeline/CalendarEventPickRenderer.js +2 -2
  31. package/widget/multimodal_v2/components/chat/timeline/PresentDeckRenderer.js +346 -170
  32. package/widget/multimodal_v2/components/chat/timeline/SystemRenderer.js +4 -7
  33. package/widget/multimodal_v2/components/decks/index.js +506 -52
  34. package/widget/multimodal_v2/components/helpers/content_click_payloads.js +67 -1
  35. package/widget/multimodal_v2/components/lightbox_gather/index.js +4 -4
  36. package/widget/multimodal_v2/components/meetings/MeetingBookerEntry.js +8 -1
  37. package/widget/multimodal_v2/components/meetings/MeetingOfferEmailStep.js +1 -1
  38. package/widget/multimodal_v2/components/meetings/MeetingSummaryRail.js +1 -1
  39. package/widget/multimodal_v2/components/meetings/MeetingTimeSelector.js +1 -1
  40. package/widget/multimodal_v2/components/multimodal/Controls.js +11 -3
  41. package/widget/multimodal_v2/components/multimodal/useMultimodal.js +19 -7
  42. package/widget/multimodal_v2/components/presentations/index.js +61 -0
  43. package/widget/multimodal_v2/constants.js +1 -1
  44. package/widget/multimodal_v2/images/icons.js +17 -1
  45. package/widget/multimodal_v2/index.panda.js +2 -2
  46. package/widget/multimodal_v2/locales/cs.js +8 -1
  47. package/widget/multimodal_v2/locales/da.js +8 -1
  48. package/widget/multimodal_v2/locales/de.js +8 -1
  49. package/widget/multimodal_v2/locales/en.js +8 -1
  50. package/widget/multimodal_v2/locales/en_AU.js +8 -1
  51. package/widget/multimodal_v2/locales/en_GB.js +8 -1
  52. package/widget/multimodal_v2/locales/es.js +8 -1
  53. package/widget/multimodal_v2/locales/es_LA.js +8 -1
  54. package/widget/multimodal_v2/locales/fi.js +8 -1
  55. package/widget/multimodal_v2/locales/fr.js +8 -1
  56. package/widget/multimodal_v2/locales/fr_CA.js +8 -1
  57. package/widget/multimodal_v2/locales/id.js +8 -1
  58. package/widget/multimodal_v2/locales/it.js +8 -1
  59. package/widget/multimodal_v2/locales/ja.js +8 -1
  60. package/widget/multimodal_v2/locales/ko.js +8 -1
  61. package/widget/multimodal_v2/locales/nb.js +8 -1
  62. package/widget/multimodal_v2/locales/nl.js +8 -1
  63. package/widget/multimodal_v2/locales/pl.js +8 -1
  64. package/widget/multimodal_v2/locales/pt_BR.js +8 -1
  65. package/widget/multimodal_v2/locales/ru.js +8 -1
  66. package/widget/multimodal_v2/locales/sv.js +8 -1
  67. package/widget/multimodal_v2/locales/th.js +8 -1
  68. package/widget/multimodal_v2/locales/tr.js +8 -1
  69. package/widget/multimodal_v2/locales/vi.js +8 -1
  70. package/widget/multimodal_v2/locales/zh_CN.js +8 -1
  71. package/widget/multimodal_v2/locales/zh_TW.js +8 -1
  72. package/widget/multimodal_v2/multimodal.js +5 -1
  73. package/widget/multimodal_v2/multimodal_channel.js +13 -8
  74. package/widget/multimodal_v2/signals/messages_signal.js +4 -0
  75. package/widget/multimodal_v2/stores/ai_conversation_store.js +6 -2
  76. package/widget/multimodal_v2/stores/artifact_store.js +352 -27
  77. package/widget/multimodal_v2/stores/call_store.js +40 -1
  78. package/widget/multimodal_v2/stores/call_ui_adapter.js +1 -1
  79. package/widget/multimodal_v2/stores/decks_store.js +405 -39
  80. package/widget/multimodal_v2/stores/meeting_store.js +15 -10
  81. package/widget/multimodal_v2/stores/presentation_ga_adapter.js +64 -0
  82. package/widget/multimodal_v2/stores/presentation_telemetry_store.js +556 -0
  83. package/widget/multimodal_v2/stores/presentations_store.js +1 -0
  84. package/widget/multimodal_v2/stores/root_store.js +157 -268
  85. package/widget/multimodal_v2/stores/screen_share_store.js +41 -21
  86. package/widget/multimodal_v2/stores/translation_store.js +9 -0
  87. package/widget/multimodal_v2/stores/ui_store.js +7 -7
  88. package/widget/multimodal_v2/stores/website_navigation_store.js +3 -1
  89. package/widget/multimodal_v2/text_turn_response_tracker.js +419 -0
  90. package/widget/multimodal_v2/types.js +21 -1
  91. package/widget/multimodal_v2/utils/locale.js +2 -1
  92. package/widget/multimodal_v2/utils/signals.js +35 -4
  93. package/widget/pardot_visitor.js +7 -1
  94. package/widget/server-logger.js +97 -2
  95. package/widget/tracker.js +69 -21
  96. package/widget/widget/page_mirror_factory.js +9 -1
  97. package/widget/widget/widget.js +29 -4
@@ -1,13 +1,33 @@
1
1
  import { __webpack_require__ } from "../../../js/runtime.js";
2
- import { signals_core_module_r, signals_core_module_d } from "../../../js/multimodal_v2.js";
2
+ import { signals_core_module_d, signals_core_module_w, signals_core_module_E, signals_core_module_r } from "../../../js/multimodal_v2.js";
3
+ import { getSentryClient } from "../../widget/sentry.js";
3
4
  import { BaseStore } from "./base_store.js";
4
5
  import "../../utils/native_globals.js";
5
- import { MaximizedState as types_MaximizedState } from "../types.js";
6
+ import { MaximizedState as types_MaximizedState, StartCallTrigger as types_StartCallTrigger } from "../types.js";
6
7
 
7
8
  /* provided dependency */ var artifact_store_setTimeout = __webpack_require__(68034).pS;
9
+ /* provided dependency */ var artifact_store_clearTimeout = __webpack_require__(68034).Kj;
10
+ function _extends() {
11
+ _extends = Object.assign || function(target) {
12
+ for(var i = 1; i < arguments.length; i++){
13
+ var source = arguments[i];
14
+ for(var key in source){
15
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
16
+ target[key] = source[key];
17
+ }
18
+ }
19
+ }
20
+ return target;
21
+ };
22
+ return _extends.apply(this, arguments);
23
+ }
24
+
8
25
 
9
26
 
10
27
 
28
+ // Expiry closes the overlay rather than giving the deck back: handing it back
29
+ // paints the self-guided viewer while Piper's share may still be seconds away.
30
+ const DECK_PRESENTATION_START_TIMEOUT_MS = 30000;
11
31
  /**
12
32
  * Owns artifact orchestration: presenting slides / presentation videos / the
13
33
  * meeting booker in the maximized ARTIFACT overlay, navigating the host page
@@ -48,7 +68,17 @@ import { MaximizedState as types_MaximizedState } from "../types.js";
48
68
  this.markPending(message.id);
49
69
  try {
50
70
  if (delayMs > 0) await new Promise((resolve)=>artifact_store_setTimeout(resolve, delayMs));
51
- await this.presentArtifactMessage(message);
71
+ await this.presentArtifactMessage(message, {
72
+ origin: "incoming"
73
+ });
74
+ } catch (error) {
75
+ var _getSentryClient;
76
+ (_getSentryClient = getSentryClient({
77
+ project: "multimodal"
78
+ })) == null ? void 0 : _getSentryClient.captureException(error);
79
+ if (message.type === "calendar_event_offer") {
80
+ this.rootStore.reportBookerLoadFailure(message, "chat");
81
+ }
52
82
  } finally{
53
83
  // Safe to release the slot now: on the way through here the artifact id has
54
84
  // been set, so the renderer is already withholding it for that reason
@@ -64,19 +94,20 @@ import { MaximizedState as types_MaximizedState } from "../types.js";
64
94
  * Unlike {@link queueArtifactMessage} this leaves the timeline alone, for the
65
95
  * visitor asking to (re)open an artifact from a card that is already on screen:
66
96
  * hiding it mid-load would blank the spot they just clicked.
67
- */ this.presentArtifactMessage = async (message)=>{
97
+ */ this.presentArtifactMessage = async (message, param)=>{
98
+ let { origin = "revisit" } = param === void 0 ? {} : param;
68
99
  switch(message.type){
69
100
  case "present_content":
70
101
  await this.rootStore.slidesStore.load();
71
102
  // Mirror the on-screen slide into the voice agent's perception loop
72
103
  // (ScreenShareStore ships it via the update_perception channel action).
73
104
  this.loadSlidePerception();
74
- this.showArtifact(message);
105
+ this.showArtifact(message, origin);
75
106
  break;
76
107
  case "present_video":
77
108
  if (this.rootStore.callInProgress.value) {
78
109
  await this.rootStore.presentationsStore.load();
79
- if (!this.hiddenArtifactIds.has(message.id)) this.showArtifact(message);
110
+ if (!this.hiddenArtifactIds.has(message.id)) this.showArtifact(message, origin);
80
111
  }
81
112
  break;
82
113
  case "present_deck":
@@ -98,7 +129,7 @@ import { MaximizedState as types_MaximizedState } from "../types.js";
98
129
  // (ScreenShareStore ships it via the update_perception channel action).
99
130
  // Gated with the present so we only mirror a live, on-screen slide.
100
131
  this.loadSlidePerception();
101
- this.showArtifact(message);
132
+ this.showArtifact(message, origin);
102
133
  }
103
134
  }
104
135
  break;
@@ -107,7 +138,7 @@ import { MaximizedState as types_MaximizedState } from "../types.js";
107
138
  // element-conversation lightbox is a separate flow; see
108
139
  // RootStore#elementConversationOffer.)
109
140
  await this.rootStore.meetingStore.load();
110
- this.showArtifact(message);
141
+ this.showArtifact(message, origin);
111
142
  break;
112
143
  case "navigate_content":
113
144
  // The one kind that never reaches the overlay: it drives the host page
@@ -124,7 +155,108 @@ import { MaximizedState as types_MaximizedState } from "../types.js";
124
155
  // is the last share, whose `slides` may not include this slide, so the store
125
156
  // can't look the title up from it.
126
157
  store.prepareRevisit(message.id, slideIndex, pageTitle);
127
- this.showArtifact(message);
158
+ this.showArtifact(message, "revisit");
159
+ }, // card. Like revisitDeckSlide this bypasses the call gate above — the visitor
160
+ // asked for it, so it is legal off-call — and goes straight to showArtifact so
161
+ // a deck they dismissed earlier can be reopened.
162
+ //
163
+ // Nothing is reported back: no channel action for the agent and no analytics
164
+ // event, so a visitor reading the deck on their own is invisible to Piper and
165
+ // does not count as a presentation. Known gap, not yet a product decision.
166
+ this.presentSelfGuidedDeck = async (message)=>{
167
+ const store = await this.rootStore.decksStore.load();
168
+ store.prepareSelfGuidedPlayback(message.id);
169
+ this.showArtifact(message, "revisit");
170
+ }, /**
171
+ * Puts the deck the visitor is handing over into its starting state: their
172
+ * slide held on screen, dimmed, with the viewer's controls dropped, while the
173
+ * call that will present it starts (CurrentDeck#starting).
174
+ *
175
+ * Called from RootStore#startCall, which is where both ways into a deck
176
+ * presentation meet — the deck's timeline card and the self-guided viewer's
177
+ * switch to voice both reach it through the call UI adapter — so neither has to
178
+ * know this exists. Nothing happens for any other trigger, `StartPresentation`
179
+ * included: a product demo starts from a card, not from a frame of video the
180
+ * visitor was already watching, so there is nothing of theirs to hold.
181
+ *
182
+ * Routed through `presentSelfGuidedDeck`'s door rather than
183
+ * `presentArtifactMessage`'s, and that is the point: the call does not exist
184
+ * yet, so this is an off-call view of a deck, which is exactly what the
185
+ * self-guided route is for. The guided route's `callInProgress` gate stays as
186
+ * it is — it guards Piper presenting, and she is not presenting yet.
187
+ *
188
+ * Awaited by its caller, so it is written to be safe to wait on: see
189
+ * `loadDecksForHandOff`. It resolves either way and never rejects.
190
+ */ this.startDeckPresentation = async (trigger)=>{
191
+ var _ref, _ref1;
192
+ if ((trigger == null ? void 0 : trigger.type) !== types_StartCallTrigger.StartDeck) return;
193
+ const store = await this.loadDecksForHandOff();
194
+ if (!store) return;
195
+ // The self-guided viewer is the only thing that puts a deck slide in front of
196
+ // the visitor off-call, so whether it is open is the whole of the question
197
+ // "which slide were they on". Open: the one it is showing, read from the
198
+ // viewer rather than from the trigger so this says what it means. Closed:
199
+ // they were looking at the deck's timeline card, whose preview is the deck's
200
+ // cover, so the cover is where the deck opens.
201
+ const viewer = this.selfGuidedDeckOnScreen(trigger.deckId);
202
+ const message = (_ref = viewer == null ? void 0 : viewer.message) != null ? _ref : this.latestDeckShare(trigger.deckId);
203
+ if (!message) return;
204
+ store.prepareStartingPresentation(message.id, (_ref1 = viewer == null ? void 0 : viewer.currentDeck.currentIndex) != null ? _ref1 : 0);
205
+ // Already the artifact in the switch-to-voice case, where the visitor is
206
+ // looking at this very share: the starting state is then a state change on
207
+ // what is on screen, with nothing to open and nothing to swap. Skipped rather
208
+ // than re-shown so it does not read as a revisit of itself, which is a
209
+ // lifecycle event (showArtifact) and not what happened.
210
+ if (this.current.messageId.peek() !== message.id) {
211
+ this.showArtifact(message, "revisit");
212
+ }
213
+ this.watchDeckPresentation();
214
+ }, /**
215
+ * The decks chunk on the only terms a call start can afford to wait on: a fetch
216
+ * that rejects, or that is still in flight when the hand-off it feeds would have
217
+ * expired anyway, gives up and lets the call start without the overlay.
218
+ *
219
+ * The visitor asked for a call, and the held slide is a courtesy on top of it.
220
+ * Bounded by the hand-off's own deadline rather than a second number, because
221
+ * that is the same wait: a chunk that has not landed by then has no hand-off
222
+ * left to set up.
223
+ */ this.loadDecksForHandOff = async ()=>{
224
+ let abandon;
225
+ const bound = new Promise((resolve)=>{
226
+ abandon = artifact_store_setTimeout(()=>resolve(null), DECK_PRESENTATION_START_TIMEOUT_MS);
227
+ });
228
+ try {
229
+ return await Promise.race([
230
+ this.rootStore.decksStore.load().catch(()=>null),
231
+ bound
232
+ ]);
233
+ } finally{
234
+ artifact_store_clearTimeout(abandon);
235
+ }
236
+ }, /**
237
+ * Gives the deck back to the visitor: the viewer returns, at the slide they
238
+ * handed over, and its controls come back with it.
239
+ *
240
+ * The whole of giving up, because nothing was taken apart to get here — the
241
+ * starting state is a flag over a viewer that stayed exactly where it was. From
242
+ * the timeline card that leaves them in the deck they asked to be shown, able
243
+ * to read it themselves, which is the same place the card's own "view deck"
244
+ * affordance would have put them and a better answer than closing the overlay
245
+ * on a deck they just asked for. Both entry points therefore land in one state,
246
+ * and the messenger behind it holds the "voice and video are temporarily
247
+ * unavailable" notice that explains why nobody is presenting
248
+ * (AiConversationStore#failoverToText).
249
+ *
250
+ * Idempotent, and the only way out that has to be: the other two — Piper's
251
+ * share arriving, the overlay closing — clear the visitor's playback state
252
+ * wholesale and take this with it (DecksStore#clearSelfGuided).
253
+ *
254
+ * For a start that failed, and only that: the wait running out closes the
255
+ * overlay instead, and deliberately does not come through here.
256
+ */ this.endDeckPresentation = ()=>{
257
+ var _this_rootStore_decksStore_value;
258
+ this.stopWatchingDeckPresentation();
259
+ (_this_rootStore_decksStore_value = this.rootStore.decksStore.value) == null ? void 0 : _this_rootStore_decksStore_value.cancelStartingPresentation();
128
260
  }, /**
129
261
  * Tears down the overlay for whatever artifact is in it. Deliberately says
130
262
  * nothing about the meeting booker: clearing the artifact message is the whole
@@ -132,37 +264,89 @@ import { MaximizedState as types_MaximizedState } from "../types.js";
132
264
  * has. Meeting-aware teardown — which also has the element conversation to let
133
265
  * go of — belongs to `closeMeetingBooker` and `clearMeetingArtifact`.
134
266
  */ this.clearArtifact = ()=>{
267
+ var _this_rootStore_decksStore_value, _this_rootStore_decksStore_value1;
135
268
  const artifactMessage = this.current.message.value;
136
269
  const closedArtifact = this.rootStore.maximizedState.value === types_MaximizedState.ARTIFACT && ((artifactMessage == null ? void 0 : artifactMessage.type) === "present_video" || (artifactMessage == null ? void 0 : artifactMessage.type) === "present_deck") ? artifactMessage : null;
270
+ // A deck the visitor was driving themselves is theirs to close. Piper isn't
271
+ // presenting it, so the agent must not hear about it below and apologise for
272
+ // a presentation it never gave.
273
+ const closedSelfGuided = (closedArtifact == null ? void 0 : closedArtifact.type) === "present_deck" && Boolean((_this_rootStore_decksStore_value = this.rootStore.decksStore.value) == null ? void 0 : _this_rootStore_decksStore_value.isSelfGuided(closedArtifact.id));
274
+ // A single slide the visitor pulled back up from the transcript is a review of
275
+ // something already shown, so it reports no progress: the deck message it
276
+ // targets is the latest share, which may already hold the whole run's
277
+ // progress, and a review of slide 2 would overwrite that with slides 0-2.
278
+ const closedRevisit = (closedArtifact == null ? void 0 : closedArtifact.type) === "present_deck" && Boolean((_this_rootStore_decksStore_value1 = this.rootStore.decksStore.value) == null ? void 0 : _this_rootStore_decksStore_value1.isRevisiting(closedArtifact.id));
279
+ // How far the presentation got, read before the teardown below: that clears
280
+ // the overlay's message id, and `currentDeck` is derived from it.
281
+ const closedDeckProgress = (closedArtifact == null ? void 0 : closedArtifact.type) === "present_deck" && !closedSelfGuided && !closedRevisit ? this.deckProgress(closedArtifact) : null;
137
282
  if (closedArtifact) {
138
283
  this.hiddenArtifactIds.add(closedArtifact.id);
139
284
  }
285
+ const presentationMessage = (artifactMessage == null ? void 0 : artifactMessage.type) === "present_content" || (artifactMessage == null ? void 0 : artifactMessage.type) === "present_video" || (artifactMessage == null ? void 0 : artifactMessage.type) === "present_deck" ? artifactMessage : null;
286
+ if (presentationMessage) {
287
+ this.rootStore.presentationTelemetryStore.onVisitorDismiss(presentationMessage.id);
288
+ }
140
289
  this.rootStore.clearMaximizedState({
141
290
  when: types_MaximizedState.ARTIFACT
142
291
  });
143
292
  // Closing the presentation overlay mid-call: let the agent know so it stops
144
293
  // narrating and acknowledges that the visitor dismissed the presentation.
145
- if (closedArtifact && this.rootStore.callInProgress.value) {
146
- this.channel.perform("notify_artifact_closed", {
294
+ if (closedArtifact && !closedSelfGuided && this.rootStore.callInProgress.value) {
295
+ this.channel.perform("notify_artifact_closed", _extends({
147
296
  artifact_type: closedArtifact.type === "present_video" ? "presentation" : "deck",
148
297
  artifact_message_id: closedArtifact.id
149
- });
298
+ }, closedDeckProgress != null ? closedDeckProgress : {}));
150
299
  }
151
- }, /** Silently hides the live deck/demo targeted by Piper's hide event. */ this.hideArtifact = (artifactMessageId)=>{
300
+ }, /**
301
+ * How far a guided deck had got when the visitor closed it: the slide on screen
302
+ * and the sequence it belongs to, reported exactly as a barge-in reports them
303
+ * (DecksStore#notifyInterruption). The server splits the sequence at the
304
+ * on-screen slide to record what was really presented, and clears the deck's
305
+ * `awaiting_narration` in the same write.
306
+ *
307
+ * Deliberately without the barge-in guards (auto-advancing, more than one
308
+ * slide, not already on the last one). Those exist because resuming is all a
309
+ * barge-in reports for, and there is nothing to resume in those cases. A close
310
+ * also has to clear `awaiting_narration`, and a deck left carrying it absorbs
311
+ * the next assistant transcript into itself, marking its whole payload
312
+ * presented (Internal::PyperxController#absorb_deck_narration!) — which is
313
+ * exactly what a single-slide deck or a close on the last slide would hit. So
314
+ * every close reports.
315
+ */ this.deckProgress = (message)=>{
316
+ var _message_slides;
152
317
  var _this_rootStore_decksStore_value;
318
+ const current = (_this_rootStore_decksStore_value = this.rootStore.decksStore.value) == null ? void 0 : _this_rootStore_decksStore_value.currentDeck.value;
319
+ if (!current) return null;
320
+ const sequence = ((_message_slides = message.slides) != null ? _message_slides : []).map((slide)=>slide.slideIndex);
321
+ return {
322
+ deck_id: message.deckId,
323
+ current_slide_index: current.currentIndex,
324
+ // A legacy share carries no `slides`, but it still put one slide on screen.
325
+ // Standing in for the sequence keeps it non-empty, which is what the server
326
+ // needs before it will record anything at all.
327
+ sequence_slide_indexes: sequence.length > 0 ? sequence : [
328
+ current.currentIndex
329
+ ]
330
+ };
331
+ }, /** Silently hides the live deck/demo targeted by Piper's hide event. */ this.hideArtifact = (artifactMessageId)=>{
332
+ var _this_rootStore_decksStore_value, _this_rootStore_decksStore_value1;
153
333
  // Record the hide before checking the current overlay. The matching artifact
154
334
  // may still be waiting on a lazy store; when that wait resolves,
155
335
  // presentArtifactMessage checks this set and does not open stale content.
156
336
  this.hiddenArtifactIds.add(artifactMessageId);
157
337
  const artifactMessage = this.current.message.value;
338
+ // The visitor's own view of the deck — a single revisited slide, or the deck
339
+ // they are stepping through — belongs to them, not to Piper's presentation.
340
+ const isMatchingRevisit = (artifactMessage == null ? void 0 : artifactMessage.id) === artifactMessageId && artifactMessage.type === "present_deck" && Boolean(((_this_rootStore_decksStore_value = this.rootStore.decksStore.value) == null ? void 0 : _this_rootStore_decksStore_value.isRevisiting(artifactMessageId)) || ((_this_rootStore_decksStore_value1 = this.rootStore.decksStore.value) == null ? void 0 : _this_rootStore_decksStore_value1.isSelfGuided(artifactMessageId)));
341
+ if (!isMatchingRevisit) {
342
+ this.rootStore.presentationTelemetryStore.onAgentHide(artifactMessageId);
343
+ }
158
344
  if (this.rootStore.maximizedState.value !== types_MaximizedState.ARTIFACT) return;
159
345
  if ((artifactMessage == null ? void 0 : artifactMessage.id) !== artifactMessageId) return;
160
346
  if (artifactMessage.type !== "present_video" && artifactMessage.type !== "present_deck") return;
161
347
  // A visitor may reopen an old deck slide from the timeline after Piper has
162
348
  // finished. A late/replayed completion event must not close that review.
163
- if (artifactMessage.type === "present_deck" && ((_this_rootStore_decksStore_value = this.rootStore.decksStore.value) == null ? void 0 : _this_rootStore_decksStore_value.isRevisiting(artifactMessageId))) {
164
- return;
165
- }
349
+ if (isMatchingRevisit) return;
166
350
  // Do not call clearArtifact: that path represents a visitor dismissal and
167
351
  // notifies Piper when a video is closed mid-presentation.
168
352
  this.rootStore.clearMaximizedState({
@@ -184,20 +368,161 @@ import { MaximizedState as types_MaximizedState } from "../types.js";
184
368
  const pending = new Set(this.pending.peek());
185
369
  if (!pending.delete(messageId)) return;
186
370
  this.pending.value = pending;
187
- }, this.showArtifact = (message)=>{
371
+ }, this.deckPresentationWatch = null, this.deckPresentationTimer = null, /** Starts the wait's clock once per hand-off. Not a signal: nothing renders it. */ this.deckPresentationDeadlineArmed = false, /**
372
+ * Whether the deck's starting state is still waiting on the call, as opposed to
373
+ * on the presentation — the boundary between its two messages
374
+ * (components/decks/index.tsx reads this for its text).
375
+ *
376
+ * `isVideoLoading` because that is the signal the call UI's own connecting state
377
+ * is drawn from: useMultimodal's `isConnecting` is exactly this, and it is what
378
+ * decides whether "Connecting you to {name}" — the same string this overlay
379
+ * borrows — is on screen in the multimodal component. Bound to the same signal
380
+ * rather than to one that merely correlates with it, so the two surfaces cannot
381
+ * disagree about whether the call is still coming up. They did: `isAudioConnected`
382
+ * (coffee's `is_audio_ready`, which is a VAD signal) lags the moment the call UI
383
+ * stops saying connecting, and Piper could be heard talking over an overlay that
384
+ * still read "connecting".
385
+ *
386
+ * The second term is the window before there is a call at all, which
387
+ * `isVideoLoading` does not cover on its own: RootStore#startCall raises it in
388
+ * the same tick it opens this overlay, so today no frame ever renders with a
389
+ * call-less deck — but stated rather than relied on, because "no call yet" being
390
+ * part of connecting is a property of this state and not of the order of two
391
+ * statements in another file. It cannot disagree with the call UI either: with no
392
+ * call there is nothing there to call ready. Once `isVideoLoading` has gone false
393
+ * the call object exists by construction (CallStore raises it on coffee's own
394
+ * status), so this term never fires again afterwards.
395
+ */ this.deckPresentationConnecting = signals_core_module_w(()=>{
396
+ const { state } = this.rootStore.getCallUiAdapter();
397
+ return state.isVideoLoading.value || !state.isInCall.value;
398
+ }), /**
399
+ * The starting state's life is the call start it is waiting on, so that is what
400
+ * is watched rather than any one failure. `isVideoLoading` is the widget's
401
+ * existing "a call is starting" flag (RootStore, raised by
402
+ * AiConversationStore#startConversation) and `isInCall` is the call itself, so
403
+ * neither being true means the attempt is over and no call came out of it —
404
+ * which is every way a start can fail at once, the room-creation failover behind
405
+ * `failoverToText` included, rather than a list of them that the next failure
406
+ * path would not be on.
407
+ *
408
+ * The same watch starts the wait's clock, off the label's own condition
409
+ * (`deckPresentationConnecting`): the label follows it live, the clock once.
410
+ */ this.watchDeckPresentation = ()=>{
411
+ this.stopWatchingDeckPresentation();
412
+ // Neither flag is up yet on this first run: startCall opens the overlay before
413
+ // it reaches the store that starts connecting. So the attempt has to be seen
414
+ // to begin before its absence can mean it ended.
415
+ let startSeen = false;
416
+ this.deckPresentationWatch = signals_core_module_E(()=>{
417
+ var _this_rootStore_decksStore_value_currentDeck_value, _this_rootStore_decksStore_value;
418
+ // Ended by something that had no reason to tell this watch: Piper's own
419
+ // share replacing the message, or the overlay closing. Both clear the
420
+ // visitor's playback state and the flag with it
421
+ // (DecksStore#clearSelfGuided), leaving nothing to wait for.
422
+ if (!((_this_rootStore_decksStore_value = this.rootStore.decksStore.value) == null ? void 0 : (_this_rootStore_decksStore_value_currentDeck_value = _this_rootStore_decksStore_value.currentDeck.value) == null ? void 0 : _this_rootStore_decksStore_value_currentDeck_value.starting)) {
423
+ void Promise.resolve().then(this.stopWatchingDeckPresentation);
424
+ return;
425
+ }
426
+ // Read through the adapter, which is what already derives these for the call
427
+ // UI, rather than re-deriving them from the call store. Fetched inside the
428
+ // effect because the adapter is built per call and swaps under debug mode;
429
+ // its signals are the stores' own either way.
430
+ const { state } = this.rootStore.getCallUiAdapter();
431
+ const starting = state.isVideoLoading.value;
432
+ const inCall = state.isInCall.value;
433
+ // Once, on the first time the call reads as connected: a reconnect renews
434
+ // neither the promise of a presentation nor the deadline on it.
435
+ if (!this.deckPresentationConnecting.value && !this.deckPresentationDeadlineArmed) {
436
+ this.deckPresentationDeadlineArmed = true;
437
+ this.armDeckPresentationTimeout();
438
+ }
439
+ if (starting || inCall) {
440
+ startSeen = true;
441
+ return;
442
+ }
443
+ if (!startSeen) return;
444
+ // Off the effect deliberately: ending the starting state disposes this very
445
+ // effect, and disposing an effect from inside its own callback is not
446
+ // something @preact/signals promises to survive.
447
+ void Promise.resolve().then(this.endDeckPresentation);
448
+ });
449
+ }, this.armDeckPresentationTimeout = ()=>{
450
+ if (this.deckPresentationTimer !== null) artifact_store_clearTimeout(this.deckPresentationTimer);
451
+ this.deckPresentationTimer = artifact_store_setTimeout(()=>{
452
+ var _decksStore_currentDeck_peek;
453
+ this.deckPresentationTimer = null;
454
+ // The watch clears this timer a microtask after the state it reads changes,
455
+ // so a fire that beat the clear must not close an arrived presentation.
456
+ const decksStore = this.rootStore.decksStore.peek();
457
+ if (!(decksStore == null ? void 0 : (_decksStore_currentDeck_peek = decksStore.currentDeck.peek()) == null ? void 0 : _decksStore_currentDeck_peek.starting)) return;
458
+ this.stopWatchingDeckPresentation();
459
+ // The message goes unconditionally, not as the close's side effect: it is
460
+ // what clears the flag, through the deck store's own subscription.
461
+ signals_core_module_r(()=>{
462
+ this.rootStore.clearMaximizedState({
463
+ when: types_MaximizedState.ARTIFACT
464
+ });
465
+ this.rootStore.setUIState({
466
+ artifactMessageId: null
467
+ });
468
+ });
469
+ }, DECK_PRESENTATION_START_TIMEOUT_MS);
470
+ }, /**
471
+ * Stands the watch and its clock down and forgets the phase, without touching
472
+ * the deck. Separate from `endDeckPresentation` because the starting state can
473
+ * end without this store deciding it should — see the effect above.
474
+ */ this.stopWatchingDeckPresentation = ()=>{
475
+ this.deckPresentationWatch == null ? void 0 : this.deckPresentationWatch.call(this);
476
+ this.deckPresentationWatch = null;
477
+ // Every route out of the starting state passes through here, so the next
478
+ // hand-off cannot inherit a deadline the last one already spent.
479
+ this.deckPresentationDeadlineArmed = false;
480
+ if (this.deckPresentationTimer !== null) {
481
+ artifact_store_clearTimeout(this.deckPresentationTimer);
482
+ this.deckPresentationTimer = null;
483
+ }
484
+ }, /** The latest share of a deck that has a card of its own, as its card picks one. */ this.latestDeckShare = (deckId)=>{
485
+ const messages = this.rootStore.messages.peek();
486
+ for(let i = messages.length - 1; i >= 0; i--){
487
+ const message = messages[i];
488
+ if ((message == null ? void 0 : message.type) === "present_deck" && message.deckId === deckId && !message.navigationOnly) {
489
+ return message;
490
+ }
491
+ }
492
+ return null;
493
+ }, /** The deck the visitor is stepping through themselves, if that is what is on screen. */ this.selfGuidedDeckOnScreen = (deckId)=>{
494
+ if (this.rootStore.maximizedState.peek() !== types_MaximizedState.ARTIFACT) return null;
495
+ const message = this.current.message.peek();
496
+ if ((message == null ? void 0 : message.type) !== "present_deck" || message.deckId !== deckId) return null;
497
+ const decksStore = this.rootStore.decksStore.value;
498
+ if (!(decksStore == null ? void 0 : decksStore.isSelfGuided(message.id))) return null;
499
+ const currentDeck = decksStore.currentDeck.peek();
500
+ return currentDeck ? {
501
+ message,
502
+ currentDeck
503
+ } : null;
504
+ }, this.showArtifact = (message, origin)=>{
505
+ const replacedMessage = this.current.message.peek();
506
+ const incomingPresentation = origin === "incoming" && (message.type === "present_content" || message.type === "present_video" || message.type === "present_deck");
507
+ if (((replacedMessage == null ? void 0 : replacedMessage.id) !== message.id || origin === "revisit") && !incomingPresentation && ((replacedMessage == null ? void 0 : replacedMessage.type) === "present_content" || (replacedMessage == null ? void 0 : replacedMessage.type) === "present_video" || (replacedMessage == null ? void 0 : replacedMessage.type) === "present_deck")) {
508
+ this.rootStore.presentationTelemetryStore.onArtifactHidden(replacedMessage.id, "replaced");
509
+ }
188
510
  if (this.current.messageId.value === message.id) {
189
511
  this.rootStore.setMaximizedState(types_MaximizedState.ARTIFACT);
190
- return;
191
- }
192
- signals_core_module_r(()=>{
193
- this.rootStore.setUIState({
194
- artifactMessageId: message.id
512
+ } else {
513
+ signals_core_module_r(()=>{
514
+ this.rootStore.setUIState({
515
+ artifactMessageId: message.id
516
+ });
517
+ this.rootStore.setMaximizedState(types_MaximizedState.ARTIFACT);
518
+ if (this.rootStore.displayMode.minimized.value && !this.rootStore.conversationMode.video.value) {
519
+ this.rootStore.setOpenedDisplayMode();
520
+ }
195
521
  });
196
- this.rootStore.setMaximizedState(types_MaximizedState.ARTIFACT);
197
- if (this.rootStore.displayMode.minimized.value && !this.rootStore.conversationMode.video.value) {
198
- this.rootStore.setOpenedDisplayMode();
199
- }
200
- });
522
+ }
523
+ if (origin === "incoming" && (message.type === "present_content" || message.type === "present_video" || message.type === "present_deck")) {
524
+ this.rootStore.presentationTelemetryStore.onLiveArtifactVisible(message);
525
+ }
201
526
  };
202
527
  }
203
528
  }
@@ -155,6 +155,14 @@ class CallStore {
155
155
  if (dailyWsProxyUrl) {
156
156
  dailyConfig.proxyUrl = dailyWsProxyUrl;
157
157
  }
158
+ // QAI-5864 (deferred): this is where the future "voice" capability guard belongs — an
159
+ // `isCapabilityAllowed("voice")` check gating `createCallObject`/`join` is what stops
160
+ // daily-js from contacting its `c.daily.co` fallback origin. It is a no-op for today's
161
+ // text-only npm package: `setup()` never defines the call-machine URL there, so the
162
+ // `selfHosted && !localBundleUrl` throw above already fires before this line and no Daily
163
+ // connection is established. The guard (plus a "voice" entry in CAPABILITY_KEYS /
164
+ // CAPABILITY_CONFIG_ENABLERS with piperXAgentVoice/piperXAgentAvatar) lands with the voice
165
+ // build, per capabilities.ts.
158
166
  this.persistentCallObject = Ha.createCallObject({
159
167
  dailyConfig
160
168
  });
@@ -260,6 +268,11 @@ class CallStore {
260
268
  this.isSilentRestart = signals_core_module_d(false);
261
269
  this.voiceGreetingStartTimestamp = null;
262
270
  this.isVisitorSpeaking = signals_core_module_d(false);
271
+ this.isBotSpeaking = signals_core_module_d(false);
272
+ // Monotonic Coffee marker emitted on the first audio byte of each distinct
273
+ // realtime response. Unlike isBotSpeaking, this advances even when a tool
274
+ // preamble and its continuation play without an audible silence gap.
275
+ this.voiceAnswerSequence = signals_core_module_d(null);
263
276
  this.audioAnalyser = signals_core_module_d(null);
264
277
  this.audioContext = null;
265
278
  this.currentAudioTrackId = null;
@@ -382,6 +395,10 @@ class CallStore {
382
395
  status
383
396
  });
384
397
  };
398
+ this.endCall = async ()=>{
399
+ this.endPresentationTelemetry();
400
+ await this.aiConversationStore.endConversation();
401
+ };
385
402
  this.leaveCall = async ()=>{
386
403
  const call = this.dailyCall.value;
387
404
  if (!call) return;
@@ -446,6 +463,7 @@ class CallStore {
446
463
  this.handleCoffeeStateUpdate = (state)=>{
447
464
  this.logCoffeeTimingMetrics(state);
448
465
  signals_core_module_r(()=>{
466
+ var _state_voice_answer_seq;
449
467
  var _this_perceivedTracker, _this_perceivedTracker1, _this_perceivedTracker2, _this_perceivedTracker3;
450
468
  // Participant IDs
451
469
  if (state.agent_participant_id) {
@@ -486,6 +504,8 @@ class CallStore {
486
504
  }
487
505
  // Speaking state
488
506
  this.rootStore.setTypingIndicator(state.is_bot_speaking);
507
+ this.isBotSpeaking.value = state.is_bot_speaking;
508
+ this.voiceAnswerSequence.value = (_state_voice_answer_seq = state.voice_answer_seq) != null ? _state_voice_answer_seq : null;
489
509
  this.isVisitorSpeaking.value = state.is_user_speaking;
490
510
  (_this_perceivedTracker = this.perceivedTracker) == null ? void 0 : _this_perceivedTracker.setUserSpeaking(state.is_user_speaking);
491
511
  (_this_perceivedTracker1 = this.perceivedTracker) == null ? void 0 : _this_perceivedTracker1.setBotSpeaking(state.is_bot_speaking);
@@ -494,6 +514,7 @@ class CallStore {
494
514
  });
495
515
  // Terminal states (outside batch since they're async)
496
516
  if (state.is_rep_joined && state.call_status === "ended") {
517
+ this.endPresentationTelemetry();
497
518
  this.handleSwitchToText();
498
519
  } else if (state.call_status === "restarting") {
499
520
  if (this.rootStore.isVideoLoading.value || this.dailyCall.value) {
@@ -502,11 +523,20 @@ class CallStore {
502
523
  }
503
524
  };
504
525
  this.handleCallEnded = async ()=>{
526
+ this.endPresentationTelemetry();
505
527
  this.rootStore.setTypingIndicator(false);
506
528
  this.teardownAudioAnalyser();
529
+ this.resetRealtimeSpeechState();
507
530
  this.teardownPerceivedVoiceMetric();
508
531
  await this.aiConversationStore.handleEndResponse();
509
532
  };
533
+ this.endPresentationTelemetry = ()=>{
534
+ try {
535
+ this.rootStore.presentationTelemetryStore.onCallEnded();
536
+ } catch (unused) {
537
+ // Presentation telemetry must never prevent call teardown.
538
+ }
539
+ };
510
540
  this.setupAudioAnalyser = (call)=>{
511
541
  try {
512
542
  var _call_participants, _localParticipant_tracks_audio, _localParticipant_tracks;
@@ -543,6 +573,14 @@ class CallStore {
543
573
  this.currentAudioTrackId = null;
544
574
  this.isVisitorSpeaking.value = false;
545
575
  };
576
+ // Coffee owns these signals, not the local microphone analyser. Replacing a
577
+ // Daily audio track tears down and rebuilds the analyser during a live call;
578
+ // clearing Coffee state there would fabricate a silence edge and discard the
579
+ // monotonic response boundary used to synchronize deck narration.
580
+ this.resetRealtimeSpeechState = ()=>{
581
+ this.isBotSpeaking.value = false;
582
+ this.voiceAnswerSequence.value = null;
583
+ };
546
584
  this.setupPerceivedVoiceMetric = ()=>{
547
585
  if (!this.perceivedMetricEnabled || this.perceivedTracker || this.perceivedMetricUnavailable) return;
548
586
  try {
@@ -699,7 +737,7 @@ class CallStore {
699
737
  }
700
738
  this.isHandlingFatalCallEnd = true;
701
739
  try {
702
- await this.aiConversationStore.endConversation();
740
+ await this.endCall();
703
741
  } catch (error) {
704
742
  var _getSentryClient;
705
743
  (_getSentryClient = getSentryClient({
@@ -726,6 +764,7 @@ class CallStore {
726
764
  call.off("error", this.handleError);
727
765
  this.voiceGreetingStartTimestamp = null;
728
766
  this.teardownAudioAnalyser();
767
+ this.resetRealtimeSpeechState();
729
768
  this.teardownPerceivedVoiceMetric();
730
769
  this.userStoppedSpeakingTimestamp = null;
731
770
  this._callConnectedEmitted = false;
@@ -86,7 +86,7 @@ import { signals_core_module_w } from "../../../js/multimodal_v2.js";
86
86
  },
87
87
  endCall: ()=>{
88
88
  var _callStore_value;
89
- return (_callStore_value = callStore.value) == null ? void 0 : _callStore_value.aiConversationStore.endConversation();
89
+ return (_callStore_value = callStore.value) == null ? void 0 : _callStore_value.endCall();
90
90
  },
91
91
  selectMic: (deviceId)=>{
92
92
  var _deviceStore_value;