@oxvo/ai-live-assist 7.3.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 (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -0
  3. package/cjs/AiLiveAssist.d.ts +108 -0
  4. package/cjs/AiLiveAssist.js +1774 -0
  5. package/cjs/client.d.ts +53 -0
  6. package/cjs/client.js +193 -0
  7. package/cjs/context.d.ts +58 -0
  8. package/cjs/context.js +979 -0
  9. package/cjs/control.d.ts +31 -0
  10. package/cjs/control.js +190 -0
  11. package/cjs/experienceState.d.ts +18 -0
  12. package/cjs/experienceState.js +82 -0
  13. package/cjs/index.d.ts +13 -0
  14. package/cjs/index.js +32 -0
  15. package/cjs/media.d.ts +34 -0
  16. package/cjs/media.js +207 -0
  17. package/cjs/messages.d.ts +2 -0
  18. package/cjs/messages.js +95 -0
  19. package/cjs/package.json +1 -0
  20. package/cjs/placement.d.ts +52 -0
  21. package/cjs/placement.js +293 -0
  22. package/cjs/presentation.d.ts +41 -0
  23. package/cjs/presentation.js +483 -0
  24. package/cjs/recordingPolicy.d.ts +2 -0
  25. package/cjs/recordingPolicy.js +12 -0
  26. package/cjs/safeSvg.d.ts +1 -0
  27. package/cjs/safeSvg.js +157 -0
  28. package/cjs/tabLock.d.ts +31 -0
  29. package/cjs/tabLock.js +260 -0
  30. package/cjs/types.d.ts +299 -0
  31. package/cjs/types.js +2 -0
  32. package/cjs/ui.d.ts +184 -0
  33. package/cjs/ui.js +2353 -0
  34. package/cjs/version.d.ts +1 -0
  35. package/cjs/version.js +4 -0
  36. package/cjs/visualContext.d.ts +21 -0
  37. package/cjs/visualContext.js +72 -0
  38. package/cjs/voicePresenceUi.d.ts +148 -0
  39. package/cjs/voicePresenceUi.js +2182 -0
  40. package/lib/AiLiveAssist.d.ts +108 -0
  41. package/lib/AiLiveAssist.js +1769 -0
  42. package/lib/client.d.ts +53 -0
  43. package/lib/client.js +187 -0
  44. package/lib/context.d.ts +58 -0
  45. package/lib/context.js +975 -0
  46. package/lib/control.d.ts +31 -0
  47. package/lib/control.js +186 -0
  48. package/lib/experienceState.d.ts +18 -0
  49. package/lib/experienceState.js +78 -0
  50. package/lib/index.d.ts +13 -0
  51. package/lib/index.js +26 -0
  52. package/lib/media.d.ts +34 -0
  53. package/lib/media.js +203 -0
  54. package/lib/messages.d.ts +2 -0
  55. package/lib/messages.js +92 -0
  56. package/lib/placement.d.ts +52 -0
  57. package/lib/placement.js +286 -0
  58. package/lib/presentation.d.ts +41 -0
  59. package/lib/presentation.js +478 -0
  60. package/lib/recordingPolicy.d.ts +2 -0
  61. package/lib/recordingPolicy.js +8 -0
  62. package/lib/safeSvg.d.ts +1 -0
  63. package/lib/safeSvg.js +153 -0
  64. package/lib/tabLock.d.ts +31 -0
  65. package/lib/tabLock.js +256 -0
  66. package/lib/types.d.ts +299 -0
  67. package/lib/types.js +1 -0
  68. package/lib/ui.d.ts +184 -0
  69. package/lib/ui.js +2349 -0
  70. package/lib/version.d.ts +1 -0
  71. package/lib/version.js +1 -0
  72. package/lib/visualContext.d.ts +21 -0
  73. package/lib/visualContext.js +68 -0
  74. package/lib/voicePresenceUi.d.ts +148 -0
  75. package/lib/voicePresenceUi.js +2178 -0
  76. package/package.json +58 -0
@@ -0,0 +1,1774 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.confirmationReplyIntent = void 0;
4
+ const client_js_1 = require("./client.js");
5
+ const control_js_1 = require("./control.js");
6
+ const context_js_1 = require("./context.js");
7
+ const media_js_1 = require("./media.js");
8
+ const presentation_js_1 = require("./presentation.js");
9
+ const recordingPolicy_js_1 = require("./recordingPolicy.js");
10
+ const tabLock_js_1 = require("./tabLock.js");
11
+ const ui_js_1 = require("./ui.js");
12
+ const version_js_1 = require("./version.js");
13
+ const voicePresenceUi_js_1 = require("./voicePresenceUi.js");
14
+ const TERMINAL_STATES = new Set(['ended', 'failed', 'revoked']);
15
+ const ACTION_SCOPED_ERROR_CODES = new Set([
16
+ 'CONFIRMATION_EXPIRED',
17
+ 'STALE_TARGET',
18
+ 'TOKEN_REPLAYED',
19
+ ]);
20
+ const STORAGE_TTL_MS = 15 * 60 * 1000;
21
+ const APPROVAL_REPLIES = new Set([
22
+ 'apply',
23
+ 'apply it',
24
+ 'approve',
25
+ 'confirm',
26
+ 'continue',
27
+ 'do it',
28
+ 'go ahead',
29
+ 'proceed',
30
+ 'save',
31
+ 'save changes',
32
+ 'save it',
33
+ 'submit',
34
+ 'yes',
35
+ 'yes please',
36
+ ]);
37
+ const DECLINE_REPLIES = new Set([
38
+ 'cancel',
39
+ 'cancel it',
40
+ 'close',
41
+ 'close it',
42
+ 'decline',
43
+ 'do not do that',
44
+ 'do not save',
45
+ 'dont do that',
46
+ 'dont save',
47
+ 'never mind',
48
+ 'nevermind',
49
+ 'no',
50
+ 'no thanks',
51
+ 'reject',
52
+ 'stop',
53
+ ]);
54
+ const confirmationReplyIntent = (value) => {
55
+ const normalized = value
56
+ .normalize('NFKC')
57
+ .toLocaleLowerCase('en')
58
+ .replace(/[\u2018\u2019']/gu, '')
59
+ .replace(/[^\p{L}\p{N}]+/gu, ' ')
60
+ .trim()
61
+ .replace(/\s+/gu, ' ');
62
+ if (!normalized || normalized.split(' ').length > 5)
63
+ return null;
64
+ if (DECLINE_REPLIES.has(normalized))
65
+ return 'decline';
66
+ if (APPROVAL_REPLIES.has(normalized))
67
+ return 'approve';
68
+ return null;
69
+ };
70
+ exports.confirmationReplyIntent = confirmationReplyIntent;
71
+ const boundedLocale = (value) => {
72
+ const normalized = value.replace('_', '-').slice(0, 16);
73
+ return /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}$/.test(normalized)
74
+ ? normalized
75
+ : 'en';
76
+ };
77
+ const path = () => window.location.pathname || '/';
78
+ const randomId = (prefix) => `${prefix}_${crypto.randomUUID()}`;
79
+ const versionParts = (value) => {
80
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(value);
81
+ if (!match)
82
+ return null;
83
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
84
+ };
85
+ const versionAtLeast = (current, minimum) => {
86
+ const left = versionParts(current);
87
+ const right = versionParts(minimum);
88
+ if (!left || !right)
89
+ return false;
90
+ for (let index = 0; index < left.length; index += 1) {
91
+ const currentPart = left[index] ?? 0;
92
+ const minimumPart = right[index] ?? 0;
93
+ if (currentPart !== minimumPart)
94
+ return currentPart > minimumPart;
95
+ }
96
+ return true;
97
+ };
98
+ const parseResumeRecord = (value) => {
99
+ if (!value)
100
+ return null;
101
+ try {
102
+ const record = JSON.parse(value);
103
+ if (typeof record.sessionId !== 'string' ||
104
+ typeof record.resumeCredential !== 'string' ||
105
+ !['text', 'voice'].includes(record.mode ?? '') ||
106
+ !Array.isArray(record.scopes) ||
107
+ typeof record.locale !== 'string' ||
108
+ typeof record.expiresAt !== 'number' ||
109
+ record.expiresAt <= Date.now()) {
110
+ return null;
111
+ }
112
+ return {
113
+ ...record,
114
+ tabId: typeof record.tabId === 'string' &&
115
+ /^[A-Za-z0-9_.:-]{8,128}$/.test(record.tabId)
116
+ ? record.tabId
117
+ : '',
118
+ clientSequence: Number.isSafeInteger(record.clientSequence) &&
119
+ (record.clientSequence ?? -1) >= 0
120
+ ? record.clientSequence
121
+ : 0,
122
+ visualConsentGranted: typeof record.visualConsentGranted === 'boolean'
123
+ ? record.visualConsentGranted
124
+ : record.scopes.includes('visual_context'),
125
+ manualTurn: record.manualTurn === true,
126
+ protocolVersion: record.protocolVersion === 2 ? 2 : 1,
127
+ experienceMode: record.experienceMode === 'voice_presence' ? 'voice_presence' : 'standard',
128
+ fallbackIntent: record.fallbackIntent === 'standard_assist' ||
129
+ record.fallbackIntent === 'human_handoff'
130
+ ? record.fallbackIntent
131
+ : null,
132
+ };
133
+ }
134
+ catch {
135
+ return null;
136
+ }
137
+ };
138
+ class AiLiveAssist {
139
+ constructor(app, options) {
140
+ this.app = app;
141
+ this.options = options;
142
+ this.version = version_js_1.VERSION;
143
+ this.bootstrap = null;
144
+ this.view = null;
145
+ this.result = null;
146
+ this.channel = null;
147
+ this.media = null;
148
+ this.context = null;
149
+ this.presentation = null;
150
+ this.connectionAbort = null;
151
+ this.scopes = [];
152
+ this.mode = 'text';
153
+ this.ended = false;
154
+ this.reconnecting = false;
155
+ this.pendingSessionAuthorization = null;
156
+ this.terminalRequestPending = false;
157
+ this.reconnectAttempts = 0;
158
+ this.heartbeatSeconds = 15;
159
+ this.pendingAction = false;
160
+ this.pendingConfirmation = null;
161
+ this.destroyed = false;
162
+ this.lastLaunch = null;
163
+ this.visualConsentGranted = false;
164
+ this.manualTurn = false;
165
+ this.pushActive = false;
166
+ this.activeFallback = null;
167
+ this.pendingHumanFallback = false;
168
+ this.pendingFocusedHandoff = null;
169
+ this.agentDiscoveryLink = null;
170
+ this.onPageHide = (event) => {
171
+ if (!this.result || this.ended)
172
+ return;
173
+ this.cleanupTransport();
174
+ if (!event.persisted)
175
+ this.lock.release();
176
+ };
177
+ this.onPageShow = (event) => {
178
+ if (!event.persisted || this.ended || this.channel?.isOpen)
179
+ return;
180
+ const resume = this.readResume();
181
+ if (resume && this.lock.acquire())
182
+ void this.resume(resume);
183
+ };
184
+ this.onWindowFocus = () => this.requestFocusedHandoff();
185
+ this.onVisibilityChange = () => {
186
+ if (document.visibilityState === 'visible')
187
+ this.requestFocusedHandoff();
188
+ };
189
+ this.onOffline = () => {
190
+ if (!this.ended && this.result) {
191
+ this.view?.showConnecting(true, this.mode);
192
+ this.cleanupTransport();
193
+ }
194
+ };
195
+ this.onOnline = () => this.scheduleReconnect();
196
+ this.onVisitorControl = (event) => {
197
+ if (event.isTrusted && !this.result && this.isVoicePresence()) {
198
+ this.requestFocusedHandoff();
199
+ }
200
+ if (!this.pendingAction)
201
+ return;
202
+ const eventPath = event.composedPath();
203
+ if (eventPath.some((target) => target instanceof HTMLElement &&
204
+ target.dataset.oxvoAiLiveAssistRoot === 'true')) {
205
+ return;
206
+ }
207
+ this.pendingAction = false;
208
+ if (this.view?.cancelPendingConfirmation())
209
+ return;
210
+ void this.updatePause(true);
211
+ };
212
+ if (options.recordReplay === false)
213
+ (0, recordingPolicy_js_1.disableReplayRecording)(app);
214
+ this.siteKey = options.siteKey || app.getProjectKey();
215
+ this.storageKey = `__oxvo_ai_live_assist_resume_${this.siteKey}`;
216
+ const resume = this.readResume();
217
+ this.tabId = resume?.tabId || app.session.getTabId() || randomId('tab');
218
+ this.windowId = randomId('window');
219
+ this.locale = boundedLocale(options.locale ||
220
+ document.documentElement.lang ||
221
+ navigator.language ||
222
+ 'en');
223
+ const endpoint = app.options.endpoint;
224
+ const runtimeUrl = options.runtimeUrl ||
225
+ (endpoint
226
+ ? new URL(endpoint, window.location.href).origin
227
+ : window.location.origin);
228
+ this.runtime = new client_js_1.RuntimeClient(runtimeUrl, version_js_1.VERSION);
229
+ this.lock = new tabLock_js_1.TabActivityLock(this.siteKey, this.windowId, (active) => this.view?.showActiveElsewhere(active), (requesterId, requestId) => this.handoffToFocusedTab(requesterId, requestId), (payload) => void this.acceptFocusedTabHandoff(payload));
230
+ this.bindWindowLifecycle();
231
+ void this.initialize();
232
+ }
233
+ async end() {
234
+ if (this.ended)
235
+ return;
236
+ this.ended = true;
237
+ const result = this.result;
238
+ const pendingAuthorization = this.pendingSessionAuthorization;
239
+ const controlChannel = this.channel;
240
+ const fencingToken = this.channel?.leaseToken ?? 0;
241
+ const terminalMessageId = result?.protocolVersion === 2
242
+ ? this.send('experience.end', { reason: 'visitor_button' })
243
+ : null;
244
+ this.terminalRequestPending = Boolean(result || pendingAuthorization);
245
+ this.clearResume();
246
+ this.cleanupTransport({
247
+ preserveConnectionRequest: this.terminalRequestPending,
248
+ preserveControlChannel: Boolean(controlChannel),
249
+ });
250
+ this.lock.release();
251
+ this.view?.showEnded();
252
+ this.state('ended');
253
+ try {
254
+ let authorizedResult = result;
255
+ if (!authorizedResult && pendingAuthorization) {
256
+ try {
257
+ authorizedResult = await pendingAuthorization;
258
+ }
259
+ catch {
260
+ authorizedResult = null;
261
+ }
262
+ }
263
+ if (authorizedResult) {
264
+ await this.requestRuntimeEnd(authorizedResult, result ? fencingToken : 0, controlChannel, terminalMessageId);
265
+ }
266
+ }
267
+ finally {
268
+ this.terminalRequestPending = false;
269
+ this.cleanupTransport();
270
+ }
271
+ }
272
+ destroy() {
273
+ if (this.destroyed)
274
+ return;
275
+ if (this.result?.protocolVersion === 2 && !this.ended) {
276
+ this.send('experience.end', { reason: 'unmount' });
277
+ }
278
+ this.destroyed = true;
279
+ window.removeEventListener('pagehide', this.onPageHide);
280
+ window.removeEventListener('pageshow', this.onPageShow);
281
+ window.removeEventListener('online', this.onOnline);
282
+ window.removeEventListener('offline', this.onOffline);
283
+ window.removeEventListener('focus', this.onWindowFocus);
284
+ document.removeEventListener('visibilitychange', this.onVisibilityChange);
285
+ document.removeEventListener('pointerdown', this.onVisitorControl, true);
286
+ document.removeEventListener('keydown', this.onVisitorControl, true);
287
+ this.cleanupTransport({
288
+ preserveConnectionRequest: this.terminalRequestPending,
289
+ preserveControlChannel: this.terminalRequestPending,
290
+ });
291
+ this.lock.destroy();
292
+ this.agentDiscoveryLink?.remove();
293
+ this.agentDiscoveryLink = null;
294
+ this.view?.destroy();
295
+ this.view = null;
296
+ }
297
+ async initialize() {
298
+ if (this.options.enabled === false || this.destroyed)
299
+ return;
300
+ const controller = new AbortController();
301
+ const timeout = setTimeout(() => controller.abort(), Math.max(1000, Math.min(15000, this.options.bootstrapTimeoutMs ?? 15000)));
302
+ try {
303
+ const bootstrap = await this.runtime.bootstrap(this.siteKey, path(), this.visitorId(), controller.signal);
304
+ this.syncAgentDiscovery(bootstrap.agentAccess?.discoveryUrl ?? null);
305
+ if (bootstrap.replayRecordingEnabled === false) {
306
+ (0, recordingPolicy_js_1.disableReplayRecording)(this.app);
307
+ }
308
+ const experienceMode = bootstrap.experienceMode ?? 'standard';
309
+ const voicePresence = experienceMode === 'voice_presence';
310
+ const requestedProtocol = voicePresence ? 2 : 1;
311
+ const minimumProtocol = bootstrap.minimumProtocolVersion ?? requestedProtocol;
312
+ if (!bootstrap.available ||
313
+ bootstrap.protocol.min > requestedProtocol ||
314
+ bootstrap.protocol.max < requestedProtocol ||
315
+ minimumProtocol > requestedProtocol ||
316
+ !bootstrap.configRevision ||
317
+ !bootstrap.consent ||
318
+ !bootstrap.capabilities ||
319
+ (voicePresence &&
320
+ (!bootstrap.capabilities.voice ||
321
+ !bootstrap.capabilities.voicePresence ||
322
+ !bootstrap.appearance?.voicePresence?.enabled))) {
323
+ return;
324
+ }
325
+ if (bootstrap.widgetVersion &&
326
+ bootstrap.widgetVersion.split('.')[0] !== version_js_1.VERSION.split('.')[0]) {
327
+ return;
328
+ }
329
+ if (bootstrap.minimumClientVersion &&
330
+ !versionAtLeast(version_js_1.VERSION, bootstrap.minimumClientVersion)) {
331
+ return;
332
+ }
333
+ let resume = this.readResume();
334
+ const resumedFallback = resume?.experienceMode === 'standard' &&
335
+ resume.protocolVersion === 2 &&
336
+ resume.fallbackIntent !== null &&
337
+ this.fallbackAllowed(bootstrap, resume.fallbackIntent);
338
+ if (resume &&
339
+ !resumedFallback &&
340
+ (resume.experienceMode !== experienceMode ||
341
+ resume.protocolVersion !== requestedProtocol ||
342
+ resume.fallbackIntent !== null)) {
343
+ this.clearResume();
344
+ resume = null;
345
+ }
346
+ if (resumedFallback && resume) {
347
+ this.activeFallback = resume.fallbackIntent;
348
+ this.pendingHumanFallback = resume.fallbackIntent === 'human_handoff';
349
+ }
350
+ const effectiveBootstrap = resumedFallback
351
+ ? this.standardFallbackBootstrap(bootstrap)
352
+ : bootstrap;
353
+ this.bootstrap = effectiveBootstrap;
354
+ if (!resume &&
355
+ bootstrap.visitorEligibility?.status === 'visitor_limited' &&
356
+ bootstrap.appearance?.hideLauncherWhenVisitorLimited !== false) {
357
+ return;
358
+ }
359
+ const callbacks = this.widgetCallbacks();
360
+ const voicePresenceView = voicePresence &&
361
+ (!resumedFallback || resume?.fallbackIntent === 'human_handoff');
362
+ this.view = voicePresenceView
363
+ ? new voicePresenceUi_js_1.VoicePresenceView(bootstrap, this.options.messages ?? {}, callbacks, this.tabId, (fallback) => void this.activateFallback(fallback), this.options.visualTestMode === true)
364
+ : new ui_js_1.WidgetView(effectiveBootstrap, this.options.messages ?? {}, callbacks);
365
+ this.view.showActiveElsewhere(this.lock.isOwnedByOther());
366
+ if (!resume && bootstrap.visitorEligibility?.status !== 'eligible') {
367
+ const key = bootstrap.visitorEligibility?.status === 'visitor_limited'
368
+ ? 'visitorLimitReached'
369
+ : bootstrap.visitorEligibility?.status === 'cooldown_limited'
370
+ ? 'visitorCooldown'
371
+ : bootstrap.visitorEligibility?.status === 'visitor_active'
372
+ ? 'visitorAlreadyActive'
373
+ : null;
374
+ if (key && !(voicePresenceView && key === 'visitorAlreadyActive')) {
375
+ this.view.showLaunchUnavailable(this.view.message(key));
376
+ }
377
+ }
378
+ if (resume && this.lock.acquire()) {
379
+ this.view.showConnecting(true, resume.mode);
380
+ await this.resume(resume);
381
+ }
382
+ else if (voicePresenceView && !resume) {
383
+ this.requestFocusedHandoff();
384
+ }
385
+ }
386
+ catch (error) {
387
+ if (!(error instanceof DOMException && error.name === 'AbortError')) {
388
+ this.view?.showError(this.publicError(error), this.retryable(error));
389
+ }
390
+ }
391
+ finally {
392
+ clearTimeout(timeout);
393
+ }
394
+ }
395
+ syncAgentDiscovery(discoveryUrl) {
396
+ this.agentDiscoveryLink?.remove();
397
+ this.agentDiscoveryLink = null;
398
+ if (!discoveryUrl || !document.head)
399
+ return;
400
+ let parsed;
401
+ try {
402
+ parsed = new URL(discoveryUrl, window.location.href);
403
+ }
404
+ catch {
405
+ return;
406
+ }
407
+ if (!['http:', 'https:'].includes(parsed.protocol))
408
+ return;
409
+ const link = document.createElement('link');
410
+ link.rel = 'alternate';
411
+ link.type = 'application/vnd.oxvo.ai-assist+json';
412
+ link.href = parsed.href;
413
+ link.dataset.oxvoAiAgentDiscovery = 'true';
414
+ document.head.append(link);
415
+ this.agentDiscoveryLink = link;
416
+ }
417
+ widgetCallbacks() {
418
+ return {
419
+ onStart: (mode, scopes) => void this.launch(mode, scopes),
420
+ onEnd: () => void this.end(),
421
+ onMute: (muted) => this.updateMute(muted),
422
+ onInterrupt: () => this.interrupt(),
423
+ onVoiceTurnMode: (manual) => this.updateVoiceTurnMode(manual),
424
+ onPushToTalk: (active) => this.updatePushToTalk(active),
425
+ onPause: (paused) => void this.updatePause(paused),
426
+ onCaptions: (enabled) => this.updateCaptions(enabled),
427
+ onTextOnly: () => void this.switchToText(),
428
+ onText: (text) => void this.sendText(text),
429
+ onHuman: () => this.send('handoff.request', {
430
+ reason: 'The visitor requested a human support agent.',
431
+ }),
432
+ onConfirmation: (actionId, proposalChecksum, approved) => {
433
+ this.pendingConfirmation = null;
434
+ this.send('confirmation.result', {
435
+ actionId,
436
+ proposalChecksum,
437
+ approved,
438
+ });
439
+ if (!approved)
440
+ this.pendingAction = false;
441
+ },
442
+ onVisualContext: (request, approved) => {
443
+ void this.resolveVisualContext(request, approved, true);
444
+ },
445
+ onVisualPause: (paused) => this.updateVisualPause(paused),
446
+ onFeedback: (outcome) => void this.submitFeedback(outcome),
447
+ onRetry: () => void this.retry(),
448
+ onPlaybackState: (value) => {
449
+ if (this.result?.protocolVersion === 2) {
450
+ this.send('playback.state', value);
451
+ }
452
+ },
453
+ };
454
+ }
455
+ fallbackAllowed(bootstrap, intent) {
456
+ const configured = bootstrap.appearance?.voicePresence?.fallback;
457
+ if (intent === 'standard_assist') {
458
+ return (configured === 'offer_standard_assist' &&
459
+ Boolean(bootstrap.capabilities?.text || bootstrap.capabilities?.voice));
460
+ }
461
+ return (configured === 'offer_human_handoff' &&
462
+ bootstrap.capabilities?.handoff === true &&
463
+ bootstrap.capabilities.text);
464
+ }
465
+ standardFallbackBootstrap(bootstrap) {
466
+ if (!bootstrap.capabilities || !bootstrap.consent)
467
+ return bootstrap;
468
+ const requiredScopes = ['ai_disclosure', 'text'];
469
+ const optionalScopes = [
470
+ ...(bootstrap.consent?.requiredScopes ?? []),
471
+ ...(bootstrap.consent?.optionalScopes ?? []),
472
+ ].filter((scope, index, values) => scope !== 'ai_disclosure' &&
473
+ scope !== 'text' &&
474
+ scope !== 'presentation_guidance' &&
475
+ values.indexOf(scope) === index);
476
+ return {
477
+ ...bootstrap,
478
+ experienceMode: 'standard',
479
+ minimumProtocolVersion: 2,
480
+ capabilities: {
481
+ ...bootstrap.capabilities,
482
+ voicePresence: false,
483
+ presentation: false,
484
+ aiCursor: false,
485
+ annotations: false,
486
+ spatialCallouts: false,
487
+ },
488
+ consent: {
489
+ ...bootstrap.consent,
490
+ requiredScopes,
491
+ optionalScopes,
492
+ },
493
+ };
494
+ }
495
+ async activateFallback(intent) {
496
+ const bootstrap = this.bootstrap;
497
+ if (!bootstrap || !this.fallbackAllowed(bootstrap, intent))
498
+ return;
499
+ if (intent === 'human_handoff' &&
500
+ this.result?.capabilities?.handoff &&
501
+ this.channel?.isOpen) {
502
+ this.view?.setStatus(this.view.message('humanConnecting'));
503
+ this.send('handoff.request', {
504
+ reason: 'The visitor requested human support after voice assistance became unavailable.',
505
+ });
506
+ return;
507
+ }
508
+ if (this.result && !this.ended)
509
+ await this.end();
510
+ this.result = null;
511
+ this.lastLaunch = null;
512
+ this.activeFallback = intent;
513
+ this.pendingHumanFallback = intent === 'human_handoff';
514
+ this.ended = false;
515
+ const fallbackBootstrap = this.standardFallbackBootstrap(bootstrap);
516
+ this.bootstrap = fallbackBootstrap;
517
+ if (intent === 'standard_assist') {
518
+ this.view?.destroy();
519
+ this.view = new ui_js_1.WidgetView(fallbackBootstrap, this.options.messages ?? {}, this.widgetCallbacks());
520
+ this.state('fallback_standard_offered');
521
+ return;
522
+ }
523
+ const scopes = [
524
+ ...(fallbackBootstrap.consent?.requiredScopes ?? []),
525
+ ...(fallbackBootstrap.consent?.optionalScopes ?? []),
526
+ ].filter((scope, index, values) => values.indexOf(scope) === index);
527
+ if (!scopes.includes('handoff_context')) {
528
+ this.view?.showError(this.view.message('humanUnavailable'), false, false);
529
+ return;
530
+ }
531
+ this.view?.setStatus(this.view.message('humanConnecting'));
532
+ await this.launch('text', scopes);
533
+ }
534
+ async launch(mode, scopes) {
535
+ const bootstrap = this.bootstrap;
536
+ if (!bootstrap?.configRevision || !bootstrap.consent || !this.view)
537
+ return;
538
+ const experienceMode = bootstrap.experienceMode ?? 'standard';
539
+ const voicePresence = experienceMode === 'voice_presence';
540
+ const protocolVersion = this.activeFallback ? 2 : voicePresence ? 2 : 1;
541
+ if (voicePresence && mode !== 'voice') {
542
+ this.view.showError('Voice assist requires microphone access.', false, false);
543
+ return;
544
+ }
545
+ if (!this.lock.acquire()) {
546
+ this.view.showActiveElsewhere(true);
547
+ return;
548
+ }
549
+ this.lastLaunch = { mode, scopes: [...scopes] };
550
+ let activeMode = mode;
551
+ let activeScopes = [...scopes];
552
+ this.manualTurn =
553
+ mode === 'voice' && bootstrap.voice?.turnMode === 'manual';
554
+ this.pushActive = false;
555
+ this.ended = false;
556
+ if (this.view instanceof voicePresenceUi_js_1.VoicePresenceView) {
557
+ this.view.showRequestingMicrophone();
558
+ }
559
+ else {
560
+ this.view.showConnecting(false, mode);
561
+ }
562
+ this.state('requesting_access');
563
+ let runtimeSessionCreated = false;
564
+ let media = this.createMedia();
565
+ this.media = media;
566
+ try {
567
+ media.setManualTurn(this.manualTurn);
568
+ try {
569
+ await media.prepare(mode);
570
+ }
571
+ catch (error) {
572
+ if (voicePresence ||
573
+ mode !== 'voice' ||
574
+ !bootstrap.capabilities?.text) {
575
+ throw error;
576
+ }
577
+ media.close();
578
+ activeMode = 'text';
579
+ activeScopes = activeScopes.filter((scope) => scope !== 'microphone');
580
+ this.manualTurn = false;
581
+ media = this.createMedia();
582
+ this.media = media;
583
+ this.view.setStatus(this.view.message('microphoneUnavailable'));
584
+ }
585
+ if (voicePresence)
586
+ this.view.showConnecting(false, 'voice');
587
+ this.mode = activeMode;
588
+ this.scopes = activeScopes;
589
+ this.visualConsentGranted = activeScopes.includes('visual_context');
590
+ const controller = new AbortController();
591
+ this.connectionAbort = controller;
592
+ const authorization = this.runtime.launch({
593
+ siteKey: this.siteKey,
594
+ configRevision: bootstrap.configRevision,
595
+ path: path(),
596
+ tabId: this.tabId,
597
+ mode: activeMode,
598
+ anonymousId: this.visitorId(),
599
+ ...this.replayContext(),
600
+ disclosureChecksum: bootstrap.consent.disclosureChecksum,
601
+ scopes: activeScopes,
602
+ locale: this.locale,
603
+ microphone: activeMode === 'voice',
604
+ reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
605
+ protocolVersion,
606
+ experienceMode,
607
+ ...(this.activeFallback
608
+ ? { fallbackIntent: this.activeFallback }
609
+ : {}),
610
+ lowPower: this.lowPowerMode(),
611
+ }, controller.signal);
612
+ this.pendingSessionAuthorization = authorization;
613
+ let result;
614
+ try {
615
+ result = await authorization;
616
+ }
617
+ finally {
618
+ if (this.pendingSessionAuthorization === authorization) {
619
+ this.pendingSessionAuthorization = null;
620
+ }
621
+ }
622
+ if (result.experienceMode !== experienceMode ||
623
+ result.protocolVersion !== protocolVersion) {
624
+ throw new client_js_1.RuntimeClientError('PROTOCOL_INVALID', 'The voice assist runtime returned an incompatible session.', false, null, 502);
625
+ }
626
+ this.result = result;
627
+ runtimeSessionCreated = true;
628
+ if (this.ended)
629
+ return;
630
+ this.writeResume(result, activeMode, activeScopes, undefined, 0);
631
+ await this.connectTransport(result, activeMode, activeScopes, false, controller, media);
632
+ this.reconnectAttempts = 0;
633
+ this.lastLaunch = null;
634
+ }
635
+ catch (error) {
636
+ this.cleanupTransport();
637
+ this.lock.release();
638
+ if (this.ended)
639
+ return;
640
+ if (!(await this.applyVisitorLimitFailure(error))) {
641
+ this.view?.showError(this.publicError(error), this.retryable(error), runtimeSessionCreated);
642
+ }
643
+ this.state('error');
644
+ }
645
+ }
646
+ async applyVisitorLimitFailure(error) {
647
+ if (!(error instanceof client_js_1.RuntimeClientError) || error.code !== 'RATE_LIMITED') {
648
+ return false;
649
+ }
650
+ const controller = new AbortController();
651
+ const timeout = setTimeout(() => controller.abort(), 5000);
652
+ try {
653
+ const bootstrap = await this.runtime.bootstrap(this.siteKey, path(), this.visitorId(), controller.signal);
654
+ this.bootstrap = bootstrap;
655
+ const status = bootstrap.visitorEligibility?.status;
656
+ if (status === 'visitor_limited') {
657
+ if (bootstrap.appearance?.hideLauncherWhenVisitorLimited !== false) {
658
+ this.view?.destroy();
659
+ this.view = null;
660
+ return true;
661
+ }
662
+ this.view?.showLaunchUnavailable(this.view.message('visitorLimitReached'));
663
+ return true;
664
+ }
665
+ if (status === 'cooldown_limited') {
666
+ this.view?.showLaunchUnavailable(this.view.message('visitorCooldown'));
667
+ return true;
668
+ }
669
+ }
670
+ catch {
671
+ // Preserve the original launch error when eligibility refresh is unavailable.
672
+ }
673
+ finally {
674
+ clearTimeout(timeout);
675
+ }
676
+ return false;
677
+ }
678
+ async resume(record) {
679
+ if (this.reconnecting || this.destroyed)
680
+ return;
681
+ this.reconnecting = true;
682
+ let connected = false;
683
+ this.mode = record.mode;
684
+ this.scopes = [...record.scopes];
685
+ this.visualConsentGranted = record.visualConsentGranted;
686
+ this.manualTurn =
687
+ record.mode === 'voice' &&
688
+ (record.manualTurn || this.bootstrap?.voice?.turnMode === 'manual');
689
+ this.pushActive = false;
690
+ this.state('reconnecting');
691
+ const controller = new AbortController();
692
+ this.connectionAbort = controller;
693
+ try {
694
+ const authorization = this.runtime.resume(record.sessionId, record.resumeCredential, path(), record.protocolVersion, controller.signal);
695
+ this.pendingSessionAuthorization = authorization;
696
+ let result;
697
+ try {
698
+ result = await authorization;
699
+ }
700
+ finally {
701
+ if (this.pendingSessionAuthorization === authorization) {
702
+ this.pendingSessionAuthorization = null;
703
+ }
704
+ }
705
+ if (result.protocolVersion !== record.protocolVersion ||
706
+ result.experienceMode !== record.experienceMode) {
707
+ throw new client_js_1.RuntimeClientError('PROTOCOL_INVALID', 'The saved voice assist session is no longer compatible.', false, null, 409);
708
+ }
709
+ this.result = result;
710
+ if (this.ended)
711
+ return;
712
+ this.writeResume(result, record.mode, record.scopes, record.visualConsentGranted, record.clientSequence);
713
+ const media = this.createMedia();
714
+ this.media = media;
715
+ media.setManualTurn(this.manualTurn);
716
+ await media.prepare(record.mode);
717
+ await this.connectTransport(result, record.mode, record.scopes, true, controller, media);
718
+ this.reconnectAttempts = 0;
719
+ connected = true;
720
+ }
721
+ catch (error) {
722
+ this.clearResume();
723
+ this.cleanupTransport();
724
+ this.lock.release();
725
+ if (this.ended)
726
+ return;
727
+ this.view?.showError(this.publicError(error), this.retryable(error));
728
+ this.state('error');
729
+ }
730
+ finally {
731
+ this.reconnecting = false;
732
+ const pending = this.pendingFocusedHandoff;
733
+ if (connected && pending && !this.ended && this.result) {
734
+ this.pendingFocusedHandoff = null;
735
+ this.handoffToFocusedTab(pending.requesterId, pending.requestId);
736
+ }
737
+ }
738
+ }
739
+ async connectTransport(result, mode, scopes, resume, controller, media) {
740
+ const channel = new control_js_1.ControlChannel(result, this.tabId, version_js_1.VERSION, () => this.scheduleReconnect(), this.readResume()?.clientSequence ?? 0, (sequence) => this.updateResumeSequence(sequence));
741
+ this.channel = channel;
742
+ channel.subscribe((message) => this.handleServerMessage(message));
743
+ await channel.connect(controller.signal, resume);
744
+ const fencingToken = await media.connect(result, mode, controller.signal);
745
+ channel.setFencingToken(fencingToken);
746
+ channel.startHeartbeat(this.heartbeatSeconds);
747
+ channel.send('consent.update', { scopes, locale: this.locale });
748
+ channel.send('media.state', {
749
+ microphone: mode === 'voice' ? 'active' : 'unavailable',
750
+ playback: mode === 'voice' ? 'active' : 'paused',
751
+ captions: true,
752
+ });
753
+ if (mode === 'voice' &&
754
+ this.manualTurn &&
755
+ this.bootstrap?.voice?.turnMode !== 'manual') {
756
+ channel.send('voice.turn_mode', { mode: 'manual' });
757
+ }
758
+ this.startContext(scopes);
759
+ this.startPresentation(result, scopes);
760
+ if (this.view instanceof voicePresenceUi_js_1.VoicePresenceView) {
761
+ this.view.setCapabilityTier(result.capabilityTier);
762
+ }
763
+ this.view?.setVisualAvailable(this.visualConsentGranted, !scopes.includes('visual_context'));
764
+ this.view?.configureVoiceControls(mode === 'voice', this.manualTurn, this.bootstrap?.voice?.turnMode !== 'manual');
765
+ const welcomeMessage = this.pendingHumanFallback
766
+ ? null
767
+ : result.assistant?.welcomeMessage ??
768
+ this.bootstrap?.appearance?.welcomeMessage;
769
+ this.view?.showActive(this.view instanceof voicePresenceUi_js_1.VoicePresenceView && mode === 'voice'
770
+ ? null
771
+ : welcomeMessage);
772
+ this.state(mode === 'voice' ? 'active_voice' : 'active_text');
773
+ if (this.pendingHumanFallback) {
774
+ this.pendingHumanFallback = false;
775
+ if (result.capabilities?.handoff) {
776
+ this.view?.setStatus(this.view.message('humanConnecting'));
777
+ this.send('handoff.request', {
778
+ reason: 'The visitor chose human support because voice assistance was unavailable.',
779
+ });
780
+ }
781
+ else {
782
+ this.view?.showError(this.view.message('humanUnavailable'), false);
783
+ }
784
+ }
785
+ }
786
+ startContext(scopes) {
787
+ this.context?.stop();
788
+ this.context = null;
789
+ if (!scopes.includes('page_context') ||
790
+ !this.bootstrap?.capabilities?.pageContext) {
791
+ return;
792
+ }
793
+ this.context = new context_js_1.PageContextCollector({
794
+ onSnapshot: (snapshot) => {
795
+ if (this.channel?.leaseToken)
796
+ this.send('context.snapshot', snapshot);
797
+ },
798
+ onNavigation: (pageId, revision, url) => {
799
+ this.presentation?.clear('navigation');
800
+ this.send('context.navigation', {
801
+ pageId,
802
+ revision,
803
+ url,
804
+ navigationType: 'spa',
805
+ });
806
+ },
807
+ onHighlight: (element, status) => this.view?.updateHighlight(element, status),
808
+ }, this.bootstrap.safety?.selectorRegions ?? [], this.bootstrap.safety?.policyVersion ?? 1);
809
+ this.context.start();
810
+ }
811
+ startPresentation(result, scopes) {
812
+ this.presentation?.destroy();
813
+ this.presentation = null;
814
+ const voice = this.bootstrap?.appearance?.voicePresence;
815
+ if (result.protocolVersion !== 2 ||
816
+ result.experienceMode !== 'voice_presence' ||
817
+ result.capabilities?.presentation !== true ||
818
+ !scopes.includes('presentation_guidance') ||
819
+ !this.context ||
820
+ !voice) {
821
+ return;
822
+ }
823
+ const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
824
+ const presentationConfig = voice.presentation;
825
+ this.presentation = new presentation_js_1.PresentationPlane(this.bootstrap?.appearance?.accent ?? '#2563eb', voice.presentation.maxActiveCues, reducedMotion || result.capabilityTier !== 'standard', {
826
+ resolveTarget: (targetId, revision) => this.context?.presentationTarget(targetId, revision) ?? null,
827
+ currentPage: () => ({
828
+ pageId: this.context?.currentPageId ?? 'page_unknown',
829
+ revision: this.context?.currentRevision ?? 0,
830
+ }),
831
+ onResult: (value) => this.sendPresentationResult(value),
832
+ onVisibility: (pageId, domRevision, visiblePresentationIds) => {
833
+ this.send('presentation.visibility', {
834
+ pageId,
835
+ domRevision,
836
+ visiblePresentationIds,
837
+ documentVisible: !document.hidden,
838
+ });
839
+ },
840
+ launcherRect: () => this.view instanceof voicePresenceUi_js_1.VoicePresenceView ? this.view.launcherRect() : null,
841
+ obstacleRects: () => this.view instanceof voicePresenceUi_js_1.VoicePresenceView
842
+ ? this.view.presentationObstacles()
843
+ : [],
844
+ spatialTrail: voice.visual.spatialTrail,
845
+ kindEnabled: (kind) => {
846
+ if (kind === 'clear')
847
+ return true;
848
+ if (kind === 'pointer') {
849
+ return (presentationConfig.aiCursor && result.capabilities?.aiCursor !== false);
850
+ }
851
+ if (kind === 'spotlight')
852
+ return presentationConfig.spotlight;
853
+ if (['underline', 'circle', 'arrow', 'connector'].includes(kind)) {
854
+ return (presentationConfig.annotations &&
855
+ result.capabilities?.annotations !== false);
856
+ }
857
+ if (kind === 'sequence_marker')
858
+ return presentationConfig.sequenceMarkers;
859
+ return (kind === 'callout' &&
860
+ presentationConfig.spatialCallouts &&
861
+ result.capabilities?.spatialCallouts !== false);
862
+ },
863
+ });
864
+ }
865
+ async sendText(text) {
866
+ if (this.resolvePendingConfirmationReply(text))
867
+ return;
868
+ const supersedesConfirmation = this.pendingConfirmation !== null;
869
+ await this.context?.captureAndPublish();
870
+ if (this.ended)
871
+ return;
872
+ this.send('text.message', { text });
873
+ if (supersedesConfirmation)
874
+ this.cancelConfirmationForNewRequest();
875
+ }
876
+ resolvePendingConfirmationReply(text) {
877
+ const pending = this.pendingConfirmation;
878
+ if (!pending)
879
+ return false;
880
+ const intent = (0, exports.confirmationReplyIntent)(text);
881
+ if (!intent)
882
+ return false;
883
+ if (this.mode === 'voice')
884
+ this.interrupt();
885
+ const approved = intent === 'approve';
886
+ if (this.view?.resolvePendingConfirmation(approved))
887
+ return true;
888
+ this.pendingConfirmation = null;
889
+ this.send('confirmation.result', {
890
+ actionId: pending.actionId,
891
+ proposalChecksum: pending.proposalChecksum,
892
+ approved,
893
+ });
894
+ if (!approved)
895
+ this.pendingAction = false;
896
+ return true;
897
+ }
898
+ cancelConfirmationForNewRequest() {
899
+ const pending = this.pendingConfirmation;
900
+ if (!pending)
901
+ return;
902
+ if (this.mode === 'voice')
903
+ this.interrupt();
904
+ if (this.view?.resolvePendingConfirmation(false))
905
+ return;
906
+ this.pendingConfirmation = null;
907
+ this.pendingAction = false;
908
+ this.send('confirmation.result', {
909
+ actionId: pending.actionId,
910
+ proposalChecksum: pending.proposalChecksum,
911
+ approved: false,
912
+ });
913
+ }
914
+ async handleServerMessage(message) {
915
+ const payload = message.payload;
916
+ switch (message.type) {
917
+ case 'control.ready':
918
+ if (typeof payload.heartbeatSeconds === 'number') {
919
+ this.heartbeatSeconds = payload.heartbeatSeconds;
920
+ if (this.channel?.leaseToken) {
921
+ this.channel.startHeartbeat(this.heartbeatSeconds);
922
+ }
923
+ }
924
+ return;
925
+ case 'presence.state': {
926
+ const state = this.string(payload.state);
927
+ if (this.view instanceof voicePresenceUi_js_1.VoicePresenceView &&
928
+ [
929
+ 'ready',
930
+ 'listening',
931
+ 'thinking',
932
+ 'speaking',
933
+ 'acting',
934
+ 'awaiting_confirmation',
935
+ 'reconnecting',
936
+ 'ending',
937
+ ].includes(state) &&
938
+ this.result) {
939
+ this.view.applyPresenceState(state, message.sequence, message.sessionId, this.context?.currentPageId ?? null, this.channel?.leaseToken ?? 0);
940
+ }
941
+ return;
942
+ }
943
+ case 'caption.segment': {
944
+ const text = this.string(payload.text);
945
+ const speaker = payload.speaker === 'visitor' ? 'user' : 'assistant';
946
+ if (text) {
947
+ this.view?.addCaption(speaker, text, payload.final === true);
948
+ if (speaker === 'user' && payload.final === true) {
949
+ if (!this.resolvePendingConfirmationReply(text)) {
950
+ this.cancelConfirmationForNewRequest();
951
+ }
952
+ }
953
+ }
954
+ return;
955
+ }
956
+ case 'presentation.intent':
957
+ case 'presentation.clear': {
958
+ const intent = (0, presentation_js_1.parsePresentationIntent)(payload);
959
+ if (!intent || !this.presentation) {
960
+ if (intent) {
961
+ this.sendPresentationResult({
962
+ presentationId: intent.presentationId,
963
+ status: 'unsupported',
964
+ reasonCode: 'PRESENTATION_DISABLED',
965
+ pageId: this.context?.currentPageId ?? 'page_unknown',
966
+ domRevision: intent.domRevision,
967
+ shownAt: null,
968
+ });
969
+ }
970
+ return;
971
+ }
972
+ this.sendPresentationResult(this.presentation.render(intent));
973
+ return;
974
+ }
975
+ case 'experience.notice': {
976
+ const code = this.string(payload.code);
977
+ if (code === 'AUDIO_PLAYBACK_BLOCKED') {
978
+ this.view?.setStatus('Tap the voice button to resume audio.');
979
+ }
980
+ return;
981
+ }
982
+ case 'assistant.state': {
983
+ const state = payload.state;
984
+ if (state === 'listening' ||
985
+ state === 'thinking' ||
986
+ state === 'ready') {
987
+ this.view?.setAssistantState(state);
988
+ }
989
+ return;
990
+ }
991
+ case 'assistant.caption':
992
+ case 'user.caption': {
993
+ const text = typeof payload.text === 'string' ? payload.text : '';
994
+ if (!text)
995
+ return;
996
+ const role = message.type.startsWith('assistant')
997
+ ? 'assistant'
998
+ : 'user';
999
+ this.view?.addCaption(role, text, payload.final === true);
1000
+ if (role === 'user' && payload.final === true) {
1001
+ if (!this.resolvePendingConfirmationReply(text)) {
1002
+ this.cancelConfirmationForNewRequest();
1003
+ }
1004
+ }
1005
+ if (role === 'assistant' && message.v === 1) {
1006
+ this.view?.setAssistantState('speaking');
1007
+ }
1008
+ return;
1009
+ }
1010
+ case 'assistant.interrupted':
1011
+ this.presentation?.clear('all');
1012
+ this.media?.endPushToTalk();
1013
+ this.pushActive = false;
1014
+ this.view?.interruptPlayback();
1015
+ return;
1016
+ case 'voice.turn_mode': {
1017
+ const manual = payload.mode === 'manual';
1018
+ this.manualTurn = manual;
1019
+ this.pushActive = false;
1020
+ this.media?.setManualTurn(manual);
1021
+ this.view?.setManualTurn(manual);
1022
+ this.updateResumeManualTurn();
1023
+ return;
1024
+ }
1025
+ case 'voice.push.ready':
1026
+ if (!this.manualTurn)
1027
+ return;
1028
+ this.pushActive = true;
1029
+ this.media?.beginPushToTalk();
1030
+ this.view?.setPushActive(true);
1031
+ return;
1032
+ case 'voice.push.committed':
1033
+ this.pushActive = false;
1034
+ this.view?.setPushActive(false);
1035
+ return;
1036
+ case 'confirmation.request': {
1037
+ this.presentation?.clear('all');
1038
+ const actionId = this.string(payload.actionId);
1039
+ const proposalChecksum = this.string(payload.proposalChecksum);
1040
+ const expiresAt = this.string(payload.expiresAt);
1041
+ if (!actionId || !proposalChecksum || !expiresAt)
1042
+ return;
1043
+ this.pendingAction = true;
1044
+ this.pendingConfirmation = { actionId, proposalChecksum };
1045
+ this.view?.showConfirmation({
1046
+ actionId,
1047
+ proposalChecksum,
1048
+ action: this.string(payload.action) ||
1049
+ this.string(payload.title) ||
1050
+ this.view?.message('connectedTool') || '',
1051
+ targetLabel: this.string(payload.targetLabel) ||
1052
+ this.string(payload.title) ||
1053
+ this.view?.message('approvedService') || '',
1054
+ reason: this.string(payload.reason) || this.string(payload.description),
1055
+ expectedEffect: this.string(payload.expectedEffect) ||
1056
+ this.view?.message('approvedToolEffect') || '',
1057
+ expiresAt,
1058
+ });
1059
+ void this.confirmSafetySurface(actionId, proposalChecksum);
1060
+ return;
1061
+ }
1062
+ case 'action.grant':
1063
+ await this.executeGrant(payload);
1064
+ return;
1065
+ case 'action.verified':
1066
+ this.pendingAction = false;
1067
+ this.view?.showActionState(payload.verified === true ? 'verified' : 'failed');
1068
+ return;
1069
+ case 'action.revoked':
1070
+ this.pendingAction = false;
1071
+ this.pendingConfirmation = null;
1072
+ this.view?.dismissActionState();
1073
+ return;
1074
+ case 'context.resync_required':
1075
+ await this.context?.captureAndPublish();
1076
+ return;
1077
+ case 'visual_context.request': {
1078
+ const requestId = this.string(payload.requestId);
1079
+ const functionCallId = this.string(payload.functionCallId);
1080
+ const expiresAt = this.string(payload.expiresAt);
1081
+ const expiresAtMs = Date.parse(expiresAt);
1082
+ const approvalMode = payload.approvalMode === 'automatic' ? 'automatic' : 'manual';
1083
+ if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(requestId) ||
1084
+ !functionCallId ||
1085
+ functionCallId.length > 256 ||
1086
+ !Number.isFinite(expiresAtMs) ||
1087
+ expiresAtMs <= Date.now() ||
1088
+ expiresAtMs > Date.now() + 90000) {
1089
+ return;
1090
+ }
1091
+ const request = {
1092
+ requestId,
1093
+ functionCallId,
1094
+ reason: this.string(payload.reason).slice(0, 500),
1095
+ expiresAt,
1096
+ approvalMode,
1097
+ };
1098
+ if (!this.scopes.includes('visual_context') ||
1099
+ !this.scopes.includes('page_context') ||
1100
+ !this.bootstrap?.capabilities?.visualContext ||
1101
+ !this.context) {
1102
+ this.send('visual_context.result', {
1103
+ requestId,
1104
+ functionCallId,
1105
+ approved: false,
1106
+ reasonCode: 'CAPTURE_UNAVAILABLE',
1107
+ });
1108
+ return;
1109
+ }
1110
+ if (approvalMode === 'automatic') {
1111
+ void this.resolveVisualContext(request, true, false);
1112
+ }
1113
+ else {
1114
+ this.view?.showVisualRequest(request);
1115
+ }
1116
+ return;
1117
+ }
1118
+ case 'session.state': {
1119
+ const state = this.string(payload.state);
1120
+ if (state === 'paused')
1121
+ this.view?.setPaused(true);
1122
+ if (state === 'active_text' || state === 'active_voice') {
1123
+ this.view?.setPaused(false);
1124
+ }
1125
+ if (TERMINAL_STATES.has(state)) {
1126
+ this.finishFromServer(this.string(payload.reasonCode));
1127
+ }
1128
+ return;
1129
+ }
1130
+ case 'provider.reconnecting':
1131
+ this.view?.showConnecting(true, this.mode);
1132
+ this.state('reconnecting');
1133
+ return;
1134
+ case 'provider.warning':
1135
+ this.view?.setStatus(this.string(payload.message) || this.view.message('error'));
1136
+ return;
1137
+ case 'limit.reached':
1138
+ this.view?.showLimit(this.string(payload.message));
1139
+ return;
1140
+ case 'handoff.state': {
1141
+ const state = this.string(payload.state);
1142
+ const configuredMessage = this.string(payload.message);
1143
+ this.view?.setStatus(state === 'connected' && configuredMessage
1144
+ ? configuredMessage
1145
+ : state === 'connected'
1146
+ ? this.view.message('humanConnected')
1147
+ : state === 'connecting'
1148
+ ? this.view.message('humanConnecting')
1149
+ : this.view.message('humanUnavailable'));
1150
+ if (state === 'connected') {
1151
+ this.pendingAction = false;
1152
+ this.presentation?.clear('all');
1153
+ }
1154
+ return;
1155
+ }
1156
+ case 'error': {
1157
+ const code = this.string(payload.code);
1158
+ if (this.pendingAction && ACTION_SCOPED_ERROR_CODES.has(code)) {
1159
+ this.pendingAction = false;
1160
+ this.pendingConfirmation = null;
1161
+ this.view?.showActionState('failed');
1162
+ await this.context?.captureAndPublish();
1163
+ return;
1164
+ }
1165
+ const retryable = payload.retryable === true;
1166
+ this.view?.showError(this.string(payload.message), retryable);
1167
+ if (!retryable)
1168
+ this.clearResume();
1169
+ return;
1170
+ }
1171
+ }
1172
+ }
1173
+ async executeGrant(payload) {
1174
+ const grant = this.parseGrant(payload);
1175
+ if (!grant || !this.context || !this.channel)
1176
+ return;
1177
+ this.pendingAction = true;
1178
+ this.view?.showActionState('running');
1179
+ let result;
1180
+ if (!this.view?.safetySurfaceVisible()) {
1181
+ result = {
1182
+ grant: grant.grant,
1183
+ actionId: grant.actionId,
1184
+ status: 'blocked',
1185
+ observed: {
1186
+ url: `${location.origin}${location.pathname}`,
1187
+ revision: this.context.currentRevision,
1188
+ targetState: 'unchanged',
1189
+ safeSummary: 'The isolated safety controls were not visible.',
1190
+ },
1191
+ };
1192
+ }
1193
+ else {
1194
+ result = await this.context.execute(grant);
1195
+ }
1196
+ const channel = this.channel;
1197
+ const messageId = this.send('action.result', result);
1198
+ const deferredNavigation = await this.context.completeActionExecution(grant.actionId);
1199
+ if (!deferredNavigation || !channel || !messageId)
1200
+ return;
1201
+ const acknowledged = await channel.waitForAck(messageId);
1202
+ if (acknowledged &&
1203
+ channel === this.channel &&
1204
+ !this.ended &&
1205
+ new URL(deferredNavigation).origin === window.location.origin) {
1206
+ window.location.assign(deferredNavigation);
1207
+ }
1208
+ else if (!acknowledged) {
1209
+ this.view?.showActionState('failed');
1210
+ }
1211
+ }
1212
+ async confirmSafetySurface(actionId, proposalChecksum) {
1213
+ for (let attempt = 0; attempt < 4; attempt += 1) {
1214
+ await new Promise((resolve) => requestAnimationFrame(() => resolve()));
1215
+ if (this.pendingConfirmation?.actionId !== actionId ||
1216
+ this.pendingConfirmation.proposalChecksum !== proposalChecksum) {
1217
+ return;
1218
+ }
1219
+ if (this.view?.safetySurfaceVisible())
1220
+ return;
1221
+ }
1222
+ if (this.pendingConfirmation?.actionId !== actionId ||
1223
+ this.pendingConfirmation.proposalChecksum !== proposalChecksum) {
1224
+ return;
1225
+ }
1226
+ this.pendingAction = false;
1227
+ this.view?.cancelPendingConfirmation();
1228
+ }
1229
+ async resolveVisualContext(request, approved, requireVisibleSafetySurface) {
1230
+ const showManualResult = request.approvalMode === 'manual';
1231
+ if (!approved) {
1232
+ this.send('visual_context.result', {
1233
+ requestId: request.requestId,
1234
+ functionCallId: request.functionCallId,
1235
+ approved: false,
1236
+ reasonCode: 'VISITOR_DECLINED',
1237
+ });
1238
+ if (showManualResult)
1239
+ this.view?.showVisualResult(false);
1240
+ return;
1241
+ }
1242
+ if (Date.parse(request.expiresAt) <= Date.now()) {
1243
+ this.send('visual_context.result', {
1244
+ requestId: request.requestId,
1245
+ functionCallId: request.functionCallId,
1246
+ approved: false,
1247
+ reasonCode: 'REQUEST_EXPIRED',
1248
+ });
1249
+ if (showManualResult)
1250
+ this.view?.showVisualResult(false);
1251
+ return;
1252
+ }
1253
+ if (requireVisibleSafetySurface && !this.view?.safetySurfaceVisible()) {
1254
+ this.send('visual_context.result', {
1255
+ requestId: request.requestId,
1256
+ functionCallId: request.functionCallId,
1257
+ approved: false,
1258
+ reasonCode: 'SAFETY_SURFACE_HIDDEN',
1259
+ });
1260
+ if (showManualResult)
1261
+ this.view?.showVisualResult(false);
1262
+ return;
1263
+ }
1264
+ const context = this.context;
1265
+ if (!context) {
1266
+ this.send('visual_context.result', {
1267
+ requestId: request.requestId,
1268
+ functionCallId: request.functionCallId,
1269
+ approved: false,
1270
+ reasonCode: 'CAPTURE_UNAVAILABLE',
1271
+ });
1272
+ if (showManualResult)
1273
+ this.view?.showVisualResult(false);
1274
+ return;
1275
+ }
1276
+ await context.captureAndPublish();
1277
+ if (Date.parse(request.expiresAt) <= Date.now()) {
1278
+ this.send('visual_context.result', {
1279
+ requestId: request.requestId,
1280
+ functionCallId: request.functionCallId,
1281
+ approved: false,
1282
+ reasonCode: 'REQUEST_EXPIRED',
1283
+ });
1284
+ if (showManualResult)
1285
+ this.view?.showVisualResult(false);
1286
+ return;
1287
+ }
1288
+ const capture = context.captureVisualContext();
1289
+ if (!capture) {
1290
+ this.send('visual_context.result', {
1291
+ requestId: request.requestId,
1292
+ functionCallId: request.functionCallId,
1293
+ approved: false,
1294
+ reasonCode: 'CAPTURE_UNAVAILABLE',
1295
+ });
1296
+ if (showManualResult)
1297
+ this.view?.showVisualResult(false);
1298
+ return;
1299
+ }
1300
+ this.send('visual_context.result', {
1301
+ requestId: request.requestId,
1302
+ functionCallId: request.functionCallId,
1303
+ approved: true,
1304
+ capture,
1305
+ });
1306
+ if (showManualResult)
1307
+ this.view?.showVisualResult(true);
1308
+ }
1309
+ parseGrant(payload) {
1310
+ const action = this.string(payload.action);
1311
+ if (![
1312
+ 'scroll',
1313
+ 'highlight',
1314
+ 'focus',
1315
+ 'click',
1316
+ 'type',
1317
+ 'select',
1318
+ 'submit',
1319
+ ].includes(action)) {
1320
+ return null;
1321
+ }
1322
+ const grant = this.string(payload.grant);
1323
+ const actionId = this.string(payload.actionId);
1324
+ const targetId = this.string(payload.targetId);
1325
+ const expiresAt = this.string(payload.expiresAt);
1326
+ const domRevision = payload.domRevision;
1327
+ const fencingToken = payload.fencingToken;
1328
+ if (!grant ||
1329
+ !actionId ||
1330
+ !targetId ||
1331
+ !expiresAt ||
1332
+ typeof domRevision !== 'number' ||
1333
+ typeof fencingToken !== 'number' ||
1334
+ fencingToken !== this.channel?.leaseToken) {
1335
+ return null;
1336
+ }
1337
+ return {
1338
+ grant,
1339
+ actionId,
1340
+ action: action,
1341
+ targetId,
1342
+ domRevision,
1343
+ fencingToken,
1344
+ expiresAt,
1345
+ ...(typeof payload.value === 'string' ? { value: payload.value } : {}),
1346
+ ...(typeof payload.optionValueId === 'string'
1347
+ ? { optionValueId: payload.optionValueId }
1348
+ : {}),
1349
+ ...(typeof payload.desiredChecked === 'boolean'
1350
+ ? { desiredChecked: payload.desiredChecked }
1351
+ : {}),
1352
+ };
1353
+ }
1354
+ updateMute(muted) {
1355
+ this.media?.setMuted(muted);
1356
+ this.send('media.state', {
1357
+ microphone: muted
1358
+ ? 'muted'
1359
+ : this.mode === 'voice'
1360
+ ? 'active'
1361
+ : 'unavailable',
1362
+ playback: this.mode === 'voice' ? 'active' : 'paused',
1363
+ captions: true,
1364
+ });
1365
+ }
1366
+ interrupt() {
1367
+ if (this.mode !== 'voice')
1368
+ return;
1369
+ this.presentation?.clear('all');
1370
+ this.media?.endPushToTalk();
1371
+ this.pushActive = false;
1372
+ this.view?.setPushActive(false);
1373
+ this.send('session.interrupt', {});
1374
+ }
1375
+ updateVoiceTurnMode(manual) {
1376
+ if (this.mode !== 'voice' || !this.channel?.isOpen)
1377
+ return;
1378
+ if (!manual && this.bootstrap?.voice?.turnMode === 'manual')
1379
+ return;
1380
+ if (this.pushActive) {
1381
+ this.media?.endPushToTalk();
1382
+ this.pushActive = false;
1383
+ this.view?.setPushActive(false);
1384
+ }
1385
+ this.send('voice.turn_mode', { mode: manual ? 'manual' : 'configured' });
1386
+ }
1387
+ updatePushToTalk(active) {
1388
+ if (this.mode !== 'voice' ||
1389
+ !this.manualTurn ||
1390
+ !this.channel?.isOpen) {
1391
+ return;
1392
+ }
1393
+ if (active) {
1394
+ if (!this.pushActive)
1395
+ this.send('voice.push.start', {});
1396
+ return;
1397
+ }
1398
+ if (!this.pushActive)
1399
+ return;
1400
+ this.media?.endPushToTalk();
1401
+ this.pushActive = false;
1402
+ this.view?.setPushActive(false);
1403
+ this.send('voice.push.commit', {});
1404
+ }
1405
+ async updatePause(paused) {
1406
+ if (!this.result || !this.channel?.leaseToken)
1407
+ return;
1408
+ try {
1409
+ this.send(paused ? 'session.pause' : 'session.resume', {
1410
+ actions: true,
1411
+ });
1412
+ }
1413
+ catch {
1414
+ await this.runtime.pauseOrResume(this.result, this.channel.leaseToken, paused ? 'pause' : 'resume');
1415
+ }
1416
+ }
1417
+ updateCaptions(enabled) {
1418
+ this.send('media.state', {
1419
+ microphone: this.mode === 'voice' ? 'active' : 'unavailable',
1420
+ playback: this.mode === 'voice' ? 'active' : 'paused',
1421
+ captions: enabled,
1422
+ });
1423
+ }
1424
+ updateVisualPause(paused) {
1425
+ if (!this.visualConsentGranted || !this.channel?.isOpen)
1426
+ return;
1427
+ this.scopes = paused
1428
+ ? this.scopes.filter((scope) => scope !== 'visual_context')
1429
+ : [...new Set([...this.scopes, 'visual_context'])];
1430
+ this.send('consent.update', { scopes: this.scopes, locale: this.locale });
1431
+ const record = this.readResume();
1432
+ if (record) {
1433
+ this.storeResume({ ...record, scopes: [...this.scopes] });
1434
+ }
1435
+ }
1436
+ async switchToText() {
1437
+ if (this.mode === 'text' || this.isVoicePresence())
1438
+ return;
1439
+ this.media?.switchToTextOnly();
1440
+ this.mode = 'text';
1441
+ this.manualTurn = false;
1442
+ this.pushActive = false;
1443
+ this.send('session.mode', { mode: 'text' });
1444
+ this.send('media.state', {
1445
+ microphone: 'unavailable',
1446
+ playback: 'paused',
1447
+ captions: true,
1448
+ });
1449
+ const record = this.readResume();
1450
+ if (record) {
1451
+ this.storeResume({ ...record, mode: 'text', manualTurn: false });
1452
+ }
1453
+ this.view?.setMuted(false);
1454
+ this.view?.configureVoiceControls(false, false, false);
1455
+ this.view?.setStatus(this.view.message('textMode'));
1456
+ }
1457
+ async submitFeedback(outcome) {
1458
+ if (!this.result)
1459
+ return;
1460
+ try {
1461
+ await this.runtime.feedback(this.result, { rating: null, outcome });
1462
+ }
1463
+ catch {
1464
+ // Feedback is optional and never reopens a terminal session.
1465
+ }
1466
+ }
1467
+ retry() {
1468
+ const resume = this.readResume();
1469
+ if (resume && this.lock.acquire()) {
1470
+ this.view?.showConnecting(true, resume.mode);
1471
+ return this.resume(resume);
1472
+ }
1473
+ if (this.lastLaunch) {
1474
+ return this.launch(this.lastLaunch.mode, this.lastLaunch.scopes);
1475
+ }
1476
+ }
1477
+ scheduleReconnect() {
1478
+ if (this.ended ||
1479
+ this.reconnecting ||
1480
+ this.destroyed ||
1481
+ !navigator.onLine) {
1482
+ return;
1483
+ }
1484
+ const record = this.readResume();
1485
+ if (!record || this.reconnectAttempts >= 5) {
1486
+ this.view?.showError(this.view.message('reconnectFailed'), false);
1487
+ return;
1488
+ }
1489
+ this.reconnectAttempts += 1;
1490
+ const delay = Math.min(8000, 500 * 2 ** (this.reconnectAttempts - 1));
1491
+ this.view?.showConnecting(true, record.mode);
1492
+ this.cleanupTransport();
1493
+ setTimeout(() => {
1494
+ if (!this.ended && navigator.onLine)
1495
+ void this.resume(record);
1496
+ }, delay + Math.floor(Math.random() * 250));
1497
+ }
1498
+ createMedia() {
1499
+ return new media_js_1.RealtimeMedia(this.runtime, {
1500
+ onLocalStream: (stream) => {
1501
+ if (this.view instanceof voicePresenceUi_js_1.VoicePresenceView) {
1502
+ this.view.setLocalStream(stream);
1503
+ }
1504
+ },
1505
+ onRemoteStream: (stream) => this.view?.setRemoteStream(stream),
1506
+ onConnectionState: (state) => {
1507
+ if (state === 'failed')
1508
+ this.scheduleReconnect();
1509
+ },
1510
+ onMicrophoneUnavailable: () => {
1511
+ if (this.isVoicePresence()) {
1512
+ this.send('experience.end', { reason: 'microphone_revoked' });
1513
+ this.finishFromServer('MICROPHONE_REVOKED');
1514
+ }
1515
+ else {
1516
+ void this.switchToText();
1517
+ }
1518
+ },
1519
+ });
1520
+ }
1521
+ finishFromServer(reasonCode) {
1522
+ if (this.ended)
1523
+ return;
1524
+ this.ended = true;
1525
+ this.clearResume();
1526
+ this.cleanupTransport();
1527
+ this.lock.release();
1528
+ this.view?.showEnded(reasonCode === 'VISITOR_INACTIVITY_LIMIT'
1529
+ ? this.view.message('idleEnded')
1530
+ : reasonCode === 'CREDIT_LIMIT_REACHED' ||
1531
+ reasonCode === 'CREDITS_UNAVAILABLE'
1532
+ ? this.view.message('limitReached')
1533
+ : undefined);
1534
+ this.state('ended');
1535
+ }
1536
+ cleanupTransport(options = {}) {
1537
+ if (!options.preserveConnectionRequest) {
1538
+ this.connectionAbort?.abort();
1539
+ this.connectionAbort = null;
1540
+ }
1541
+ this.presentation?.destroy();
1542
+ this.presentation = null;
1543
+ this.context?.stop();
1544
+ this.context = null;
1545
+ if (!options.preserveControlChannel) {
1546
+ this.channel?.close();
1547
+ this.channel = null;
1548
+ }
1549
+ this.media?.close();
1550
+ this.media = null;
1551
+ this.pendingAction = false;
1552
+ this.pendingConfirmation = null;
1553
+ this.pushActive = false;
1554
+ }
1555
+ async requestRuntimeEnd(result, fencingToken, fallbackChannel, terminalMessageId) {
1556
+ if (terminalMessageId &&
1557
+ fallbackChannel &&
1558
+ await fallbackChannel.waitForAck(terminalMessageId, 2000)) {
1559
+ return;
1560
+ }
1561
+ const controller = new AbortController();
1562
+ const timeout = setTimeout(() => controller.abort(), 2000);
1563
+ try {
1564
+ await this.runtime.end(result, fencingToken, controller.signal);
1565
+ }
1566
+ catch {
1567
+ try {
1568
+ fallbackChannel?.send('session.end', { reason: 'visitor' });
1569
+ }
1570
+ catch {
1571
+ // Runtime reconciliation finalizes sessions whose client disappeared.
1572
+ }
1573
+ }
1574
+ finally {
1575
+ clearTimeout(timeout);
1576
+ }
1577
+ }
1578
+ send(type, payload) {
1579
+ if (!this.channel?.isOpen)
1580
+ return null;
1581
+ try {
1582
+ return this.channel.send(type, payload);
1583
+ }
1584
+ catch {
1585
+ this.scheduleReconnect();
1586
+ return null;
1587
+ }
1588
+ }
1589
+ replayContext() {
1590
+ const meta = this.app.getSessionMeta();
1591
+ const sessionId = String(meta.sessionID ?? '');
1592
+ return /^\d{1,32}$/.test(sessionId)
1593
+ ? { replay: { sessionId, tabId: this.tabId } }
1594
+ : {};
1595
+ }
1596
+ visitorId() {
1597
+ const key = `__oxvo_ai_live_assist_visitor_${this.siteKey}`;
1598
+ const meta = this.app.getSessionMeta();
1599
+ const trackerId = typeof meta.userUUID === 'string' ? meta.userUUID : '';
1600
+ if (/^[A-Za-z0-9_.:-]{8,128}$/.test(trackerId))
1601
+ return trackerId;
1602
+ try {
1603
+ const stored = localStorage.getItem(key) ?? '';
1604
+ if (/^[A-Za-z0-9_.:-]{8,128}$/.test(stored))
1605
+ return stored;
1606
+ const created = randomId('visitor');
1607
+ localStorage.setItem(key, created);
1608
+ return created;
1609
+ }
1610
+ catch {
1611
+ return randomId('visitor');
1612
+ }
1613
+ }
1614
+ readResume() {
1615
+ try {
1616
+ return parseResumeRecord(sessionStorage.getItem(this.storageKey));
1617
+ }
1618
+ catch {
1619
+ return null;
1620
+ }
1621
+ }
1622
+ writeResume(result, mode, scopes, visualConsentGranted = scopes.includes('visual_context'), clientSequence = 0) {
1623
+ this.storeResume({
1624
+ sessionId: result.sessionId,
1625
+ resumeCredential: result.resumeCredential,
1626
+ tabId: this.tabId,
1627
+ clientSequence,
1628
+ mode,
1629
+ scopes: [...scopes],
1630
+ locale: this.locale,
1631
+ expiresAt: Date.now() + STORAGE_TTL_MS,
1632
+ visualConsentGranted,
1633
+ manualTurn: mode === 'voice' && this.manualTurn,
1634
+ protocolVersion: result.protocolVersion,
1635
+ experienceMode: result.experienceMode,
1636
+ fallbackIntent: this.activeFallback,
1637
+ });
1638
+ }
1639
+ updateResumeManualTurn() {
1640
+ const record = this.readResume();
1641
+ if (record)
1642
+ this.storeResume({ ...record, manualTurn: this.manualTurn });
1643
+ }
1644
+ updateResumeSequence(clientSequence) {
1645
+ const record = this.readResume();
1646
+ if (record && record.clientSequence !== clientSequence) {
1647
+ this.storeResume({ ...record, clientSequence });
1648
+ }
1649
+ }
1650
+ storeResume(record) {
1651
+ try {
1652
+ sessionStorage.setItem(this.storageKey, JSON.stringify(record));
1653
+ }
1654
+ catch {
1655
+ // Resume is an enhancement; active in-memory control remains valid.
1656
+ }
1657
+ }
1658
+ clearResume() {
1659
+ try {
1660
+ sessionStorage.removeItem(this.storageKey);
1661
+ }
1662
+ catch {
1663
+ // Ignore unavailable storage.
1664
+ }
1665
+ }
1666
+ publicError(error) {
1667
+ if (error instanceof client_js_1.RuntimeClientError)
1668
+ return error.message;
1669
+ if (error instanceof DOMException && error.name === 'NotAllowedError') {
1670
+ return this.isVoicePresence()
1671
+ ? 'Microphone access was not granted. Allow microphone access to use voice assist.'
1672
+ : 'Microphone access was not granted. You can continue with text.';
1673
+ }
1674
+ return 'AI Live Assist is temporarily unavailable.';
1675
+ }
1676
+ retryable(error) {
1677
+ return error instanceof client_js_1.RuntimeClientError ? error.retryable : false;
1678
+ }
1679
+ string(value) {
1680
+ return typeof value === 'string' ? value.slice(0, 2000) : '';
1681
+ }
1682
+ state(value) {
1683
+ try {
1684
+ this.options.onStateChange?.(value);
1685
+ }
1686
+ catch {
1687
+ // Consumer callbacks cannot interrupt safety state transitions.
1688
+ }
1689
+ }
1690
+ sendPresentationResult(value) {
1691
+ this.send('presentation.result', { ...value });
1692
+ }
1693
+ isVoicePresence() {
1694
+ return (this.bootstrap?.experienceMode ?? 'standard') === 'voice_presence';
1695
+ }
1696
+ lowPowerMode() {
1697
+ const connection = navigator.connection;
1698
+ const configured = this.bootstrap?.appearance?.voicePresence?.visual.lowPowerMode;
1699
+ if (configured === 'always')
1700
+ return true;
1701
+ if (configured === 'never')
1702
+ return false;
1703
+ return connection?.saveData === true || connection?.effectiveType === '2g';
1704
+ }
1705
+ bindWindowLifecycle() {
1706
+ window.addEventListener('pagehide', this.onPageHide);
1707
+ window.addEventListener('pageshow', this.onPageShow);
1708
+ window.addEventListener('online', this.onOnline);
1709
+ window.addEventListener('offline', this.onOffline);
1710
+ window.addEventListener('focus', this.onWindowFocus);
1711
+ document.addEventListener('visibilitychange', this.onVisibilityChange);
1712
+ document.addEventListener('pointerdown', this.onVisitorControl, true);
1713
+ document.addEventListener('keydown', this.onVisitorControl, true);
1714
+ }
1715
+ requestFocusedHandoff() {
1716
+ if (document.visibilityState === 'hidden' ||
1717
+ this.destroyed ||
1718
+ this.ended ||
1719
+ this.result ||
1720
+ this.reconnecting ||
1721
+ !this.bootstrap ||
1722
+ !this.view ||
1723
+ !this.isVoicePresence()) {
1724
+ return;
1725
+ }
1726
+ this.lock.requestTakeover();
1727
+ }
1728
+ handoffToFocusedTab(requesterId, requestId) {
1729
+ if (this.destroyed ||
1730
+ this.ended ||
1731
+ !this.isVoicePresence()) {
1732
+ return;
1733
+ }
1734
+ if (this.reconnecting) {
1735
+ this.pendingFocusedHandoff = { requesterId, requestId };
1736
+ return;
1737
+ }
1738
+ if (!this.result)
1739
+ return;
1740
+ this.pendingFocusedHandoff = null;
1741
+ const record = this.readResume();
1742
+ if (!record || !record.tabId)
1743
+ return;
1744
+ this.cleanupTransport();
1745
+ this.result = null;
1746
+ this.clearResume();
1747
+ if (!this.lock.handoff(requesterId, requestId, record)) {
1748
+ this.storeResume(record);
1749
+ if (this.lock.acquire()) {
1750
+ this.view?.showConnecting(true, record.mode);
1751
+ void this.resume(record);
1752
+ }
1753
+ }
1754
+ }
1755
+ async acceptFocusedTabHandoff(payload) {
1756
+ const record = parseResumeRecord(JSON.stringify(payload));
1757
+ if (!record ||
1758
+ !record.tabId ||
1759
+ record.protocolVersion !== 2 ||
1760
+ record.experienceMode !== 'voice_presence') {
1761
+ this.lock.release();
1762
+ return;
1763
+ }
1764
+ this.tabId = record.tabId;
1765
+ this.storeResume(record);
1766
+ this.ended = false;
1767
+ this.view?.showActiveElsewhere(false);
1768
+ if (!this.bootstrap || !this.view || this.reconnecting || this.destroyed)
1769
+ return;
1770
+ this.view.showConnecting(true, record.mode);
1771
+ await this.resume(record);
1772
+ }
1773
+ }
1774
+ exports.default = AiLiveAssist;