@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
@@ -1,14 +1,36 @@
1
1
  import { __webpack_require__ } from "../../../js/runtime.js";
2
2
  import { signals_core_module_E, signals_core_module_d, signals_core_module_w } from "../../../js/multimodal_v2.js";
3
+ import { DeckMode as types_DeckMode } from "../types.js";
3
4
  import "../../utils/native_globals.js";
4
5
 
5
6
  /* provided dependency */ var decks_store_setTimeout = __webpack_require__(68034).pS;
6
7
  /* provided dependency */ var decks_store_clearTimeout = __webpack_require__(68034).Kj;
8
+ function _extends() {
9
+ _extends = Object.assign || function(target) {
10
+ for(var i = 1; i < arguments.length; i++){
11
+ var source = arguments[i];
12
+ for(var key in source){
13
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
14
+ target[key] = source[key];
15
+ }
16
+ }
17
+ }
18
+ return target;
19
+ };
20
+ return _extends.apply(this, arguments);
21
+ }
22
+
7
23
 
8
24
  // Floor for how long a slide stays up before auto-advancing. A slide whose
9
25
  // talk-track generation failed has an estimated duration of 0; without a floor
10
26
  // setTimeout(…, 0) would flash through the whole sequence instantly.
27
+ // An absent estimate is legacy unknown timing and is not auto-completed.
11
28
  const MIN_SLIDE_DURATION_MS = 2000;
29
+ // Matches the server-side deck-ready wait. If Coffee never reports a silent
30
+ // boundary (for example on an older/partial client where the lead-in flows
31
+ // directly into narration), start timing when the server releases the tool on
32
+ // its timeout rather than leaving the deck frozen forever.
33
+ const DECK_READY_TIMEOUT_MS = 3000;
12
34
  class DecksStore {
13
35
  dispose() {
14
36
  this.unsubscribeArtifact == null ? void 0 : this.unsubscribeArtifact.call(this);
@@ -17,13 +39,46 @@ class DecksStore {
17
39
  this.disposeInterruptEffect = null;
18
40
  this.disposeTextInterruptEffect == null ? void 0 : this.disposeTextInterruptEffect.call(this);
19
41
  this.disposeTextInterruptEffect = null;
42
+ this.disposeNarrationStartEffect == null ? void 0 : this.disposeNarrationStartEffect.call(this);
43
+ this.disposeNarrationStartEffect = null;
44
+ this.disposeDeckReadyEffect == null ? void 0 : this.disposeDeckReadyEffect.call(this);
45
+ this.disposeDeckReadyEffect = null;
46
+ this.clearPendingDeckReady();
47
+ this.clearNarrationStart();
20
48
  this.clearAdvanceTimer();
49
+ // The starting state's watch and deadline are armed by the artifact store but
50
+ // are waiting on state that lives here, so they go down with it.
51
+ this.rootStore.artifactStore.stopWatchingDeckPresentation();
52
+ }
53
+ // Called by the guided deck image's load event. Send at most one readiness
54
+ // acknowledgement per presentation message; later slide loads do not gate the
55
+ // already-running narration turn.
56
+ notifyCurrentSlideReady() {
57
+ var _this_deckReadyDeadlineAt;
58
+ const message = this.rootStore.artifact.message.peek();
59
+ const deck = this.currentDeck.peek();
60
+ if (!message || message.type !== "present_deck" || message.navigationOnly || !message.syncNarration || this.stopped.peek()) {
61
+ return;
62
+ }
63
+ if (!deck || deck.mode !== types_DeckMode.Guided || deck.sequencePosition !== 0) return;
64
+ if (this.readyMessageIds.has(message.id)) return;
65
+ this.clearDeckReadyFallbackTimer();
66
+ const fallbackDelay = Math.max(0, ((_this_deckReadyDeadlineAt = this.deckReadyDeadlineAt) != null ? _this_deckReadyDeadlineAt : Date.now() + DECK_READY_TIMEOUT_MS) - Date.now());
67
+ this.deckReadyFallbackTimerId = decks_store_setTimeout(()=>{
68
+ var _ref;
69
+ var _this_rootStore_callStore_value_isBotSpeaking, _this_rootStore_callStore_value;
70
+ if (this.pendingReadyMessageId.peek() !== message.id || this.stopped.peek()) return;
71
+ const speaking = (_ref = (_this_rootStore_callStore_value = this.rootStore.callStore.value) == null ? void 0 : (_this_rootStore_callStore_value_isBotSpeaking = _this_rootStore_callStore_value.isBotSpeaking) == null ? void 0 : _this_rootStore_callStore_value_isBotSpeaking.peek()) != null ? _ref : false;
72
+ this.releaseDeckReady(message.id, speaking);
73
+ }, fallbackDelay);
74
+ this.pendingReadyMessageId.value = message.id;
21
75
  }
22
76
  // Show a single slide statically (visitor clicked it in the transcript record).
23
77
  // Set the revisit target before the artifact overlay opens so the artifact
24
78
  // subscription treats the open as a revisit rather than a fresh presentation.
25
79
  prepareRevisit(messageId, slideIndex, pageTitle) {
26
80
  this.clearAdvanceTimer();
81
+ this.clearSelfGuided();
27
82
  this.stopped.value = true;
28
83
  this.revisitMessageId = messageId;
29
84
  // Set the title before the index signal, which triggers the currentDeck recompute.
@@ -33,11 +88,55 @@ class DecksStore {
33
88
  isRevisiting(messageId) {
34
89
  return this.revisitMessageId === messageId && this.revisitIndex.peek() != null;
35
90
  }
91
+ // Hand the deck over to the visitor: they step through it themselves from the
92
+ // interactive viewer. Like prepareRevisit this is set BEFORE the artifact
93
+ // overlay opens, so the artifact subscription treats the open as self-guided
94
+ // rather than a fresh presentation. `stopped` keeps the interruption effects
95
+ // inert — there is no narration to barge in on.
96
+ //
97
+ // Deliberately never auto-advances, unlike the guided path next door: the
98
+ // visitor sets the pace, and a timer would fight the arrows they are using.
99
+ prepareSelfGuidedPlayback(messageId, startIndex) {
100
+ if (startIndex === void 0) startIndex = 0;
101
+ this.clearAdvanceTimer();
102
+ this.clearRevisit();
103
+ this.stopped.value = true;
104
+ this.selfGuidedMessageId = messageId;
105
+ this.selfGuidedIndex.value = Math.max(startIndex, 0);
106
+ }
107
+ isSelfGuided(messageId) {
108
+ return this.selfGuidedMessageId === messageId && this.selfGuidedIndex.peek() != null;
109
+ }
110
+ // The visitor has handed the deck to Piper and the call is starting. Holds the
111
+ // slide they were on with the viewer's controls dropped, which is the whole of
112
+ // the state — see CurrentDeck#starting.
113
+ //
114
+ // Self-guided underneath, with the same index it always had: the deck is still
115
+ // in the visitor's hands until Piper's own share replaces this message, and
116
+ // going through the same preparation means the path in from the timeline card
117
+ // and the path in from the viewer arrive at the same place. From the viewer that
118
+ // makes this a no-op plus a flag — it is already this message, at this index.
119
+ prepareStartingPresentation(messageId, startIndex) {
120
+ if (startIndex === void 0) startIndex = 0;
121
+ this.prepareSelfGuidedPlayback(messageId, startIndex);
122
+ this.startingMessageId.value = messageId;
123
+ }
124
+ // Give the deck back to the visitor, leaving them on the slide they handed over
125
+ // — the whole of what undoing this state means, since the viewer underneath was
126
+ // never dismantled. Piper arriving does not come through here: her share is a
127
+ // different message, and the artifact change clears this along with the rest of
128
+ // the visitor's playback state.
129
+ cancelStartingPresentation() {
130
+ this.startingMessageId.value = null;
131
+ }
132
+ goToNextSlide() {
133
+ this.stepSlide(1);
134
+ }
135
+ goToPreviousSlide() {
136
+ this.stepSlide(-1);
137
+ }
36
138
  // Fire-and-forget the current on-screen slide + the full sequence to the backend,
37
- // which computes the remaining slides and seeds the agent's context. The early
38
- // return in handleInterruption (guarding on `stopped`) makes this fire exactly
39
- // once per guided run; a new deck message resets `stopped` so a later resume can
40
- // be interrupted and re-report.
139
+ // which computes the remaining slides and seeds the agent's context.
41
140
  notifyInterruption(message) {
42
141
  var _ref, _message_slides;
43
142
  var _message_slides1;
@@ -52,22 +151,34 @@ class DecksStore {
52
151
  });
53
152
  }
54
153
  scheduleAdvance(slides) {
55
- var _ref;
56
- var _slides_, _slides_position;
57
- if (this.stopped.value) return; // visitor interrupted — do not advance
154
+ var _currentSlide_estimatedDurationMs;
155
+ if (this.stopped.value || slides.length === 0) return;
58
156
  const position = this.playbackIndex.value;
59
- if (position >= slides.length - 1) return; // last slide — stop advancing
60
- // Warm the browser cache for the next slide's image now, while the current one
61
- // is on screen, so the auto-advance swap doesn't flash a blank/stale frame
62
- // while the full-res PDF page downloads (narration keeps pace regardless).
63
- this.preloadImage((_slides_ = slides[position + 1]) == null ? void 0 : _slides_.imageUrl);
64
- // Each slide stays up for as long as its talk track takes to narrate, but
65
- // never less than the floor (a failed talk track estimates to 0ms).
66
- const durationMs = Math.max((_ref = (_slides_position = slides[position]) == null ? void 0 : _slides_position.estimatedDurationMs) != null ? _ref : 0, MIN_SLIDE_DURATION_MS);
157
+ const currentSlide = slides[position];
158
+ if (!currentSlide) return;
159
+ if (position >= slides.length - 1 && currentSlide.estimatedDurationMs === undefined) {
160
+ return;
161
+ }
162
+ const durationMs = Math.max((_currentSlide_estimatedDurationMs = currentSlide.estimatedDurationMs) != null ? _currentSlide_estimatedDurationMs : MIN_SLIDE_DURATION_MS, MIN_SLIDE_DURATION_MS);
163
+ if (position < slides.length - 1) {
164
+ var _slides_;
165
+ // Warm the browser cache for the next slide's image now, while the current one
166
+ // is on screen, so the auto-advance swap doesn't flash a blank/stale frame
167
+ // while the full-res PDF page downloads (narration keeps pace regardless).
168
+ this.preloadImage((_slides_ = slides[position + 1]) == null ? void 0 : _slides_.imageUrl);
169
+ }
67
170
  this.advanceTimerId = decks_store_setTimeout(()=>{
68
171
  this.advanceTimerId = null;
69
172
  if (this.stopped.value) return;
70
- this.playbackIndex.value = Math.min(this.playbackIndex.value + 1, slides.length - 1);
173
+ if (position >= slides.length - 1) {
174
+ this.rootStore.presentationTelemetryStore.onDeckFinalDisplayIntervalComplete();
175
+ return;
176
+ }
177
+ this.playbackIndex.value = position + 1;
178
+ const nextSlide = slides[position + 1];
179
+ if (nextSlide) {
180
+ this.rootStore.presentationTelemetryStore.onDeckSlideRendered(nextSlide.pageId, nextSlide.slideIndex, nextSlide.pageTitle);
181
+ }
71
182
  this.scheduleAdvance(slides);
72
183
  }, durationMs);
73
184
  }
@@ -76,12 +187,71 @@ class DecksStore {
76
187
  this.revisitIndex.value = null;
77
188
  this.revisitPageTitle = undefined;
78
189
  }
190
+ clearSelfGuided() {
191
+ this.selfGuidedMessageId = null;
192
+ this.selfGuidedIndex.value = null;
193
+ // The starting state is a state of self-guided playback, so it cannot outlive
194
+ // it. This is what makes Piper's arrival the end of it for free: her share is
195
+ // a new message, and handling it clears the visitor's playback wholesale.
196
+ this.startingMessageId.value = null;
197
+ }
198
+ // Move one slide, stopping at either end. Wrapping would silently restart the
199
+ // deck; a disabled arrow says what happened.
200
+ stepSlide(delta) {
201
+ var _ref;
202
+ var _this_currentDeck_peek;
203
+ const current = this.selfGuidedIndex.peek();
204
+ if (current == null) return;
205
+ const length = (_ref = (_this_currentDeck_peek = this.currentDeck.peek()) == null ? void 0 : _this_currentDeck_peek.slideImageUrls.length) != null ? _ref : 0;
206
+ const next = current + delta;
207
+ if (next < 0 || next >= length) return;
208
+ this.selfGuidedIndex.value = next;
209
+ }
79
210
  clearAdvanceTimer() {
80
211
  if (this.advanceTimerId !== null) {
81
212
  decks_store_clearTimeout(this.advanceTimerId);
82
213
  this.advanceTimerId = null;
83
214
  }
84
215
  }
216
+ releaseDeckReady(messageId, continuousSpeechFallback) {
217
+ if (this.pendingReadyMessageId.peek() !== messageId) return;
218
+ this.clearPendingDeckReady();
219
+ if (this.readyMessageIds.has(messageId)) return;
220
+ this.readyMessageIds.add(messageId);
221
+ if (continuousSpeechFallback) {
222
+ // The server releases present_deck after the same timeout. With no usable
223
+ // silence/sequence boundary, this is the closest observable narration
224
+ // anchor and, critically, prevents permanent first-slide stalls.
225
+ this.clearNarrationStart();
226
+ this.scheduleAdvance(this.sequence.peek());
227
+ } else {
228
+ var _ref;
229
+ var _this_rootStore_callStore_value_voiceAnswerSequence, _this_rootStore_callStore_value;
230
+ // Snapshot only after the slide is visible and the lead-in has stopped.
231
+ // Any delayed preamble marker received before this point becomes part of
232
+ // the baseline rather than being mistaken for the narration response.
233
+ this.narrationAnswerBaseline = (_ref = (_this_rootStore_callStore_value = this.rootStore.callStore.value) == null ? void 0 : (_this_rootStore_callStore_value_voiceAnswerSequence = _this_rootStore_callStore_value.voiceAnswerSequence) == null ? void 0 : _this_rootStore_callStore_value_voiceAnswerSequence.peek()) != null ? _ref : null;
234
+ this.awaitingNarrationStart.value = true;
235
+ }
236
+ this.rootStore.channel.perform("notify_deck_ready", {
237
+ presentation_message_id: messageId
238
+ });
239
+ }
240
+ clearDeckReadyFallbackTimer() {
241
+ if (this.deckReadyFallbackTimerId !== null) {
242
+ decks_store_clearTimeout(this.deckReadyFallbackTimerId);
243
+ this.deckReadyFallbackTimerId = null;
244
+ }
245
+ }
246
+ clearPendingDeckReady() {
247
+ this.pendingReadyMessageId.value = null;
248
+ this.deckReadyDeadlineAt = null;
249
+ this.clearDeckReadyFallbackTimer();
250
+ }
251
+ clearNarrationStart() {
252
+ this.awaitingNarrationStart.value = false;
253
+ this.narrationAnswerBaseline = null;
254
+ }
85
255
  // Kick off a background fetch of an image URL so it's cached before it's shown. Crossorigin
86
256
  // like every other slide load, so this warm-up can't be the one that caches an ACAO-less
87
257
  // response and poisons the entry the visible <img> needs.
@@ -97,7 +267,7 @@ class DecksStore {
97
267
  this.rootStore = rootStore;
98
268
  // Derived state: the deck slide currently presented in the ARTIFACT overlay.
99
269
  //
100
- // A single share_deck tool call can present one slide or an auto-advancing
270
+ // A single present_deck tool call can present one slide or an auto-advancing
101
271
  // sequence. The slide on screen is `slides[playbackIndex]`; for legacy/single
102
272
  // messages the index comes straight from the message.
103
273
  this.currentDeck = signals_core_module_w(()=>{
@@ -108,11 +278,43 @@ class DecksStore {
108
278
  const slideImageUrls = ((_message_slideImageUrls = message.slideImageUrls) == null ? void 0 : _message_slideImageUrls.length) > 0 ? message.slideImageUrls : [
109
279
  message.imageUrl
110
280
  ];
281
+ // Self-guided: the visitor opened the deck from its timeline card and steps
282
+ // through it themselves. The whole deck is in scope — every image the
283
+ // message carries, not just the slides this share presented — so position
284
+ // and length come from slideImageUrls.
285
+ //
286
+ // Only ever for the share the visitor is actually driving. The position is
287
+ // theirs and so is the message it is a position in, so a different message
288
+ // being on screen means this branch is not what the deck is, whatever is left
289
+ // in the position. `handleArtifactMessageChange` clears that leftover a moment
290
+ // later; checking the id means nothing can observe the gap, where before this
291
+ // branch would answer for Piper's share with the visitor's chrome — and with
292
+ // `starting` false, since that is keyed to the visitor's message too. The
293
+ // field is read rather than subscribed to because it never changes without
294
+ // `selfGuidedIndex` changing in the same breath, which is what recomputes this.
295
+ const selfGuidedIndex = this.selfGuidedIndex.value;
296
+ if (selfGuidedIndex != null && message.id === this.selfGuidedMessageId) {
297
+ var _ref3;
298
+ const currentIndex = clampIndex(selfGuidedIndex, slideImageUrls.length);
299
+ const slide = this.presentedSlides.value.get(currentIndex);
300
+ return {
301
+ deckId: message.deckId,
302
+ slideImageUrls,
303
+ currentIndex,
304
+ sequencePosition: currentIndex,
305
+ sequenceLength: slideImageUrls.length,
306
+ pageTitle: (_ref3 = slide == null ? void 0 : slide.pageTitle) != null ? _ref3 : message.pageTitle,
307
+ deckTitle: message.deckTitle,
308
+ mode: types_DeckMode.SelfGuided,
309
+ starting: this.startingMessageId.value === message.id,
310
+ caption: slide == null ? void 0 : slide.talkTrack
311
+ };
312
+ }
111
313
  // Revisit: the visitor clicked a slide in the transcript record. Show that
112
314
  // single slide statically (no auto-advance, no progress dots).
113
315
  const revisitIndex = this.revisitIndex.value;
114
316
  if (revisitIndex != null) {
115
- var _ref3, _this_revisitPageTitle;
317
+ var _ref4, _this_revisitPageTitle;
116
318
  var _message_slides_find, _message_slides;
117
319
  return {
118
320
  deckId: message.deckId,
@@ -120,8 +322,10 @@ class DecksStore {
120
322
  currentIndex: clampIndex(revisitIndex, slideImageUrls.length),
121
323
  sequencePosition: 0,
122
324
  sequenceLength: 1,
123
- pageTitle: (_ref3 = (_this_revisitPageTitle = this.revisitPageTitle) != null ? _this_revisitPageTitle : (_message_slides = message.slides) == null ? void 0 : (_message_slides_find = _message_slides.find((slide)=>slide.slideIndex === revisitIndex)) == null ? void 0 : _message_slides_find.pageTitle) != null ? _ref3 : message.pageTitle,
124
- deckTitle: message.deckTitle
325
+ pageTitle: (_ref4 = (_this_revisitPageTitle = this.revisitPageTitle) != null ? _this_revisitPageTitle : (_message_slides = message.slides) == null ? void 0 : (_message_slides_find = _message_slides.find((slide)=>slide.slideIndex === revisitIndex)) == null ? void 0 : _message_slides_find.pageTitle) != null ? _ref4 : message.pageTitle,
326
+ deckTitle: message.deckTitle,
327
+ mode: types_DeckMode.Guided,
328
+ starting: false
125
329
  };
126
330
  }
127
331
  const sequence = this.sequence.value;
@@ -135,7 +339,9 @@ class DecksStore {
135
339
  sequencePosition: position,
136
340
  sequenceLength: sequence.length,
137
341
  pageTitle: (_ref2 = slide == null ? void 0 : slide.pageTitle) != null ? _ref2 : message.pageTitle,
138
- deckTitle: message.deckTitle
342
+ deckTitle: message.deckTitle,
343
+ mode: types_DeckMode.Guided,
344
+ starting: false
139
345
  };
140
346
  });
141
347
  // Everything below is private
@@ -145,6 +351,11 @@ class DecksStore {
145
351
  this.stopped = signals_core_module_d(false);
146
352
  this.advanceTimerId = null;
147
353
  this.lastHandledMessageId = null;
354
+ // The last guided sequence for the deck on screen. A go_to call arrives as a
355
+ // new one-slide navigation message, but it is still movement within this
356
+ // presentation. Retain the sequence so the progress dots remain oriented
357
+ // while auto-advance stays stopped on the requested slide.
358
+ this.presentationSequence = signals_core_module_d(null);
148
359
  // Set when the visitor clicks a slide in the transcript record to revisit it.
149
360
  // While set, `currentDeck` shows that single slide statically. Cleared when a
150
361
  // new/different deck message becomes the active artifact.
@@ -154,9 +365,25 @@ class DecksStore {
154
365
  // revisit shows the right title even when the active message (the last share)
155
366
  // doesn't contain that slide. Cleared alongside the revisit state.
156
367
  this.revisitPageTitle = undefined;
368
+ // 0-based position within the whole deck while the visitor drives it. Null
369
+ // whenever playback is agent-driven.
370
+ this.selfGuidedIndex = signals_core_module_d(null);
371
+ this.selfGuidedMessageId = null;
372
+ // The self-guided share whose call is starting, or null. Always one of the
373
+ // above — cleared with them by clearSelfGuided, so no artifact change can leave
374
+ // it behind.
375
+ this.startingMessageId = signals_core_module_d(null);
157
376
  this.unsubscribeArtifact = null;
158
377
  this.disposeInterruptEffect = null;
159
378
  this.disposeTextInterruptEffect = null;
379
+ this.disposeNarrationStartEffect = null;
380
+ this.disposeDeckReadyEffect = null;
381
+ this.awaitingNarrationStart = signals_core_module_d(false);
382
+ this.narrationAnswerBaseline = null;
383
+ this.pendingReadyMessageId = signals_core_module_d(null);
384
+ this.deckReadyFallbackTimerId = null;
385
+ this.deckReadyDeadlineAt = null;
386
+ this.readyMessageIds = new Set();
160
387
  // Id of the most recent visitor text message we've already accounted for, so a
161
388
  // text-mode interruption only fires for a genuinely new message.
162
389
  this.lastVisitorTextMessageId = null;
@@ -164,27 +391,79 @@ class DecksStore {
164
391
  // synthesized one-item sequence for legacy messages without a `slides` array.
165
392
  this.sequence = signals_core_module_w(()=>{
166
393
  var _message_slideIndex;
167
- var _message_slides;
394
+ var _messageSlides_;
168
395
  const message = this.rootStore.artifact.message.value;
169
396
  if (!message || message.type !== "present_deck") return [];
170
- if ((_message_slides = message.slides) == null ? void 0 : _message_slides.length) return message.slides;
171
- return [
172
- {
173
- pageId: message.pageId,
174
- slideIndex: (_message_slideIndex = message.slideIndex) != null ? _message_slideIndex : 0,
175
- imageUrl: message.imageUrl,
176
- pageTitle: message.pageTitle
397
+ const messageSlides = slidesForMessage(message);
398
+ if (!message.navigationOnly) return messageSlides;
399
+ const currentSlideIndex = (_message_slideIndex = message.slideIndex) != null ? _message_slideIndex : (_messageSlides_ = messageSlides[0]) == null ? void 0 : _messageSlides_.slideIndex;
400
+ const preserved = this.presentationSequence.value;
401
+ if ((preserved == null ? void 0 : preserved.deckId) === message.deckId && currentSlideIndex !== undefined && preserved.slides.some((slide)=>slide.slideIndex === currentSlideIndex)) {
402
+ return preserved.slides;
403
+ }
404
+ return messageSlides;
405
+ });
406
+ // Every slide of the active deck the widget can caption, keyed by slide index.
407
+ //
408
+ // A presentation can span several `present_deck` messages — the initial share
409
+ // plus any resume after an interruption, where the agent re-shares only the
410
+ // remaining slides. Merging them oldest-first means the latest share wins for a
411
+ // slide covered twice, so a slide's caption is the narration actually delivered
412
+ // on the resumed run.
413
+ //
414
+ // `deckPages` goes underneath as the base layer. Every share carries a seed
415
+ // caption for every page of the deck, so a visitor stepping past the slides the
416
+ // agent actually selected still gets a caption instead of a blank panel — the
417
+ // card's slide count promises the whole deck. `slides` layered on top means a
418
+ // slide that was presented shows what she said rather than the seed.
419
+ //
420
+ // The top layer often has no caption of its own, and the seed shows through
421
+ // rather than the slide blanking out. That happens for an off-call share, which
422
+ // narrates nothing, and for a slide still being narrated, whose talk track the
423
+ // server withholds until the turn ends (Message::PresentDeckSerializer#slides).
424
+ this.presentedSlides = signals_core_module_w(()=>{
425
+ const message = this.rootStore.artifact.message.value;
426
+ if (!message || message.type !== "present_deck") return new Map();
427
+ const forDeck = this.rootStore.messages.value.filter((m)=>m.type === "present_deck" && m.deckId === message.deckId);
428
+ if (!forDeck.some((m)=>m.id === message.id)) forDeck.push(message);
429
+ forDeck.sort((a, b)=>new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
430
+ const byIndex = new Map();
431
+ for (const share of forDeck){
432
+ var _share_deckPages;
433
+ for (const page of (_share_deckPages = share.deckPages) != null ? _share_deckPages : []){
434
+ byIndex.set(page.slideIndex, page);
177
435
  }
178
- ];
436
+ }
437
+ for (const share of forDeck){
438
+ var _share_slides;
439
+ for (const slide of (_share_slides = share.slides) != null ? _share_slides : []){
440
+ var _slide_talkTrack;
441
+ const under = byIndex.get(slide.slideIndex);
442
+ byIndex.set(slide.slideIndex, _extends({}, slide, {
443
+ talkTrack: (_slide_talkTrack = slide.talkTrack) != null ? _slide_talkTrack : under == null ? void 0 : under.talkTrack
444
+ }));
445
+ }
446
+ }
447
+ return byIndex;
179
448
  });
180
449
  this.handleArtifactMessageChange = (message)=>{
181
- var _ref;
182
- var _message_slides;
183
450
  if (!message || message.type !== "present_deck") {
184
451
  this.clearAdvanceTimer();
452
+ this.clearNarrationStart();
453
+ this.clearPendingDeckReady();
185
454
  this.lastHandledMessageId = null;
186
455
  this.playbackIndex.value = 0;
456
+ this.presentationSequence.value = null;
187
457
  this.clearRevisit();
458
+ this.clearSelfGuided();
459
+ return;
460
+ }
461
+ // A self-guided open (prepareSelfGuidedPlayback set the id first): the
462
+ // visitor owns the pacing, so leave their position alone and start nothing.
463
+ if (message.id === this.selfGuidedMessageId) {
464
+ this.clearAdvanceTimer();
465
+ this.lastHandledMessageId = message.id;
466
+ this.stopped.value = true;
188
467
  return;
189
468
  }
190
469
  // A revisit open (prepareRevisit set revisitMessageId first): show the
@@ -204,21 +483,49 @@ class DecksStore {
204
483
  // presentation on the current slide with nothing left to reschedule it.
205
484
  if (this.lastHandledMessageId === message.id) return;
206
485
  this.clearAdvanceTimer();
207
- // Any other deck message supersedes a revisit view.
486
+ this.clearNarrationStart();
487
+ this.clearPendingDeckReady();
488
+ // Any other deck message supersedes a revisit or self-guided view.
208
489
  this.clearRevisit();
490
+ this.clearSelfGuided();
209
491
  this.lastHandledMessageId = message.id;
492
+ const messageSlides = slidesForMessage(message);
493
+ if (!message.navigationOnly) {
494
+ this.presentationSequence.value = {
495
+ deckId: message.deckId,
496
+ slides: messageSlides
497
+ };
498
+ }
499
+ const slides = this.sequence.peek();
500
+ if (message.navigationOnly) {
501
+ var _message_slideIndex;
502
+ var _messageSlides_;
503
+ const currentSlideIndex = (_message_slideIndex = message.slideIndex) != null ? _message_slideIndex : (_messageSlides_ = messageSlides[0]) == null ? void 0 : _messageSlides_.slideIndex;
504
+ const position = slides.findIndex((slide)=>slide.slideIndex === currentSlideIndex);
505
+ this.playbackIndex.value = Math.max(position, 0);
506
+ this.stopped.value = true;
507
+ return;
508
+ }
509
+ // Auto-advance through the sequence so the agent can present it as
510
+ // one continuous flow without re-calling the tool for each slide.
210
511
  this.playbackIndex.value = 0;
211
512
  this.stopped.value = false;
212
- // Auto-advance through a multi-slide sequence so the agent can present it as
213
- // one continuous flow without re-calling the tool for each slide.
214
- if (message.autoAdvance && ((_ref = (_message_slides = message.slides) == null ? void 0 : _message_slides.length) != null ? _ref : 0) > 1) {
215
- var _message_slides1;
216
- this.scheduleAdvance((_message_slides1 = message.slides) != null ? _message_slides1 : []);
513
+ if (message.autoAdvance || slides.length === 1) {
514
+ if (message.syncNarration) {
515
+ // Start the fallback clock with the deck message, which also starts the
516
+ // server wait. Starting it after image loading would add render time to
517
+ // the timeout and recreate narration/slide drift.
518
+ this.deckReadyDeadlineAt = Date.now() + DECK_READY_TIMEOUT_MS;
519
+ } else {
520
+ this.scheduleAdvance(slides);
521
+ }
217
522
  }
218
523
  };
219
524
  // The visitor started speaking over the presentation. Stop advancing; the current
220
525
  // slide stays on screen, and tell the agent where the presentation stopped so it
221
- // can address the visitor and later resume the remaining slides.
526
+ // can address the visitor and later resume the remaining slides. The stopped
527
+ // guard makes this fire exactly once per guided run; a new continuation message
528
+ // resets stopped so a later resume can be interrupted and re-reported.
222
529
  this.handleInterruption = ()=>{
223
530
  if (this.stopped.value) return;
224
531
  const message = this.rootStore.artifact.message.value;
@@ -227,8 +534,11 @@ class DecksStore {
227
534
  // slide is frozen on screen and which slides remain. Only worth reporting for a
228
535
  // multi-slide auto-advancing sequence with unshown slides left — single-slide
229
536
  // narration is agent-driven and the last slide has nothing to resume.
537
+ this.rootStore.presentationTelemetryStore.onDeckInterrupted();
230
538
  this.notifyInterruption(message);
231
539
  this.clearAdvanceTimer();
540
+ this.clearNarrationStart();
541
+ this.clearPendingDeckReady();
232
542
  this.stopped.value = true;
233
543
  };
234
544
  this.unsubscribeArtifact = this.rootStore.artifact.message.subscribe(this.handleArtifactMessageChange);
@@ -265,6 +575,49 @@ class DecksStore {
265
575
  }
266
576
  this.handleInterruption();
267
577
  });
578
+ // The deck message and realtime narration are separate asynchronous streams.
579
+ // Arm only after the first slide is ready and the lead-in has stopped, then
580
+ // begin slide timing on the narration's actual response boundary.
581
+ this.disposeNarrationStartEffect = signals_core_module_E(()=>{
582
+ var _ref, _ref1;
583
+ var _this_rootStore_callStore_value_voiceAnswerSequence, _this_rootStore_callStore_value, _this_rootStore_callStore_value_isBotSpeaking, _this_rootStore_callStore_value1;
584
+ const awaitingStart = this.awaitingNarrationStart.value;
585
+ const answerSequence = (_ref = (_this_rootStore_callStore_value = this.rootStore.callStore.value) == null ? void 0 : (_this_rootStore_callStore_value_voiceAnswerSequence = _this_rootStore_callStore_value.voiceAnswerSequence) == null ? void 0 : _this_rootStore_callStore_value_voiceAnswerSequence.value) != null ? _ref : null;
586
+ const speaking = (_ref1 = (_this_rootStore_callStore_value1 = this.rootStore.callStore.value) == null ? void 0 : (_this_rootStore_callStore_value_isBotSpeaking = _this_rootStore_callStore_value1.isBotSpeaking) == null ? void 0 : _this_rootStore_callStore_value_isBotSpeaking.value) != null ? _ref1 : false;
587
+ if (!awaitingStart) return;
588
+ // Coffee publishes a new sequence number on the first audio byte of each
589
+ // response. This is the authoritative narration boundary when available:
590
+ // it still advances if a preamble and post-tool narration are one
591
+ // continuous run with no false -> true speaking edge.
592
+ if (this.narrationAnswerBaseline !== null) {
593
+ // A call restart resets Coffee's response sequence. Drop the stale
594
+ // baseline so the first response from the replacement call can start
595
+ // narration even when its sequence begins below the previous call's.
596
+ if (answerSequence === null) {
597
+ this.narrationAnswerBaseline = null;
598
+ } else {
599
+ if (answerSequence <= this.narrationAnswerBaseline) return;
600
+ this.awaitingNarrationStart.value = false;
601
+ this.scheduleAdvance(this.sequence.peek());
602
+ return;
603
+ }
604
+ }
605
+ if (!speaking) return;
606
+ this.awaitingNarrationStart.value = false;
607
+ this.scheduleAdvance(this.sequence.peek());
608
+ });
609
+ // A rendered slide is not sufficient if the pre-tool lead-in is still
610
+ // audible. Releasing the tool result mid-utterance can strand the realtime
611
+ // continuation, so acknowledge readiness only once both the image is visible
612
+ // and the bot has stopped speaking.
613
+ this.disposeDeckReadyEffect = signals_core_module_E(()=>{
614
+ var _ref;
615
+ var _this_rootStore_callStore_value_isBotSpeaking, _this_rootStore_callStore_value;
616
+ const messageId = this.pendingReadyMessageId.value;
617
+ const speaking = (_ref = (_this_rootStore_callStore_value = this.rootStore.callStore.value) == null ? void 0 : (_this_rootStore_callStore_value_isBotSpeaking = _this_rootStore_callStore_value.isBotSpeaking) == null ? void 0 : _this_rootStore_callStore_value_isBotSpeaking.value) != null ? _ref : false;
618
+ if (!messageId || speaking) return;
619
+ this.releaseDeckReady(messageId, false);
620
+ });
268
621
  }
269
622
  }
270
623
 
@@ -272,6 +625,19 @@ function clampIndex(index, length) {
272
625
  if (length <= 0) return 0;
273
626
  return Math.min(Math.max(index, 0), length - 1);
274
627
  }
628
+ function slidesForMessage(message) {
629
+ var _message_slideIndex;
630
+ var _message_slides;
631
+ if ((_message_slides = message.slides) == null ? void 0 : _message_slides.length) return message.slides;
632
+ return [
633
+ {
634
+ pageId: message.pageId,
635
+ slideIndex: (_message_slideIndex = message.slideIndex) != null ? _message_slideIndex : 0,
636
+ imageUrl: message.imageUrl,
637
+ pageTitle: message.pageTitle
638
+ }
639
+ ];
640
+ }
275
641
  // The most recent visitor-authored text message in the list, or null if there
276
642
  // is none. Used to detect a text-mode interruption of a live deck.
277
643
  function latestVisitorText(messages) {