@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,2182 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VoicePresenceView = void 0;
4
+ const lucide_1 = require("lucide");
5
+ const experienceState_js_1 = require("./experienceState.js");
6
+ const messages_js_1 = require("./messages.js");
7
+ const placement_js_1 = require("./placement.js");
8
+ const safeSvg_js_1 = require("./safeSvg.js");
9
+ const compactUiText = (value, maxLength) => {
10
+ const normalized = value.replace(/\s+/g, " ").trim();
11
+ return normalized.length > maxLength
12
+ ? `${normalized.slice(0, Math.max(1, maxLength - 3)).trimEnd()}...`
13
+ : normalized;
14
+ };
15
+ const confirmationPresentation = (value, fallbackTitle, fallbackApprove) => {
16
+ const action = value.action.trim().toLowerCase();
17
+ const presentation = {
18
+ click: {
19
+ title: "Continue with this action?",
20
+ intro: "This will activate a page control.",
21
+ approve: "Continue",
22
+ },
23
+ type: {
24
+ title: "Enter this information?",
25
+ intro: "This will enter information on this page.",
26
+ approve: "Enter information",
27
+ },
28
+ select: {
29
+ title: "Apply this selection?",
30
+ intro: "This will change a selection on this page.",
31
+ approve: "Apply selection",
32
+ },
33
+ submit: {
34
+ title: "Save these changes?",
35
+ intro: "This will submit a form on this page.",
36
+ approve: "Save changes",
37
+ },
38
+ }[action] ?? {
39
+ title: fallbackTitle,
40
+ intro: "This will perform an action on this page.",
41
+ approve: fallbackApprove,
42
+ };
43
+ const target = compactUiText(value.targetLabel, 100);
44
+ const effect = compactUiText(value.expectedEffect || value.reason, 180);
45
+ const details = [
46
+ presentation.intro,
47
+ target ? `Target: ${target}.` : "",
48
+ effect ? `Expected result: ${effect}` : "",
49
+ ].filter(Boolean);
50
+ return { ...presentation, copy: details.join(" ") };
51
+ };
52
+ const INTRO_WAKE_MS = 620;
53
+ const MAX_LIVE_ANNOUNCEMENTS = 6;
54
+ const MAX_VISIBLE_SUBTITLES = 3;
55
+ const styles = `
56
+ :host {
57
+ all: initial;
58
+ position: fixed;
59
+ inset: 0;
60
+ z-index: 2147483647;
61
+ pointer-events: none;
62
+ color-scheme: light;
63
+ font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
64
+ letter-spacing: 0;
65
+ --voice-accent: #2563eb;
66
+ --voice-inline-offset: 20px;
67
+ --voice-block-offset: 20px;
68
+ --voice-orb-size: 58px;
69
+ --voice-level: 0;
70
+ --voice-text: #18212b;
71
+ --voice-muted: #66717e;
72
+ --voice-border: rgba(148, 163, 184, .34);
73
+ --voice-surface: rgba(255, 255, 255, .96);
74
+ }
75
+ *, *::before, *::after { box-sizing: border-box; letter-spacing: 0; }
76
+ button, input, summary { font: inherit; }
77
+ button { cursor: pointer; }
78
+ [hidden] { display: none !important; }
79
+ button:focus-visible, input:focus-visible, summary:focus-visible, a:focus-visible {
80
+ outline: 3px solid color-mix(in srgb, var(--voice-accent) 48%, white);
81
+ outline-offset: 3px;
82
+ }
83
+ .presence-field {
84
+ position: fixed;
85
+ inset: 0;
86
+ overflow: hidden;
87
+ opacity: 0;
88
+ pointer-events: none;
89
+ z-index: 1;
90
+ contain: strict;
91
+ transition: opacity 680ms cubic-bezier(.22, .72, .28, 1);
92
+ }
93
+ .presence-field::before,
94
+ .presence-field::after {
95
+ content: '';
96
+ position: absolute;
97
+ pointer-events: none;
98
+ transform: translate3d(0, 0, 0);
99
+ }
100
+ .presence-field::before {
101
+ right: -20vw;
102
+ bottom: -20vh;
103
+ width: min(1440px, 124vw);
104
+ height: min(1120px, 116vh);
105
+ background:
106
+ radial-gradient(ellipse 62% 56% at 100% 100%, rgba(102, 224, 211, .40) 0%, rgba(101, 170, 243, .29) 24%, rgba(139, 118, 247, .20) 45%, rgba(243, 139, 203, .12) 61%, transparent 79%),
107
+ radial-gradient(ellipse 124% 17% at 50% 104%, rgba(102, 224, 211, .29) 0%, rgba(109, 150, 246, .19) 39%, rgba(229, 139, 221, .11) 62%, transparent 86%),
108
+ radial-gradient(ellipse 17% 124% at 104% 50%, rgba(246, 151, 202, .27) 0%, rgba(127, 116, 247, .18) 39%, rgba(95, 210, 221, .10) 63%, transparent 86%);
109
+ filter: blur(30px) saturate(1.34);
110
+ opacity: .96;
111
+ }
112
+ .presence-field::after {
113
+ inset: -9%;
114
+ background:
115
+ radial-gradient(ellipse 146% 15% at 50% 102%, rgba(152, 241, 225, .25), rgba(126, 153, 247, .14) 47%, rgba(233, 150, 220, .07) 66%, transparent 88%),
116
+ radial-gradient(ellipse 15% 146% at 102% 50%, rgba(255, 172, 218, .23), rgba(135, 126, 244, .13) 47%, rgba(103, 216, 219, .07) 66%, transparent 88%),
117
+ radial-gradient(ellipse 48% 43% at 95% 95%, rgba(255, 255, 255, .21), rgba(123, 218, 224, .10) 48%, transparent 80%);
118
+ filter: blur(17px) saturate(1.24);
119
+ opacity: .88;
120
+ }
121
+ :host([data-state^="active."]) .presence-field { opacity: .80; }
122
+ :host([data-intensity="subtle"][data-state^="active."]) .presence-field { opacity: .58; }
123
+ :host([data-intensity="immersive"][data-state^="active."]) .presence-field { opacity: .96; }
124
+ :host([data-position="bottom_left"]) .presence-field::before { right: auto; left: -20vw; transform: scaleX(-1) translate3d(0, 0, 0); }
125
+ :host([data-position="bottom_left"]) .presence-field::after { transform: scaleX(-1) translate3d(0, 0, 0); }
126
+ .orb-wrap {
127
+ position: fixed;
128
+ inset-inline-end: max(var(--voice-inline-offset), env(safe-area-inset-right));
129
+ bottom: max(var(--voice-block-offset), env(safe-area-inset-bottom));
130
+ width: var(--voice-orb-size);
131
+ height: var(--voice-orb-size);
132
+ pointer-events: auto;
133
+ z-index: 4;
134
+ }
135
+ :host([data-position="bottom_left"]) .orb-wrap {
136
+ inset-inline-start: max(var(--voice-inline-offset), env(safe-area-inset-left));
137
+ inset-inline-end: auto;
138
+ }
139
+ /* Premium orb themes. Light is the backwards-compatible default. */
140
+ :host,
141
+ :host([data-orb-theme="light"]) {
142
+ --orb-shell-bg:
143
+ radial-gradient(circle at 29% 15%, #fff 0 3%, rgba(255,255,255,.42) 10%, transparent 25%),
144
+ radial-gradient(circle at 80% 84%, rgba(205,157,204,.14), transparent 35%),
145
+ linear-gradient(145deg, #fff 0%, #d5dbe2 34%, #6b7280 65%, #1a1c24 100%);
146
+ --orb-shell-border: rgba(255,255,255,.94);
147
+ --orb-shadow-rest:
148
+ 0 15px 32px rgba(31,34,43,.21),
149
+ 0 4px 9px rgba(31,34,43,.10),
150
+ 0 0 0 1px rgba(80,86,104,.09),
151
+ inset 0 1px 0 #fff;
152
+ --orb-shadow-hover:
153
+ 0 19px 39px rgba(31,34,43,.25),
154
+ 0 6px 12px rgba(31,34,43,.12),
155
+ 0 0 18px rgba(165,145,213,.10),
156
+ inset 0 1px 0 #fff;
157
+ --orb-surface-bg:
158
+ radial-gradient(ellipse 49% 32% at 27% 16%, rgba(255,255,255,.94), transparent 65%),
159
+ conic-gradient(from 216deg, #b8d9d9, #b9c8dd, #c1b7dc, #d6b7cb, #dfd0b2, #b8d9d9);
160
+ --orb-surface-border: rgba(255,255,255,.58);
161
+ --orb-surface-shadow:
162
+ inset 0 1px 0 rgba(255,255,255,.78),
163
+ inset 0 -1px 0 rgba(14,16,22,.20);
164
+ --orb-caustic-bg:
165
+ conic-gradient(from 28deg,
166
+ transparent 0 16%,
167
+ rgba(255,255,255,.44) 21%,
168
+ rgba(157,211,207,.25) 30%,
169
+ transparent 41%,
170
+ rgba(153,139,208,.24) 56%,
171
+ transparent 69%,
172
+ rgba(220,174,203,.20) 79%,
173
+ rgba(224,202,158,.18) 86%,
174
+ transparent 96%);
175
+ --orb-specular-bg:
176
+ radial-gradient(ellipse 46% 23% at 30% 16%, #fff 0%, rgba(255,255,255,.42) 34%, transparent 76%),
177
+ linear-gradient(145deg, rgba(255,255,255,.20) 0 7%, transparent 34%);
178
+ --orb-core-bg:
179
+ radial-gradient(ellipse 59% 39% at 27% 16%, rgba(255,255,255,.46), transparent 60%),
180
+ radial-gradient(circle at 76% 81%, rgba(122,101,186,.14), transparent 44%),
181
+ linear-gradient(145deg, #555c68, #282c35 49%, #08090d);
182
+ --orb-core-border: rgba(255,255,255,.25);
183
+ --orb-core-shadow:
184
+ inset 1px 1px 0 rgba(255,255,255,.25),
185
+ inset -5px -7px 13px rgba(0,0,0,.46),
186
+ 0 5px 12px rgba(5,5,8,.30);
187
+ --orb-core-refraction-bg:
188
+ radial-gradient(ellipse 67% 43% at 25% 13%, rgba(255,255,255,.26), transparent 62%),
189
+ linear-gradient(117deg, transparent 28%, rgba(178,216,221,.11) 45%, rgba(188,160,222,.10) 53%, transparent 70%);
190
+ --orb-rim-color: rgba(244,247,250,.72);
191
+ --orb-rim-accent: rgba(183,222,217,.56);
192
+ --orb-rim-secondary: rgba(179,151,211,.45);
193
+ --orb-rim-shadow:
194
+ 0 0 0 1px rgba(102,108,127,.11),
195
+ 0 0 13px rgba(155,139,201,.10);
196
+ --orb-wave-color: rgba(139,166,192,.44);
197
+ --orb-icon-color: #fff;
198
+ --orb-icon-shadow: drop-shadow(0 1px 2px rgba(0,0,0,.68));
199
+ --orb-caustic-opacity: .46;
200
+ --orb-specular-opacity: .94;
201
+ --orb-core-glint-opacity: .82;
202
+ --orb-speaking-a: rgba(122,151,204,.26);
203
+ --orb-speaking-b: rgba(157,132,198,.14);
204
+ --orb-acting-ring: rgba(157,132,198,.13);
205
+ }
206
+
207
+ :host([data-orb-theme="dark"]) {
208
+ --orb-shell-bg:
209
+ radial-gradient(circle at 30% 16%, rgba(255,255,255,.38) 0 2%, rgba(255,255,255,.10) 10%, transparent 28%),
210
+ radial-gradient(circle at 75% 81%, rgba(119,96,247,.11), transparent 40%),
211
+ linear-gradient(145deg, rgba(51,60,74,.92) 0%, rgba(16,21,29,.97) 48%, rgba(2,4,7,.995) 100%);
212
+ --orb-shell-border: rgba(255,255,255,.24);
213
+ --orb-shadow-rest:
214
+ 0 18px 40px rgba(0,0,0,.48),
215
+ 0 5px 12px rgba(0,0,0,.30),
216
+ 0 0 0 1px rgba(193,213,255,.07),
217
+ inset 0 1px 0 rgba(255,255,255,.22),
218
+ inset 0 -1px 0 rgba(0,0,0,.72);
219
+ --orb-shadow-hover:
220
+ 0 22px 48px rgba(0,0,0,.56),
221
+ 0 8px 16px rgba(0,0,0,.34),
222
+ 0 0 0 1px rgba(193,213,255,.12),
223
+ 0 0 22px rgba(93,151,255,.09),
224
+ inset 0 1px 0 rgba(255,255,255,.26),
225
+ inset 0 -1px 0 rgba(0,0,0,.68);
226
+ --orb-surface-bg:
227
+ radial-gradient(ellipse 46% 31% at 28% 17%, rgba(255,255,255,.42) 0 7%, rgba(255,255,255,.10) 24%, transparent 66%),
228
+ linear-gradient(145deg, rgba(255,255,255,.05), rgba(0,0,0,.48)),
229
+ conic-gradient(from 210deg at 50% 50%,
230
+ rgba(91,224,210,.50) 0deg,
231
+ rgba(82,137,247,.47) 72deg,
232
+ rgba(142,103,238,.44) 142deg,
233
+ rgba(220,106,180,.30) 212deg,
234
+ rgba(151,123,235,.32) 282deg,
235
+ rgba(91,224,210,.50) 360deg);
236
+ --orb-surface-border: rgba(255,255,255,.14);
237
+ --orb-surface-shadow:
238
+ inset 0 1px 0 rgba(255,255,255,.18),
239
+ inset 0 -1px 0 rgba(0,0,0,.62);
240
+ --orb-caustic-bg:
241
+ conic-gradient(from 34deg,
242
+ transparent 0 10%,
243
+ rgba(255,255,255,.38) 14%,
244
+ rgba(100,235,220,.54) 21%,
245
+ transparent 30%,
246
+ rgba(121,145,255,.48) 42%,
247
+ rgba(230,128,205,.42) 53%,
248
+ transparent 63%,
249
+ rgba(255,218,148,.28) 72%,
250
+ rgba(107,231,215,.42) 83%,
251
+ transparent 94%);
252
+ --orb-specular-bg:
253
+ radial-gradient(ellipse 46% 24% at 31% 17%, rgba(255,255,255,.98) 0%, rgba(255,255,255,.43) 32%, transparent 76%),
254
+ linear-gradient(145deg, rgba(255,255,255,.28) 0 7%, rgba(255,255,255,.08) 18%, transparent 38%),
255
+ radial-gradient(ellipse 24% 34% at 86% 83%, rgba(255,255,255,.13), transparent 72%);
256
+ --orb-core-bg:
257
+ radial-gradient(ellipse 60% 40% at 28% 16%, rgba(255,255,255,.24) 0%, rgba(255,255,255,.06) 28%, transparent 64%),
258
+ radial-gradient(circle at 76% 80%, rgba(108,93,224,.12), transparent 46%),
259
+ linear-gradient(145deg, rgba(45,53,66,.98) 0%, rgba(13,17,23,.99) 53%, rgba(2,3,5,1) 100%);
260
+ --orb-core-border: rgba(255,255,255,.13);
261
+ --orb-core-shadow:
262
+ inset 1px 1px 0 rgba(255,255,255,.15),
263
+ inset -6px -8px 15px rgba(0,0,0,.58),
264
+ 0 6px 14px rgba(0,0,0,.40);
265
+ --orb-core-refraction-bg:
266
+ radial-gradient(ellipse 67% 43% at 25% 13%, rgba(255,255,255,.26), transparent 62%),
267
+ linear-gradient(117deg, transparent 27%, rgba(128,219,255,.13) 45%, rgba(190,142,255,.11) 52%, transparent 69%);
268
+ --orb-rim-color: rgba(220,232,255,.25);
269
+ --orb-rim-accent: rgba(107,221,210,.48);
270
+ --orb-rim-secondary: rgba(128,105,229,.40);
271
+ --orb-rim-shadow:
272
+ 0 0 0 1px rgba(150,178,237,.08),
273
+ 0 0 15px rgba(90,141,230,.08);
274
+ --orb-wave-color: rgba(119,174,214,.34);
275
+ --orb-icon-color: rgba(255,255,255,.94);
276
+ --orb-icon-shadow:
277
+ drop-shadow(0 1px 2px rgba(0,0,0,.72))
278
+ drop-shadow(0 0 7px rgba(197,222,255,.12));
279
+ --orb-caustic-opacity: .34;
280
+ --orb-specular-opacity: .50;
281
+ --orb-core-glint-opacity: .43;
282
+ --orb-speaking-a: rgba(79,139,237,.25);
283
+ --orb-speaking-b: rgba(83,202,194,.11);
284
+ --orb-acting-ring: rgba(103,139,235,.12);
285
+ }
286
+
287
+ .orb {
288
+ position: relative;
289
+ display: grid;
290
+ width: 100%;
291
+ height: 100%;
292
+ min-width: 52px;
293
+ min-height: 52px;
294
+ padding: 0;
295
+ place-items: center;
296
+ overflow: visible;
297
+ color: var(--orb-icon-color);
298
+ background: var(--orb-shell-bg);
299
+ border: 1px solid var(--orb-shell-border);
300
+ border-radius: 50%;
301
+ box-shadow: var(--orb-shadow-rest);
302
+ isolation: isolate;
303
+ touch-action: manipulation;
304
+ -webkit-tap-highlight-color: transparent;
305
+ transition:
306
+ transform 180ms cubic-bezier(.2,.8,.2,1),
307
+ box-shadow 260ms cubic-bezier(.2,.8,.2,1),
308
+ filter 220ms ease,
309
+ border-color 220ms ease,
310
+ opacity 180ms ease;
311
+ }
312
+ @media (hover: hover) and (pointer: fine) {
313
+ .orb:not([disabled]):hover {
314
+ transform: translateY(-2px);
315
+ box-shadow: var(--orb-shadow-hover);
316
+ }
317
+ .orb:not([disabled]):hover .orb-surface { filter: brightness(1.035) saturate(1.04); }
318
+ }
319
+ .orb:not([disabled]):active { transform: translateY(0) scale(.965); }
320
+ .orb[disabled] { cursor: default; filter: saturate(.25); opacity: .72; }
321
+
322
+ .orb-surface,
323
+ .orb-rim,
324
+ .orb-core {
325
+ position: absolute;
326
+ inset: 0;
327
+ border-radius: inherit;
328
+ pointer-events: none;
329
+ }
330
+ .orb-surface,
331
+ .orb-core { overflow: hidden; }
332
+
333
+ .orb-surface {
334
+ inset: 2px;
335
+ background: var(--orb-surface-bg);
336
+ border: 1px solid var(--orb-surface-border);
337
+ box-shadow: var(--orb-surface-shadow);
338
+ -webkit-backdrop-filter: blur(12px) saturate(1.24);
339
+ backdrop-filter: blur(12px) saturate(1.24);
340
+ transition: filter 240ms ease;
341
+ z-index: 1;
342
+ }
343
+ .orb-surface::before {
344
+ content: '';
345
+ position: absolute;
346
+ inset: -30%;
347
+ border-radius: 50%;
348
+ background: var(--orb-caustic-bg);
349
+ filter: blur(4.5px) saturate(1.18);
350
+ mix-blend-mode: screen;
351
+ opacity: calc(var(--orb-caustic-opacity) + var(--voice-level) * .10);
352
+ transform: translate3d(-2%, 1%, 0) rotate(-8deg) scale(1.04);
353
+ animation: orb-caustic-drift 11s cubic-bezier(.45,.05,.55,.95) infinite alternate;
354
+ will-change: transform;
355
+ }
356
+ .orb-surface::after {
357
+ content: '';
358
+ position: absolute;
359
+ inset: 1px;
360
+ border-radius: inherit;
361
+ background: var(--orb-specular-bg);
362
+ opacity: var(--orb-specular-opacity);
363
+ mix-blend-mode: screen;
364
+ }
365
+
366
+ .orb-core {
367
+ inset: auto;
368
+ top: 50%;
369
+ left: 50%;
370
+ width: calc(100% - 16px);
371
+ height: calc(100% - 16px);
372
+ background: var(--orb-core-bg);
373
+ border: 1px solid var(--orb-core-border);
374
+ box-shadow: var(--orb-core-shadow);
375
+ -webkit-backdrop-filter: blur(16px) saturate(1.12);
376
+ backdrop-filter: blur(16px) saturate(1.12);
377
+ transform: translate(-50%, -50%);
378
+ transform-origin: center;
379
+ transition: transform 110ms linear, filter 180ms ease;
380
+ z-index: 2;
381
+ }
382
+ .orb-core::before {
383
+ content: '';
384
+ position: absolute;
385
+ inset: 1px;
386
+ border-radius: inherit;
387
+ background: var(--orb-core-refraction-bg);
388
+ mix-blend-mode: screen;
389
+ opacity: var(--orb-core-glint-opacity);
390
+ }
391
+ .orb-core::after {
392
+ content: '';
393
+ position: absolute;
394
+ left: 19%;
395
+ top: 14%;
396
+ width: 40%;
397
+ height: 17%;
398
+ border-radius: 50%;
399
+ background: linear-gradient(90deg, rgba(255,255,255,.68), rgba(255,255,255,.06));
400
+ filter: blur(.45px);
401
+ transform: rotate(-24deg);
402
+ opacity: var(--orb-core-glint-opacity);
403
+ }
404
+
405
+ .orb-rim {
406
+ inset: -3px;
407
+ border: 1px solid var(--orb-rim-color);
408
+ border-top-color: var(--orb-rim-accent);
409
+ border-right-color: var(--orb-rim-secondary);
410
+ box-shadow: var(--orb-rim-shadow);
411
+ opacity: calc(.48 + var(--voice-level) * .40);
412
+ transform: scale(calc(1 + var(--voice-level) * .10));
413
+ transition: opacity 90ms linear, transform 90ms linear;
414
+ z-index: 0;
415
+ }
416
+ .orb-rim::before,
417
+ .orb-rim::after {
418
+ content: '';
419
+ position: absolute;
420
+ inset: -1px;
421
+ border: 1px solid var(--orb-wave-color);
422
+ border-radius: 50%;
423
+ opacity: 0;
424
+ pointer-events: none;
425
+ }
426
+ .orb-icon {
427
+ position: relative;
428
+ z-index: 4;
429
+ display: grid;
430
+ place-items: center;
431
+ color: var(--orb-icon-color);
432
+ filter: var(--orb-icon-shadow);
433
+ }
434
+ .orb-icon svg {
435
+ width: 22px;
436
+ height: 22px;
437
+ stroke: currentColor;
438
+ stroke-width: 1.9;
439
+ }
440
+ .orb[data-active="true"] .start-icon { display: none; }
441
+ .orb:not([data-active="true"]) .end-icon { display: none; }
442
+ .orb[data-state="active.ready"] .orb-rim { animation: breathe 3.4s ease-in-out infinite; }
443
+ .orb[data-state="active.listening"] .orb-rim { animation: listen 1.3s ease-in-out infinite; }
444
+ .orb[data-state="active.thinking"] .orb-surface::before { animation: orb-caustic-orbit 2.4s linear infinite; }
445
+ .orb[data-state="active.listening"] .orb-rim::before,
446
+ .orb[data-state="active.speaking"] .orb-rim::before,
447
+ .orb[data-state="active.speaking"] .orb-rim::after {
448
+ animation: radar-wave 2.35s cubic-bezier(.18, .62, .32, 1) infinite;
449
+ }
450
+ .orb[data-state="active.speaking"] .orb-rim::after { animation-delay: 1.12s; }
451
+ .orb[data-state="active.speaking"] .orb-rim {
452
+ box-shadow:
453
+ var(--orb-rim-shadow),
454
+ 0 0 calc(12px + var(--voice-level) * 17px) var(--orb-speaking-a),
455
+ 0 0 calc(25px + var(--voice-level) * 22px) var(--orb-speaking-b);
456
+ }
457
+ .orb[data-state="active.speaking"] .orb-core {
458
+ transform: translate(-50%, -50%) scale(calc(1 + var(--voice-level) * .04));
459
+ filter: brightness(calc(1 + var(--voice-level) * .11));
460
+ }
461
+ .orb[data-state="active.acting"] {
462
+ box-shadow:
463
+ var(--orb-shadow-hover),
464
+ 0 0 0 5px var(--orb-acting-ring);
465
+ }
466
+ .orb[data-state="active.awaiting_confirmation"] .orb-rim {
467
+ animation: none;
468
+ transform: scale(1.07);
469
+ }
470
+ .orb[data-state="active.reconnecting"] { filter: saturate(.28) brightness(.94); }
471
+ .orb[data-state="active.reconnecting"] .orb-rim { animation: orb-rim-orbit 2.8s linear infinite; }
472
+ .orb[data-state="ending"] { transform: scale(.84); opacity: .72; pointer-events: none; }
473
+ .orb.wake::after {
474
+ content: '';
475
+ position: fixed;
476
+ right: -2px;
477
+ bottom: -2px;
478
+ width: min(62vw, 760px);
479
+ height: min(64vh, 680px);
480
+ pointer-events: none;
481
+ background:
482
+ radial-gradient(ellipse 78% 66% at 100% 100%, color-mix(in srgb, var(--voice-accent) 28%, rgba(99,229,211,.26)), rgba(126,116,247,.15) 36%, rgba(244,139,207,.08) 55%, transparent 76%),
483
+ radial-gradient(ellipse 110% 18% at 62% 105%, rgba(101,222,214,.16), transparent 76%),
484
+ radial-gradient(ellipse 18% 110% at 105% 62%, rgba(208,137,239,.13), transparent 76%);
485
+ filter: blur(18px) saturate(1.2);
486
+ opacity: 0;
487
+ animation: wake ${INTRO_WAKE_MS}ms ease-out both;
488
+ z-index: -1;
489
+ }
490
+ :host([data-position="bottom_left"]) .orb.wake::after { right: auto; left: -2px; transform: scaleX(-1); }
491
+ .first-label {
492
+ position: absolute;
493
+ inset-inline-end: calc(100% + 9px);
494
+ top: 50%;
495
+ width: max-content;
496
+ max-width: min(180px, calc(100vw - 94px));
497
+ padding: 7px 10px;
498
+ color: var(--voice-text);
499
+ background: var(--voice-surface);
500
+ border: 1px solid var(--voice-border);
501
+ border-radius: 8px;
502
+ box-shadow: 0 7px 20px rgba(15,23,42,.14);
503
+ font-size: 13px;
504
+ line-height: 18px;
505
+ transform: translateY(-50%);
506
+ white-space: nowrap;
507
+ }
508
+ :host([data-position="bottom_left"]) .first-label { inset-inline-start: calc(100% + 9px); inset-inline-end: auto; }
509
+ .caption-stage {
510
+ position: absolute;
511
+ inset-inline-end: 0;
512
+ bottom: calc(100% + 10px);
513
+ width: min(440px, calc(100vw - 32px));
514
+ height: min(680px, max(180px, calc(100dvh - var(--voice-block-offset) - var(--voice-orb-size) - 34px)));
515
+ pointer-events: none;
516
+ }
517
+ :host([data-position="bottom_left"]) .caption-stage { inset-inline-start: 0; inset-inline-end: auto; }
518
+ .subtitle-track {
519
+ position: absolute;
520
+ inset-inline-end: 0;
521
+ bottom: 0;
522
+ display: grid;
523
+ width: 100%;
524
+ max-height: 100%;
525
+ gap: 6px;
526
+ padding: 9px 11px;
527
+ overflow: hidden;
528
+ color: var(--voice-text);
529
+ background:
530
+ linear-gradient(145deg, rgba(255, 255, 255, .98), rgba(248, 250, 252, .92));
531
+ border: 1px solid rgba(255, 255, 255, .88);
532
+ border-radius: 8px;
533
+ box-shadow:
534
+ 0 14px 38px rgba(15, 23, 42, .16),
535
+ 0 0 0 1px rgba(148, 163, 184, .20),
536
+ inset 0 1px 0 rgba(255, 255, 255, .92);
537
+ -webkit-backdrop-filter: blur(18px) saturate(1.08);
538
+ backdrop-filter: blur(18px) saturate(1.08);
539
+ transform-origin: bottom right;
540
+ }
541
+ .subtitle-track:empty { display: none; }
542
+ :host([data-position="bottom_left"]) .subtitle-track {
543
+ inset-inline-start: 0;
544
+ inset-inline-end: auto;
545
+ transform-origin: bottom left;
546
+ }
547
+ .caption {
548
+ --caption-enter-delay: 0ms;
549
+ display: grid;
550
+ grid-template-columns: 24px minmax(0, 1fr);
551
+ width: 100%;
552
+ min-width: 0;
553
+ gap: 8px;
554
+ align-items: start;
555
+ overflow: hidden;
556
+ font-size: 13px;
557
+ line-height: 20px;
558
+ pointer-events: none;
559
+ opacity: 1;
560
+ transform: translateY(0);
561
+ transition: opacity 180ms ease, transform 180ms ease;
562
+ }
563
+ .caption + .caption {
564
+ padding-top: 6px;
565
+ border-top: 1px solid rgba(148, 163, 184, .22);
566
+ }
567
+ .caption.previous { opacity: .58; }
568
+ .caption-speaker {
569
+ display: grid;
570
+ width: 24px;
571
+ height: 24px;
572
+ place-items: center;
573
+ color: #354150;
574
+ background: rgba(248, 250, 252, .96);
575
+ border: 1px solid rgba(148, 163, 184, .28);
576
+ border-radius: 50%;
577
+ box-shadow: 0 2px 7px rgba(15, 23, 42, .08);
578
+ }
579
+ .caption.assistant .caption-speaker {
580
+ color: color-mix(in srgb, var(--voice-accent) 78%, #17212b);
581
+ background: color-mix(in srgb, var(--voice-accent) 9%, rgba(255, 255, 255, .98));
582
+ border-color: color-mix(in srgb, var(--voice-accent) 20%, rgba(148, 163, 184, .24));
583
+ }
584
+ .caption.user .caption-speaker {
585
+ color: #4b5968;
586
+ background: rgba(241, 245, 249, .98);
587
+ }
588
+ .caption-speaker svg { width: 13px; height: 13px; stroke-width: 2; }
589
+ .caption-copy {
590
+ display: -webkit-box;
591
+ min-width: 0;
592
+ max-height: calc(var(--caption-lines, 10) * 20px);
593
+ overflow: hidden;
594
+ overflow-wrap: anywhere;
595
+ -webkit-box-orient: vertical;
596
+ -webkit-line-clamp: 10;
597
+ }
598
+ .caption.entering {
599
+ animation: caption-in 180ms ease-out var(--caption-enter-delay) both;
600
+ }
601
+ .caption.leaving { opacity: 0; transform: translateY(5px); }
602
+ .status-bubble {
603
+ position: absolute;
604
+ inset-inline-end: 0;
605
+ bottom: calc(100% + 18px + var(--voice-subtitle-height, 0px));
606
+ max-width: min(280px, calc(100vw - 88px));
607
+ padding: 7px 10px;
608
+ color: var(--voice-text);
609
+ background: var(--voice-surface);
610
+ border: 1px solid var(--voice-border);
611
+ border-radius: 9px;
612
+ box-shadow: 0 7px 20px rgba(15,23,42,.14);
613
+ font-size: 12px;
614
+ line-height: 18px;
615
+ pointer-events: none;
616
+ }
617
+ :host([data-position="bottom_left"]) .status-bubble { inset-inline-start: 0; inset-inline-end: auto; }
618
+ .attribution-bubble {
619
+ position: absolute;
620
+ inset-inline-end: 0;
621
+ bottom: calc(100% + 18px);
622
+ width: max-content;
623
+ max-width: min(240px, calc(100vw - 88px));
624
+ padding: 8px 11px;
625
+ color: var(--voice-muted);
626
+ background: var(--voice-surface);
627
+ border: 1px solid var(--voice-border);
628
+ border-radius: 9px;
629
+ box-shadow: 0 7px 20px rgba(15,23,42,.14);
630
+ font-size: 12px;
631
+ line-height: 18px;
632
+ pointer-events: auto;
633
+ animation: attribution-in 180ms ease-out both;
634
+ }
635
+ .attribution-bubble a {
636
+ color: var(--voice-text);
637
+ font-weight: 650;
638
+ text-decoration: underline;
639
+ text-underline-offset: 2px;
640
+ }
641
+ :host([data-position="bottom_left"]) .attribution-bubble { inset-inline-start: 0; inset-inline-end: auto; }
642
+ .dialog-layer {
643
+ position: fixed;
644
+ inset: auto;
645
+ left: var(--voice-viewport-left, 0px);
646
+ top: var(--voice-viewport-top, 0px);
647
+ width: var(--voice-viewport-width, 100vw);
648
+ height: var(--voice-viewport-height, 100dvh);
649
+ display: grid;
650
+ padding: 16px;
651
+ place-items: center;
652
+ background: rgba(15, 23, 42, .36);
653
+ pointer-events: auto;
654
+ z-index: 10;
655
+ }
656
+ .dialog {
657
+ width: min(420px, calc(var(--voice-viewport-width, 100vw) - 32px));
658
+ max-height: min(620px, calc(var(--voice-viewport-height, 100dvh) - 32px));
659
+ padding: 20px;
660
+ overflow: auto;
661
+ color: var(--voice-text);
662
+ background: #fff;
663
+ border: 1px solid var(--voice-border);
664
+ border-radius: 12px;
665
+ box-shadow: 0 24px 64px rgba(15,23,42,.28);
666
+ pointer-events: auto;
667
+ }
668
+ .dialog-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: start; }
669
+ .dialog h2 { margin: 0; font-size: 18px; line-height: 25px; font-weight: 680; }
670
+ .dialog p { margin: 7px 0 0; color: var(--voice-muted); font-size: 14px; line-height: 21px; }
671
+ .icon-button {
672
+ display: grid;
673
+ width: 36px;
674
+ height: 36px;
675
+ min-width: 36px;
676
+ padding: 0;
677
+ place-items: center;
678
+ color: #4b5563;
679
+ background: transparent;
680
+ border: 1px solid transparent;
681
+ border-radius: 8px;
682
+ }
683
+ .icon-button:hover { background: #f3f4f6; }
684
+ .icon-button svg { width: 18px; height: 18px; }
685
+ .agreement {
686
+ display: grid;
687
+ grid-template-columns: 18px minmax(0, 1fr);
688
+ gap: 10px;
689
+ align-items: start;
690
+ margin-top: 17px;
691
+ padding: 12px;
692
+ color: #26313c;
693
+ background: #f7f9fa;
694
+ border: 1px solid #e4e8ec;
695
+ border-radius: 9px;
696
+ font-size: 13px;
697
+ line-height: 19px;
698
+ cursor: pointer;
699
+ }
700
+ .agreement input { width: 17px; height: 17px; margin: 1px 0 0; accent-color: var(--voice-accent); }
701
+ details { margin-top: 12px; color: var(--voice-muted); font-size: 12px; line-height: 18px; }
702
+ summary { width: fit-content; color: color-mix(in srgb, var(--voice-accent) 78%, #18212b); cursor: pointer; }
703
+ .legal-copy { margin-top: 8px; padding: 10px 11px; background: #fafbfc; border: 1px solid #e6e9ed; border-radius: 8px; }
704
+ .legal-copy p { margin: 0; font-size: 12px; line-height: 18px; }
705
+ .legal-links { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px; }
706
+ .legal-links a { color: color-mix(in srgb, var(--voice-accent) 82%, #18212b); }
707
+ .dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
708
+ .button {
709
+ min-height: 40px;
710
+ padding: 0 14px;
711
+ border: 1px solid #d7dce1;
712
+ border-radius: 8px;
713
+ background: #fff;
714
+ color: #27313b;
715
+ font-size: 13px;
716
+ font-weight: 620;
717
+ }
718
+ .button:hover { background: #f7f8f9; }
719
+ .button.primary { color: #fff; background: var(--voice-accent); border-color: color-mix(in srgb, var(--voice-accent) 78%, #111827); }
720
+ .button.primary:hover { background: color-mix(in srgb, var(--voice-accent) 88%, #111827); }
721
+ .button:disabled { cursor: not-allowed; opacity: .55; }
722
+ .safety-card,
723
+ .recovery-card {
724
+ position: absolute;
725
+ inset-inline-end: 0;
726
+ bottom: calc(100% + 11px);
727
+ width: min(340px, calc(100vw - 32px));
728
+ padding: 14px;
729
+ color: var(--voice-text);
730
+ background: #fff;
731
+ border: 1px solid var(--voice-border);
732
+ border-radius: 8px;
733
+ box-shadow: 0 12px 32px rgba(15,23,42,.2);
734
+ pointer-events: auto;
735
+ }
736
+ :host([data-position="bottom_left"]) .safety-card,
737
+ :host([data-position="bottom_left"]) .recovery-card { inset-inline-start: 0; inset-inline-end: auto; }
738
+ .card-title { margin: 0; font-size: 14px; line-height: 20px; font-weight: 680; }
739
+ .card-copy { margin: 5px 0 0; color: var(--voice-muted); font-size: 12px; line-height: 18px; }
740
+ .card-actions { display: flex; justify-content: flex-end; gap: 7px; margin-top: 12px; }
741
+ .card-actions .button { min-height: 36px; }
742
+ .recovery-head {
743
+ display: grid;
744
+ grid-template-columns: 28px minmax(0, 1fr) 32px;
745
+ gap: 9px;
746
+ align-items: center;
747
+ }
748
+ .recovery-badge {
749
+ display: grid;
750
+ width: 28px;
751
+ height: 28px;
752
+ place-items: center;
753
+ color: color-mix(in srgb, var(--voice-accent) 72%, #26313c);
754
+ background: color-mix(in srgb, var(--voice-accent) 7%, #fff);
755
+ border: 1px solid color-mix(in srgb, var(--voice-accent) 16%, #e2e7eb);
756
+ border-radius: 7px;
757
+ }
758
+ .recovery-badge svg { width: 16px; height: 16px; }
759
+ .recovery-close {
760
+ width: 32px;
761
+ height: 32px;
762
+ min-width: 32px;
763
+ justify-self: end;
764
+ }
765
+ .recovery-card .card-copy { margin-top: 9px; font-size: 13px; line-height: 19px; }
766
+ .recovery-card .card-actions { flex-wrap: wrap; align-items: stretch; margin-top: 13px; }
767
+ .recovery-card .card-actions .button {
768
+ display: inline-flex;
769
+ min-width: 0;
770
+ padding-inline: 11px;
771
+ align-items: center;
772
+ justify-content: center;
773
+ gap: 7px;
774
+ line-height: 18px;
775
+ white-space: normal;
776
+ }
777
+ .recovery-card .card-actions .button svg { width: 15px; height: 15px; flex: 0 0 auto; }
778
+ @media (max-width: 380px) {
779
+ .recovery-card .card-actions { display: grid; grid-template-columns: minmax(0, 1fr); }
780
+ .recovery-card .card-actions .button { width: 100%; }
781
+ }
782
+ .hidden-live {
783
+ position: fixed;
784
+ width: 1px;
785
+ height: 1px;
786
+ padding: 0;
787
+ margin: -1px;
788
+ overflow: hidden;
789
+ clip: rect(0,0,0,0);
790
+ white-space: nowrap;
791
+ border: 0;
792
+ }
793
+ audio { display: none; }
794
+ .action-highlight {
795
+ position: fixed;
796
+ display: none;
797
+ border: 2px solid var(--voice-accent);
798
+ border-radius: 8px;
799
+ box-shadow: 0 0 0 5px color-mix(in srgb, var(--voice-accent) 12%, transparent);
800
+ pointer-events: none;
801
+ z-index: 2;
802
+ }
803
+ .action-highlight.visible { display: block; }
804
+ .action-highlight.success { border-color: #138a63; }
805
+ .action-highlight.failure { border-color: #9f3545; }
806
+ @keyframes breathe { 0%,100% { opacity: .48; transform: scale(1); } 50% { opacity: .78; transform: scale(1.055); } }
807
+ @keyframes listen { 0%,100% { transform: scale(1.025); } 50% { transform: scale(.965); } }
808
+ @keyframes orb-caustic-drift {
809
+ from { transform: translate3d(-2%, 1%, 0) rotate(-8deg) scale(1.04); }
810
+ to { transform: translate3d(2%, -1%, 0) rotate(10deg) scale(1.08); }
811
+ }
812
+ @keyframes orb-caustic-orbit {
813
+ from { transform: rotate(0deg) scale(1.08); }
814
+ to { transform: rotate(360deg) scale(1.08); }
815
+ }
816
+ @keyframes orb-rim-orbit {
817
+ from { transform: rotate(0deg) scale(1.02); }
818
+ to { transform: rotate(360deg) scale(1.02); }
819
+ }
820
+ @keyframes radar-wave {
821
+ 0% { opacity: .38; transform: scale(.9); }
822
+ 68%, 100% { opacity: 0; transform: scale(1.62); }
823
+ }
824
+ @keyframes wake { 0% { opacity: 0; transform: scale(.76); } 35% { opacity: .9; } 100% { opacity: 0; transform: scale(1.16); } }
825
+ @keyframes caption-in { from { opacity: 0; transform: translateY(5px); } }
826
+ @keyframes attribution-in { from { opacity: 0; transform: translateY(4px); } }
827
+ @media (prefers-reduced-motion: reduce) {
828
+ .orb, .orb-rim, .orb-rim::before, .orb-rim::after,
829
+ .orb-surface, .orb-surface::before, .orb-surface::after,
830
+ .orb-core, .orb-core::before, .orb-core::after, .caption {
831
+ animation: none !important;
832
+ transition: none !important;
833
+ }
834
+ .orb.wake::after { animation-duration: 120ms; }
835
+ .presence-field { transition-duration: 120ms; }
836
+ }
837
+ :host([data-visual-test="true"]) *,
838
+ :host([data-visual-test="true"]) *::before,
839
+ :host([data-visual-test="true"]) *::after {
840
+ animation-play-state: paused !important;
841
+ transition: none !important;
842
+ }
843
+ :host([data-visual-test="true"]) .caption.entering { animation: none !important; }
844
+ @media (forced-colors: active) {
845
+ .orb { color: ButtonText; background: ButtonFace; border: 2px solid ButtonText; forced-color-adjust: auto; }
846
+ .orb-surface, .orb-core, .orb-rim { display: none; }
847
+ .subtitle-track, .dialog, .safety-card, .recovery-card { color: CanvasText; background: Canvas; border: 1px solid CanvasText; }
848
+ .caption-speaker { color: CanvasText; background: Canvas; border-color: CanvasText; }
849
+ }
850
+ @media (max-width: 520px) {
851
+ .dialog-layer { align-items: end; padding: 10px; }
852
+ .dialog {
853
+ width: 100%;
854
+ max-height: calc(var(--voice-viewport-height, 100dvh) - 20px);
855
+ }
856
+ .dialog-actions { flex-direction: column-reverse; }
857
+ .dialog-actions .button { width: 100%; }
858
+ }
859
+ `;
860
+ const icon = (node, className) => {
861
+ const element = (0, lucide_1.createElement)(node);
862
+ element.classList.add(className);
863
+ element.setAttribute("aria-hidden", "true");
864
+ return element;
865
+ };
866
+ const isFocusTarget = (value) => value instanceof HTMLElement &&
867
+ !value.hasAttribute("disabled") &&
868
+ value.getAttribute("aria-hidden") !== "true";
869
+ class VoicePresenceView {
870
+ constructor(config, overrides, callbacks, tabId, onFallback = () => undefined, visualTestMode = false) {
871
+ this.callbacks = callbacks;
872
+ this.onFallback = onFallback;
873
+ this.captionTimers = new Map();
874
+ this.captionResizeObserver = null;
875
+ this.captionLayoutFrame = null;
876
+ this.highlightTarget = null;
877
+ this.highlightResizeObserver = null;
878
+ this.highlightFrame = null;
879
+ this.highlightScrollTargets = [];
880
+ this.currentConfirmation = null;
881
+ this.currentVisualRequest = null;
882
+ this.restoreFocus = null;
883
+ this.active = false;
884
+ this.activeElsewhere = false;
885
+ this.destroyed = false;
886
+ this.introPlayed = false;
887
+ this.wakeTimer = null;
888
+ this.statusTimer = null;
889
+ this.attributionTimer = null;
890
+ this.audioContext = null;
891
+ this.playbackResumePending = false;
892
+ this.outputSource = null;
893
+ this.outputAnalyser = null;
894
+ this.inputSource = null;
895
+ this.inputAnalyser = null;
896
+ this.levelFrame = null;
897
+ this.levelBuffer = null;
898
+ this.lastLevelAt = 0;
899
+ this.serverPresenceState = "ready";
900
+ this.playbackState = "idle";
901
+ this.capabilityTier = "standard";
902
+ this.onModalKeyDown = (event) => {
903
+ if (event.key === "Escape") {
904
+ event.preventDefault();
905
+ this.closeActivation();
906
+ return;
907
+ }
908
+ if (event.key !== "Tab")
909
+ return;
910
+ const focusable = Array.from(this.modalLayer.querySelectorAll("button,input,summary,a[href]")).filter(isFocusTarget);
911
+ if (focusable.length === 0)
912
+ return;
913
+ const first = focusable[0];
914
+ const last = focusable.at(-1);
915
+ if (!first || !last)
916
+ return;
917
+ const activeElement = this.shadow.activeElement;
918
+ if (event.shiftKey && activeElement === first) {
919
+ event.preventDefault();
920
+ last.focus();
921
+ }
922
+ else if (!event.shiftKey && activeElement === last) {
923
+ event.preventDefault();
924
+ first.focus();
925
+ }
926
+ };
927
+ this.scheduleCaptionLayout = () => {
928
+ if (this.destroyed || this.captionLayoutFrame !== null)
929
+ return;
930
+ this.captionLayoutFrame = requestAnimationFrame(() => {
931
+ this.captionLayoutFrame = null;
932
+ this.layoutCaptions();
933
+ });
934
+ };
935
+ this.scheduleHighlightLayout = () => {
936
+ if (this.destroyed || !this.highlightTarget || this.highlightFrame !== null)
937
+ return;
938
+ this.highlightFrame = requestAnimationFrame(() => {
939
+ this.highlightFrame = null;
940
+ this.renderHighlight();
941
+ });
942
+ };
943
+ this.onVisibilityChange = () => {
944
+ if (document.hidden)
945
+ this.stopLevelLoop();
946
+ else if (this.activeAnalyser())
947
+ this.startLevelLoop();
948
+ };
949
+ const voiceConfig = config.appearance?.voicePresence;
950
+ if (!voiceConfig?.enabled) {
951
+ throw new Error("Voice Presence appearance is unavailable.");
952
+ }
953
+ this.config = config;
954
+ this.voiceConfig = voiceConfig;
955
+ this.messages = { ...messages_js_1.defaultMessages, ...overrides };
956
+ this.host = document.createElement("div");
957
+ this.host.dataset.oxvoAiLiveAssistRoot = "true";
958
+ this.host.dataset.position = voiceConfig.launcher.position;
959
+ this.host.dataset.intensity = voiceConfig.visual.intensity;
960
+ this.host.dataset.orbTheme = "light";
961
+ this.setOrbTheme(voiceConfig.launcher.theme ?? "light");
962
+ if (visualTestMode)
963
+ this.host.dataset.visualTest = "true";
964
+ this.host.setAttribute("dir", (0, placement_js_1.getDocumentDirection)());
965
+ this.shadow = this.host.attachShadow({ mode: "closed" });
966
+ const sheet = document.createElement("style");
967
+ sheet.textContent = styles;
968
+ this.shadow.append(sheet, this.build());
969
+ document.documentElement.append(this.host);
970
+ this.orb = this.require('[data-role="orb"]');
971
+ this.modalLayer = this.require('[data-role="activation-layer"]');
972
+ this.agreement = this.require('[data-role="agreement"]');
973
+ this.captionStage = this.require(".caption-stage");
974
+ this.subtitleTrack = this.require('[data-role="subtitle-track"]');
975
+ this.statusBubble = this.require('[data-role="status-bubble"]');
976
+ this.attributionBubble = this.require('[data-role="attribution"]');
977
+ this.safetyCard = this.require('[data-role="safety-card"]');
978
+ this.recoveryCard = this.require('[data-role="recovery-card"]');
979
+ this.liveRegion = this.require('[data-role="live-region"]');
980
+ this.audio = this.require("audio");
981
+ this.highlight = this.require(".action-highlight");
982
+ if (typeof ResizeObserver !== "undefined") {
983
+ this.captionResizeObserver = new ResizeObserver(this.scheduleCaptionLayout);
984
+ this.highlightResizeObserver = new ResizeObserver(this.scheduleHighlightLayout);
985
+ }
986
+ this.stateMachine = new experienceState_js_1.VoicePresenceStateMachine(tabId, (state) => {
987
+ this.orb.dataset.state = state;
988
+ this.host.dataset.state = state;
989
+ });
990
+ this.applyConfig();
991
+ this.placement = new placement_js_1.LauncherPlacementController({
992
+ host: this.host,
993
+ preferredSide: this.voiceConfig.launcher.position,
994
+ inlineOffset: this.voiceConfig.launcher.offsetInlinePx,
995
+ blockOffset: this.voiceConfig.launcher.offsetBlockPx,
996
+ size: this.voiceConfig.launcher.size === "compact" ? 52 : 58,
997
+ isActive: () => this.active,
998
+ });
999
+ this.bind();
1000
+ this.bindPlaybackState();
1001
+ this.stateMachine.transition("launcher");
1002
+ }
1003
+ setRemoteStream(stream) {
1004
+ const previous = this.audio.srcObject;
1005
+ if (previous &&
1006
+ typeof MediaStream !== "undefined" &&
1007
+ previous instanceof MediaStream &&
1008
+ previous !== stream) {
1009
+ for (const track of previous.getTracks())
1010
+ track.stop();
1011
+ }
1012
+ this.audio.srcObject = stream;
1013
+ this.connectOutputAnalyser(stream);
1014
+ void this.audio.play().catch(() => {
1015
+ this.emitPlaybackState("blocked");
1016
+ this.showTransientStatus("Tap the voice button to resume audio.", 6000);
1017
+ });
1018
+ }
1019
+ setCapabilityTier(tier) {
1020
+ this.capabilityTier = tier;
1021
+ }
1022
+ setOrbTheme(theme) {
1023
+ this.host.dataset.orbTheme = theme;
1024
+ }
1025
+ launcherRect() {
1026
+ if (!this.orb.isConnected || this.orb.hidden)
1027
+ return null;
1028
+ const rect = this.orb.getBoundingClientRect();
1029
+ return rect.width > 0 && rect.height > 0 ? rect : null;
1030
+ }
1031
+ presentationObstacles() {
1032
+ const candidates = [
1033
+ this.subtitleTrack,
1034
+ this.statusBubble,
1035
+ this.safetyCard,
1036
+ this.recoveryCard,
1037
+ this.orb,
1038
+ ];
1039
+ return candidates
1040
+ .filter((element) => element.isConnected && !element.hidden)
1041
+ .map((element) => element.getBoundingClientRect())
1042
+ .filter((rect) => rect.width > 0 && rect.height > 0);
1043
+ }
1044
+ setLocalStream(stream) {
1045
+ this.ensureAudioContext();
1046
+ this.inputSource?.disconnect();
1047
+ this.inputAnalyser?.disconnect();
1048
+ this.inputSource = null;
1049
+ this.inputAnalyser = null;
1050
+ if (!stream || !this.audioContext)
1051
+ return;
1052
+ try {
1053
+ this.inputAnalyser = this.audioContext.createAnalyser();
1054
+ this.inputAnalyser.fftSize = 256;
1055
+ this.inputAnalyser.smoothingTimeConstant = 0.82;
1056
+ this.inputSource = this.audioContext.createMediaStreamSource(stream);
1057
+ this.inputSource.connect(this.inputAnalyser);
1058
+ this.levelBuffer = new Uint8Array(this.inputAnalyser.fftSize);
1059
+ if (this.stateMachine.state === "active.listening")
1060
+ this.startLevelLoop();
1061
+ }
1062
+ catch {
1063
+ this.inputSource = null;
1064
+ this.inputAnalyser = null;
1065
+ }
1066
+ }
1067
+ setStatus(value) {
1068
+ if (!value || value === this.messages.ready) {
1069
+ this.hideStatus();
1070
+ return;
1071
+ }
1072
+ this.showTransientStatus(value, 4000);
1073
+ }
1074
+ message(key) {
1075
+ return this.messages[key];
1076
+ }
1077
+ setAssistantState(state) {
1078
+ if (!this.active)
1079
+ return;
1080
+ if (state === "thinking" || state === "speaking") {
1081
+ this.resumeRemotePlayback();
1082
+ }
1083
+ this.stateMachine.transition(`active.${state}`);
1084
+ if (state === "listening" || state === "speaking")
1085
+ this.startLevelLoop();
1086
+ else
1087
+ this.stopLevelLoop();
1088
+ }
1089
+ applyPresenceState(state, sequence, sessionId, pageId, fencingToken) {
1090
+ this.serverPresenceState = state;
1091
+ if (state === "thinking" || state === "speaking") {
1092
+ this.resumeRemotePlayback();
1093
+ }
1094
+ const next = this.effectivePresenceState();
1095
+ this.stateMachine.transition(next, {
1096
+ sessionId,
1097
+ serverSequence: sequence,
1098
+ ...(pageId ? { pageId } : {}),
1099
+ fencingToken,
1100
+ });
1101
+ if (state === "reconnecting")
1102
+ this.showTransientStatus(this.messages.reconnecting, 5000);
1103
+ else if (state !== "awaiting_confirmation")
1104
+ this.hideStatus();
1105
+ this.syncLevelLoop();
1106
+ }
1107
+ showPreflight() {
1108
+ this.stateMachine.transition("preflight");
1109
+ this.closeActivation(false);
1110
+ }
1111
+ showRequestingMicrophone() {
1112
+ this.recoveryCard.hidden = true;
1113
+ this.orb.hidden = false;
1114
+ this.stateMachine.transition("requesting_microphone");
1115
+ this.showTransientStatus("Requesting microphone access", 0);
1116
+ }
1117
+ showConnecting(resume = false, _mode = "voice") {
1118
+ void _mode;
1119
+ this.active = true;
1120
+ this.hideAttribution();
1121
+ this.orb.dataset.active = "true";
1122
+ this.orb.setAttribute("aria-label", "End voice assist");
1123
+ if (resume) {
1124
+ this.introPlayed = true;
1125
+ this.stateMachine.transition("active.reconnecting");
1126
+ }
1127
+ else {
1128
+ this.stateMachine.transition("connecting");
1129
+ }
1130
+ this.showTransientStatus(resume ? this.messages.reconnecting : this.messages.connecting, 0);
1131
+ }
1132
+ showActive(welcomeMessage) {
1133
+ this.active = true;
1134
+ this.placement.refresh(true);
1135
+ this.hideStatus();
1136
+ this.orb.dataset.active = "true";
1137
+ this.orb.disabled = false;
1138
+ this.orb.setAttribute("aria-label", "End voice assist");
1139
+ this.stateMachine.transition("active.ready");
1140
+ if (!this.introPlayed &&
1141
+ this.voiceConfig.visual.introWake &&
1142
+ this.capabilityTier === "standard") {
1143
+ this.introPlayed = true;
1144
+ this.orb.classList.add("wake");
1145
+ this.wakeTimer = setTimeout(() => {
1146
+ this.wakeTimer = null;
1147
+ this.orb.classList.remove("wake");
1148
+ }, INTRO_WAKE_MS + 40);
1149
+ }
1150
+ if (welcomeMessage && this.voiceConfig.captions.showAssistantFinal) {
1151
+ this.addCaption("assistant", welcomeMessage, true);
1152
+ }
1153
+ }
1154
+ addCaption(role, text, final) {
1155
+ if (!this.voiceConfig.captions.enabled)
1156
+ return;
1157
+ if (!final) {
1158
+ if (role === "user" && this.voiceConfig.captions.showInterimVisitor) {
1159
+ this.showTransientStatus(this.messages.listening, 1200);
1160
+ }
1161
+ return;
1162
+ }
1163
+ if ((role === "assistant" && !this.voiceConfig.captions.showAssistantFinal) ||
1164
+ (role === "user" && !this.voiceConfig.captions.showVisitorFinal)) {
1165
+ return;
1166
+ }
1167
+ const normalized = text.replace(/\s+/g, " ").trim();
1168
+ if (!normalized)
1169
+ return;
1170
+ const maxCharacters = this.voiceConfig.captions.maxCharacters;
1171
+ const maxLines = Math.min(10, Math.max(1, this.voiceConfig.captions.maxLines));
1172
+ const rendered = normalized.length > maxCharacters
1173
+ ? `${normalized.slice(0, Math.max(1, maxCharacters - 3)).trimEnd()}...`
1174
+ : normalized;
1175
+ const hadCaptions = this.subtitleTrack.childElementCount > 0;
1176
+ while (this.subtitleTrack.childElementCount >= MAX_VISIBLE_SUBTITLES) {
1177
+ const oldest = this.subtitleTrack.firstElementChild;
1178
+ if (!oldest)
1179
+ break;
1180
+ this.discardCaption(oldest);
1181
+ }
1182
+ const line = document.createElement("div");
1183
+ line.className = `caption ${role} entering`;
1184
+ line.dataset.captionId = `${role}-${crypto.randomUUID()}`;
1185
+ line.dataset.speaker = role;
1186
+ line.style.setProperty("--caption-lines", String(maxLines));
1187
+ const speaker = document.createElement("span");
1188
+ speaker.className = "caption-speaker";
1189
+ speaker.setAttribute("aria-hidden", "true");
1190
+ speaker.append(icon(role === "assistant" ? lucide_1.AudioWaveform : lucide_1.UserRound, "caption-symbol"));
1191
+ const copy = document.createElement("span");
1192
+ copy.className = "caption-copy";
1193
+ copy.textContent = rendered;
1194
+ copy.style.webkitLineClamp = String(maxLines);
1195
+ copy.style.maxHeight = `${maxLines * 20}px`;
1196
+ line.append(speaker, copy);
1197
+ line.style.setProperty("--caption-enter-delay", hadCaptions ? "40ms" : "0ms");
1198
+ this.subtitleTrack.append(line);
1199
+ this.captionResizeObserver?.observe(line);
1200
+ line.addEventListener("animationend", () => line.classList.remove("entering"), { once: true });
1201
+ this.layoutCaptions();
1202
+ const timer = setTimeout(() => this.removeCaption(line), this.voiceConfig.captions.visibleMs);
1203
+ this.captionTimers.set(line, timer);
1204
+ this.announce(`${role === "assistant" ? "AI" : "You"}: ${normalized}`);
1205
+ }
1206
+ showConfirmation(value) {
1207
+ this.currentConfirmation = value;
1208
+ this.currentVisualRequest = null;
1209
+ this.hideStatus();
1210
+ this.stateMachine.transition("active.awaiting_confirmation");
1211
+ const presentation = confirmationPresentation(value, this.messages.actionRequest, this.messages.approve);
1212
+ this.renderSafetyCard(presentation.title, presentation.copy, presentation.approve, this.messages.decline, () => this.resolveConfirmation(true), () => this.resolveConfirmation(false));
1213
+ }
1214
+ showActionState(status) {
1215
+ this.currentConfirmation = null;
1216
+ this.hideSafetyCard();
1217
+ if (status === "running") {
1218
+ this.stateMachine.transition("active.acting");
1219
+ this.showTransientStatus(this.messages.actionRunning, 4000);
1220
+ return;
1221
+ }
1222
+ this.stateMachine.transition("active.ready");
1223
+ if (status === "verified") {
1224
+ this.hideStatus();
1225
+ return;
1226
+ }
1227
+ this.showTransientStatus(this.messages.actionFailed, 3500);
1228
+ }
1229
+ dismissActionState() {
1230
+ this.currentConfirmation = null;
1231
+ this.hideSafetyCard();
1232
+ this.hideStatus();
1233
+ if (this.active)
1234
+ this.stateMachine.transition("active.ready");
1235
+ }
1236
+ showVisualRequest(value) {
1237
+ this.currentVisualRequest = value;
1238
+ this.currentConfirmation = null;
1239
+ this.hideStatus();
1240
+ this.stateMachine.transition("active.awaiting_confirmation");
1241
+ this.renderSafetyCard(this.messages.visualRequest, value.reason, this.messages.shareVisual, this.messages.declineVisual, () => this.resolveVisualRequest(true), () => this.resolveVisualRequest(false));
1242
+ }
1243
+ showVisualResult(shared) {
1244
+ this.currentVisualRequest = null;
1245
+ this.hideSafetyCard();
1246
+ this.stateMachine.transition("active.ready");
1247
+ this.showTransientStatus(shared ? this.messages.visualShared : this.messages.visualUnavailable, 3500);
1248
+ }
1249
+ setVisualAvailable(_available, _paused = false) {
1250
+ void _available;
1251
+ void _paused;
1252
+ }
1253
+ setVisualPaused(_paused) {
1254
+ void _paused;
1255
+ }
1256
+ setPaused(value) {
1257
+ if (value)
1258
+ this.showTransientStatus(this.messages.paused, 0);
1259
+ else
1260
+ this.hideStatus();
1261
+ }
1262
+ setMuted(_value) {
1263
+ void _value;
1264
+ }
1265
+ configureVoiceControls(_voice, _manual, _canUseAutomatic) {
1266
+ void _voice;
1267
+ void _manual;
1268
+ void _canUseAutomatic;
1269
+ }
1270
+ setManualTurn(_value) {
1271
+ void _value;
1272
+ }
1273
+ setPushActive(_value) {
1274
+ void _value;
1275
+ }
1276
+ interruptPlayback() {
1277
+ this.audio.pause();
1278
+ this.stopLevelLoop();
1279
+ this.host.style.setProperty("--voice-level", "0");
1280
+ if (this.active)
1281
+ this.stateMachine.transition("active.ready");
1282
+ }
1283
+ showError(message, retryable = false, sessionActive = this.active) {
1284
+ const safeMessage = message || this.messages.error;
1285
+ if (sessionActive && this.active) {
1286
+ this.stateMachine.transition(retryable ? "recoverable_error" : "terminal_error");
1287
+ this.hideStatus();
1288
+ this.renderRecoveryCard(safeMessage, retryable);
1289
+ return;
1290
+ }
1291
+ this.active = false;
1292
+ this.stateMachine.transition(retryable ? "recoverable_error" : "terminal_error");
1293
+ this.orb.hidden = true;
1294
+ this.renderRecoveryCard(safeMessage, retryable);
1295
+ }
1296
+ showEnded(message) {
1297
+ this.active = false;
1298
+ this.hideSafetyCard();
1299
+ this.recoveryCard.hidden = true;
1300
+ this.recoveryCard.replaceChildren();
1301
+ this.clearCaptions();
1302
+ this.hideStatus();
1303
+ this.stopAudio();
1304
+ this.stateMachine.transition("ending");
1305
+ this.orb.dataset.active = "false";
1306
+ this.orb.setAttribute("aria-label", this.launcherLabel());
1307
+ this.stateMachine.transition("ended");
1308
+ if (message)
1309
+ this.showTransientStatus(message, 3000);
1310
+ this.stateMachine.transition("launcher");
1311
+ this.showAttribution();
1312
+ this.placement.refresh();
1313
+ }
1314
+ showLimit(message) {
1315
+ this.showError(message || this.messages.limitReached, false, false);
1316
+ }
1317
+ showActiveElsewhere(active) {
1318
+ this.activeElsewhere = active;
1319
+ this.orb.disabled = active;
1320
+ this.orb.hidden = active;
1321
+ this.orb.setAttribute("aria-label", active ? this.messages.activeElsewhere : this.launcherLabel());
1322
+ if (active) {
1323
+ this.closeActivation(false);
1324
+ this.hideSafetyCard();
1325
+ this.recoveryCard.hidden = true;
1326
+ this.hideStatus();
1327
+ return;
1328
+ }
1329
+ this.hideStatus();
1330
+ }
1331
+ showLaunchUnavailable(message) {
1332
+ this.active = false;
1333
+ this.orb.hidden = true;
1334
+ this.renderRecoveryCard(message, false);
1335
+ this.stateMachine.reset();
1336
+ }
1337
+ updateHighlight(element, status) {
1338
+ this.highlightResizeObserver?.disconnect();
1339
+ this.unbindHighlightScrollTargets();
1340
+ this.highlightTarget = element?.isConnected ? element : null;
1341
+ this.highlight.className = `action-highlight ${status}`;
1342
+ if (!this.highlightTarget)
1343
+ return;
1344
+ this.highlightResizeObserver?.observe(this.highlightTarget);
1345
+ this.bindHighlightScrollTargets(this.highlightTarget);
1346
+ this.renderHighlight();
1347
+ }
1348
+ safetySurfaceVisible() {
1349
+ if (document.visibilityState === "hidden")
1350
+ return false;
1351
+ const surface = this.safetyCard.hidden
1352
+ ? this.active && !this.orb.hidden
1353
+ ? this.orb
1354
+ : null
1355
+ : this.safetyCard;
1356
+ if (!surface)
1357
+ return false;
1358
+ const rect = surface.getBoundingClientRect();
1359
+ const computed = getComputedStyle(surface);
1360
+ if (rect.width < 2 ||
1361
+ rect.height < 2 ||
1362
+ rect.bottom <= 0 ||
1363
+ rect.right <= 0 ||
1364
+ rect.top >= window.innerHeight ||
1365
+ rect.left >= window.innerWidth ||
1366
+ computed.display === "none" ||
1367
+ computed.visibility === "hidden" ||
1368
+ Number.parseFloat(computed.opacity || "1") <= 0) {
1369
+ return false;
1370
+ }
1371
+ const x = Math.max(0, Math.min(window.innerWidth - 1, rect.left + rect.width / 2));
1372
+ const y = Math.max(0, Math.min(window.innerHeight - 1, rect.top + 20));
1373
+ const documentTop = document.elementFromPoint(x, y);
1374
+ if (documentTop !== this.host && !surface.contains(documentTop))
1375
+ return false;
1376
+ const shadowTop = this.shadow.elementFromPoint?.(x, y) ?? null;
1377
+ return !shadowTop || surface.contains(shadowTop);
1378
+ }
1379
+ cancelPendingConfirmation() {
1380
+ if (this.resolvePendingConfirmation(false))
1381
+ return true;
1382
+ if (this.currentVisualRequest) {
1383
+ this.resolveVisualRequest(false);
1384
+ return true;
1385
+ }
1386
+ return false;
1387
+ }
1388
+ resolvePendingConfirmation(approved) {
1389
+ if (!this.currentConfirmation)
1390
+ return false;
1391
+ this.resolveConfirmation(approved);
1392
+ return true;
1393
+ }
1394
+ destroy() {
1395
+ if (this.destroyed)
1396
+ return;
1397
+ this.destroyed = true;
1398
+ document.removeEventListener("visibilitychange", this.onVisibilityChange);
1399
+ document.removeEventListener("scroll", this.scheduleHighlightLayout, true);
1400
+ window.removeEventListener("resize", this.scheduleHighlightLayout);
1401
+ window.visualViewport?.removeEventListener("resize", this.scheduleHighlightLayout);
1402
+ window.visualViewport?.removeEventListener("scroll", this.scheduleHighlightLayout);
1403
+ this.modalLayer.removeEventListener("keydown", this.onModalKeyDown);
1404
+ if (this.wakeTimer)
1405
+ clearTimeout(this.wakeTimer);
1406
+ if (this.statusTimer)
1407
+ clearTimeout(this.statusTimer);
1408
+ if (this.attributionTimer)
1409
+ clearTimeout(this.attributionTimer);
1410
+ if (this.captionLayoutFrame !== null)
1411
+ cancelAnimationFrame(this.captionLayoutFrame);
1412
+ if (this.highlightFrame !== null)
1413
+ cancelAnimationFrame(this.highlightFrame);
1414
+ this.captionResizeObserver?.disconnect();
1415
+ this.highlightResizeObserver?.disconnect();
1416
+ this.unbindHighlightScrollTargets();
1417
+ this.placement.destroy();
1418
+ this.clearCaptions();
1419
+ this.stopAudio();
1420
+ this.host.remove();
1421
+ }
1422
+ build() {
1423
+ const fragment = document.createDocumentFragment();
1424
+ const presenceField = document.createElement("div");
1425
+ presenceField.className = "presence-field";
1426
+ presenceField.dataset.role = "presence-field";
1427
+ presenceField.setAttribute("aria-hidden", "true");
1428
+ const orbWrap = document.createElement("div");
1429
+ orbWrap.className = "orb-wrap";
1430
+ const captions = document.createElement("div");
1431
+ captions.className = "caption-stage";
1432
+ captions.setAttribute("aria-hidden", "true");
1433
+ const subtitleTrack = document.createElement("div");
1434
+ subtitleTrack.className = "subtitle-track";
1435
+ subtitleTrack.dataset.role = "subtitle-track";
1436
+ captions.append(subtitleTrack);
1437
+ const status = document.createElement("div");
1438
+ status.className = "status-bubble";
1439
+ status.dataset.role = "status-bubble";
1440
+ status.hidden = true;
1441
+ status.setAttribute("role", "status");
1442
+ const attribution = document.createElement("div");
1443
+ attribution.className = "attribution-bubble";
1444
+ attribution.dataset.role = "attribution";
1445
+ attribution.hidden = true;
1446
+ attribution.setAttribute("role", "status");
1447
+ attribution.append(document.createTextNode("Powered by "));
1448
+ const attributionLink = document.createElement("a");
1449
+ attributionLink.href = "https://oxvo.com/";
1450
+ attributionLink.target = "_blank";
1451
+ attributionLink.rel = "noopener noreferrer";
1452
+ attributionLink.textContent = "OXVO.com";
1453
+ attribution.append(attributionLink);
1454
+ const safety = document.createElement("div");
1455
+ safety.className = "safety-card";
1456
+ safety.dataset.role = "safety-card";
1457
+ safety.hidden = true;
1458
+ safety.setAttribute("role", "alertdialog");
1459
+ safety.setAttribute("aria-modal", "false");
1460
+ const recovery = document.createElement("div");
1461
+ recovery.className = "recovery-card";
1462
+ recovery.dataset.role = "recovery-card";
1463
+ recovery.hidden = true;
1464
+ recovery.setAttribute("role", "alertdialog");
1465
+ recovery.setAttribute("aria-modal", "false");
1466
+ const orb = document.createElement("button");
1467
+ orb.type = "button";
1468
+ orb.className = "orb";
1469
+ orb.dataset.role = "orb";
1470
+ orb.dataset.active = "false";
1471
+ orb.setAttribute("aria-label", this.launcherLabel());
1472
+ const surface = document.createElement("span");
1473
+ surface.className = "orb-surface";
1474
+ const rim = document.createElement("span");
1475
+ rim.className = "orb-rim";
1476
+ const core = document.createElement("span");
1477
+ core.className = "orb-core";
1478
+ const start = document.createElement("span");
1479
+ start.className = "orb-icon start-icon";
1480
+ const customStartIcon = (0, safeSvg_js_1.createSafeSvgIcon)(this.voiceConfig.launcher.customIconSvg);
1481
+ if (customStartIcon) {
1482
+ customStartIcon.classList.add("start-symbol", "custom-start-symbol");
1483
+ start.append(customStartIcon);
1484
+ }
1485
+ else {
1486
+ start.append(icon(lucide_1.AudioWaveform, "start-symbol"));
1487
+ }
1488
+ const end = document.createElement("span");
1489
+ end.className = "orb-icon end-icon";
1490
+ end.append(icon(lucide_1.X, "end-symbol"));
1491
+ orb.append(surface, rim, core, start, end);
1492
+ if (this.voiceConfig.launcher.showLabelOnFirstVisit) {
1493
+ const label = document.createElement("span");
1494
+ label.className = "first-label";
1495
+ label.dataset.role = "first-label";
1496
+ label.textContent = this.config.appearance?.launcherLabel || "Talk to us";
1497
+ orbWrap.append(label);
1498
+ }
1499
+ orbWrap.append(captions, status, attribution, safety, recovery, orb);
1500
+ const modalLayer = document.createElement("div");
1501
+ modalLayer.className = "dialog-layer";
1502
+ modalLayer.dataset.role = "activation-layer";
1503
+ modalLayer.hidden = true;
1504
+ const dialog = document.createElement("section");
1505
+ dialog.className = "dialog";
1506
+ dialog.setAttribute("role", "dialog");
1507
+ dialog.setAttribute("aria-modal", "true");
1508
+ dialog.setAttribute("aria-labelledby", "oxvo-voice-title");
1509
+ dialog.setAttribute("aria-describedby", "oxvo-voice-description");
1510
+ const head = document.createElement("div");
1511
+ head.className = "dialog-head";
1512
+ const copy = document.createElement("div");
1513
+ const title = document.createElement("h2");
1514
+ title.id = "oxvo-voice-title";
1515
+ title.textContent = this.voiceConfig.activation.title;
1516
+ const description = document.createElement("p");
1517
+ description.id = "oxvo-voice-description";
1518
+ description.textContent = this.voiceConfig.activation.description;
1519
+ copy.append(title, description);
1520
+ const close = document.createElement("button");
1521
+ close.type = "button";
1522
+ close.className = "icon-button";
1523
+ close.dataset.role = "activation-close";
1524
+ close.setAttribute("aria-label", this.voiceConfig.activation.secondaryLabel);
1525
+ close.append(icon(lucide_1.X, "close-symbol"));
1526
+ head.append(copy, close);
1527
+ const agreement = document.createElement("label");
1528
+ agreement.className = "agreement";
1529
+ const checkbox = document.createElement("input");
1530
+ checkbox.type = "checkbox";
1531
+ checkbox.checked = true;
1532
+ checkbox.dataset.role = "agreement";
1533
+ const agreementText = document.createElement("span");
1534
+ agreementText.textContent = `I agree to use ${this.config.appearance?.brandName || "AI voice assist"}.`;
1535
+ agreement.append(checkbox, agreementText);
1536
+ if (this.voiceConfig.activation.detailsLabel) {
1537
+ const details = document.createElement("details");
1538
+ const summary = document.createElement("summary");
1539
+ summary.textContent = this.voiceConfig.activation.detailsLabel;
1540
+ const legal = document.createElement("div");
1541
+ legal.className = "legal-copy";
1542
+ const disclosure = document.createElement("p");
1543
+ disclosure.textContent =
1544
+ this.config.consent?.disclosure || this.messages.disclosure;
1545
+ const boundary = document.createElement("p");
1546
+ boundary.textContent = this.messages.safetyBoundary;
1547
+ const links = document.createElement("div");
1548
+ links.className = "legal-links";
1549
+ if (this.config.consent?.policyUrl) {
1550
+ links.append(this.legalLink(this.config.consent.policyUrl, this.messages.privacyPolicy));
1551
+ }
1552
+ if (this.config.consent?.termsUrl) {
1553
+ links.append(this.legalLink(this.config.consent.termsUrl, this.messages.termsOfService));
1554
+ }
1555
+ legal.append(disclosure, boundary, links);
1556
+ details.append(summary, legal);
1557
+ dialog.append(head, agreement, details);
1558
+ }
1559
+ else {
1560
+ dialog.append(head, agreement);
1561
+ }
1562
+ const actions = document.createElement("div");
1563
+ actions.className = "dialog-actions";
1564
+ const secondary = document.createElement("button");
1565
+ secondary.type = "button";
1566
+ secondary.className = "button";
1567
+ secondary.dataset.role = "activation-secondary";
1568
+ secondary.textContent = this.voiceConfig.activation.secondaryLabel;
1569
+ const primary = document.createElement("button");
1570
+ primary.type = "button";
1571
+ primary.className = "button primary";
1572
+ primary.dataset.role = "activation-primary";
1573
+ primary.textContent = this.voiceConfig.activation.primaryLabel;
1574
+ actions.append(secondary, primary);
1575
+ dialog.append(actions);
1576
+ modalLayer.append(dialog);
1577
+ const live = document.createElement("div");
1578
+ live.className = "hidden-live";
1579
+ live.dataset.role = "live-region";
1580
+ live.setAttribute("aria-live", "polite");
1581
+ live.setAttribute("aria-atomic", "false");
1582
+ const audio = document.createElement("audio");
1583
+ audio.autoplay = true;
1584
+ audio.setAttribute("playsinline", "");
1585
+ audio.setAttribute("aria-hidden", "true");
1586
+ const highlight = document.createElement("div");
1587
+ highlight.className = "action-highlight";
1588
+ highlight.setAttribute("aria-hidden", "true");
1589
+ fragment.append(presenceField, orbWrap, modalLayer, live, audio, highlight);
1590
+ return fragment;
1591
+ }
1592
+ bind() {
1593
+ this.orb.addEventListener("click", () => {
1594
+ if (this.active) {
1595
+ this.stateMachine.transition("ending");
1596
+ this.callbacks.onEnd();
1597
+ return;
1598
+ }
1599
+ if (this.activeElsewhere)
1600
+ return;
1601
+ this.openActivation();
1602
+ });
1603
+ this.require('[data-role="activation-close"]').addEventListener("click", () => this.closeActivation());
1604
+ this.require('[data-role="activation-secondary"]').addEventListener("click", () => this.closeActivation());
1605
+ this.require('[data-role="activation-primary"]').addEventListener("click", () => {
1606
+ if (!this.agreement.checked) {
1607
+ this.agreement.focus();
1608
+ return;
1609
+ }
1610
+ this.ensureAudioContext();
1611
+ this.showPreflight();
1612
+ const consent = this.config.consent;
1613
+ const scopes = [
1614
+ ...new Set([
1615
+ ...(consent?.requiredScopes ?? []),
1616
+ ...(consent?.optionalScopes ?? []),
1617
+ ]),
1618
+ ];
1619
+ this.callbacks.onStart("voice", scopes);
1620
+ });
1621
+ this.modalLayer.addEventListener("keydown", this.onModalKeyDown);
1622
+ this.modalLayer.addEventListener("pointerdown", (event) => {
1623
+ if (event.target === this.modalLayer)
1624
+ this.closeActivation();
1625
+ });
1626
+ document.addEventListener("visibilitychange", this.onVisibilityChange);
1627
+ document.addEventListener("scroll", this.scheduleHighlightLayout, {
1628
+ capture: true,
1629
+ passive: true,
1630
+ });
1631
+ window.addEventListener("resize", this.scheduleHighlightLayout, {
1632
+ passive: true,
1633
+ });
1634
+ window.visualViewport?.addEventListener("resize", this.scheduleHighlightLayout, {
1635
+ passive: true,
1636
+ });
1637
+ window.visualViewport?.addEventListener("scroll", this.scheduleHighlightLayout, {
1638
+ passive: true,
1639
+ });
1640
+ }
1641
+ bindPlaybackState() {
1642
+ this.audio.addEventListener("playing", () => {
1643
+ this.playbackState = "playing";
1644
+ this.emitPlaybackState("playing");
1645
+ this.syncPresenceFromPlayback();
1646
+ });
1647
+ this.audio.addEventListener("waiting", () => {
1648
+ this.playbackState = "waiting";
1649
+ this.emitPlaybackState("waiting");
1650
+ this.syncPresenceFromPlayback();
1651
+ });
1652
+ this.audio.addEventListener("pause", () => {
1653
+ this.playbackState = "paused";
1654
+ this.emitPlaybackState("paused");
1655
+ this.syncPresenceFromPlayback();
1656
+ });
1657
+ this.audio.addEventListener("ended", () => {
1658
+ this.playbackState = "ended";
1659
+ this.emitPlaybackState("ended");
1660
+ this.syncPresenceFromPlayback();
1661
+ });
1662
+ }
1663
+ resumeRemotePlayback() {
1664
+ if (!this.active ||
1665
+ !this.audio.paused ||
1666
+ !this.audio.srcObject ||
1667
+ this.playbackResumePending) {
1668
+ return;
1669
+ }
1670
+ this.playbackResumePending = true;
1671
+ void this.audio
1672
+ .play()
1673
+ .catch(() => {
1674
+ this.emitPlaybackState("blocked");
1675
+ this.showTransientStatus("Tap the voice button to resume audio.", 6000);
1676
+ })
1677
+ .finally(() => {
1678
+ this.playbackResumePending = false;
1679
+ });
1680
+ }
1681
+ effectivePresenceState() {
1682
+ const state = this.serverPresenceState;
1683
+ if (state === "ending")
1684
+ return "ending";
1685
+ if (state === "acting" ||
1686
+ state === "awaiting_confirmation" ||
1687
+ state === "reconnecting") {
1688
+ return `active.${state}`;
1689
+ }
1690
+ if (this.playbackState === "playing")
1691
+ return "active.speaking";
1692
+ if (state === "speaking") {
1693
+ return this.playbackState === "paused" || this.playbackState === "ended"
1694
+ ? "active.ready"
1695
+ : "active.thinking";
1696
+ }
1697
+ return `active.${state}`;
1698
+ }
1699
+ syncPresenceFromPlayback() {
1700
+ if (!this.active)
1701
+ return;
1702
+ this.stateMachine.transition(this.effectivePresenceState());
1703
+ this.syncLevelLoop();
1704
+ }
1705
+ syncLevelLoop() {
1706
+ if (this.stateMachine.state === "active.listening" ||
1707
+ this.stateMachine.state === "active.speaking") {
1708
+ this.startLevelLoop();
1709
+ }
1710
+ else {
1711
+ this.stopLevelLoop();
1712
+ }
1713
+ }
1714
+ emitPlaybackState(state) {
1715
+ this.callbacks.onPlaybackState({
1716
+ state,
1717
+ audible: state === "playing" && !this.audio.muted && this.audio.volume > 0,
1718
+ utteranceId: null,
1719
+ sampledAt: new Date().toISOString(),
1720
+ });
1721
+ }
1722
+ applyConfig() {
1723
+ const accent = this.config.appearance?.accent;
1724
+ if (accent && /^#[0-9a-fA-F]{6}$/.test(accent)) {
1725
+ this.host.style.setProperty("--voice-accent", accent);
1726
+ }
1727
+ this.host.style.setProperty("--voice-inline-offset", `${this.voiceConfig.launcher.offsetInlinePx}px`);
1728
+ this.host.style.setProperty("--voice-block-offset", `${this.voiceConfig.launcher.offsetBlockPx}px`);
1729
+ this.host.style.setProperty("--voice-orb-size", this.voiceConfig.launcher.size === "compact" ? "52px" : "58px");
1730
+ }
1731
+ openActivation() {
1732
+ this.hideAttribution();
1733
+ const shadowActiveElement = this.shadow.activeElement;
1734
+ const documentActiveElement = document.activeElement;
1735
+ this.restoreFocus =
1736
+ shadowActiveElement instanceof HTMLElement
1737
+ ? shadowActiveElement
1738
+ : documentActiveElement instanceof HTMLElement &&
1739
+ documentActiveElement !== this.host
1740
+ ? documentActiveElement
1741
+ : this.orb;
1742
+ this.require('[data-role="first-label"]', false)?.remove();
1743
+ this.placement.refresh(true);
1744
+ this.modalLayer.hidden = false;
1745
+ this.stateMachine.transition("activation_modal");
1746
+ queueMicrotask(() => this.agreement.focus());
1747
+ }
1748
+ closeActivation(restore = true) {
1749
+ if (this.modalLayer.hidden)
1750
+ return;
1751
+ this.modalLayer.hidden = true;
1752
+ if (this.stateMachine.state === "activation_modal")
1753
+ this.stateMachine.transition("launcher");
1754
+ if (restore)
1755
+ this.restoreFocus?.focus();
1756
+ this.restoreFocus = null;
1757
+ this.placement.refresh();
1758
+ }
1759
+ showAttribution() {
1760
+ this.hideAttribution();
1761
+ if (this.config.appearance?.attribution?.enabled === false)
1762
+ return;
1763
+ this.attributionBubble.hidden = false;
1764
+ this.attributionTimer = setTimeout(() => {
1765
+ this.attributionTimer = null;
1766
+ this.attributionBubble.hidden = true;
1767
+ }, 7000);
1768
+ }
1769
+ hideAttribution() {
1770
+ if (this.attributionTimer)
1771
+ clearTimeout(this.attributionTimer);
1772
+ this.attributionTimer = null;
1773
+ this.attributionBubble.hidden = true;
1774
+ }
1775
+ renderSafetyCard(title, copy, approveLabel, declineLabel, approve, decline) {
1776
+ const heading = document.createElement("h3");
1777
+ heading.className = "card-title";
1778
+ heading.textContent = title.slice(0, 160);
1779
+ const body = document.createElement("p");
1780
+ body.className = "card-copy";
1781
+ body.textContent = copy.slice(0, 600);
1782
+ const actions = document.createElement("div");
1783
+ actions.className = "card-actions";
1784
+ const no = document.createElement("button");
1785
+ no.type = "button";
1786
+ no.className = "button";
1787
+ no.dataset.role = "safety-decline";
1788
+ no.textContent = declineLabel;
1789
+ no.addEventListener("click", decline, { once: true });
1790
+ const yes = document.createElement("button");
1791
+ yes.type = "button";
1792
+ yes.className = "button primary";
1793
+ yes.dataset.role = "safety-approve";
1794
+ yes.textContent = approveLabel;
1795
+ yes.addEventListener("click", approve, { once: true });
1796
+ actions.append(no, yes);
1797
+ this.safetyCard.replaceChildren(heading, body, actions);
1798
+ this.safetyCard.hidden = false;
1799
+ this.syncSubtitleOcclusion();
1800
+ queueMicrotask(() => no.focus());
1801
+ }
1802
+ hideSafetyCard() {
1803
+ this.safetyCard.hidden = true;
1804
+ this.safetyCard.replaceChildren();
1805
+ this.syncSubtitleOcclusion();
1806
+ }
1807
+ resolveConfirmation(approved) {
1808
+ const confirmation = this.currentConfirmation;
1809
+ if (!confirmation)
1810
+ return;
1811
+ this.currentConfirmation = null;
1812
+ this.hideSafetyCard();
1813
+ this.stateMachine.transition("active.ready");
1814
+ this.callbacks.onConfirmation(confirmation.actionId, confirmation.proposalChecksum, approved);
1815
+ }
1816
+ resolveVisualRequest(approved) {
1817
+ const request = this.currentVisualRequest;
1818
+ if (!request)
1819
+ return;
1820
+ this.currentVisualRequest = null;
1821
+ this.hideSafetyCard();
1822
+ this.stateMachine.transition("active.ready");
1823
+ this.callbacks.onVisualContext(request, approved);
1824
+ }
1825
+ renderRecoveryCard(message, retryable) {
1826
+ const badge = document.createElement("div");
1827
+ badge.className = "recovery-badge";
1828
+ badge.setAttribute("aria-hidden", "true");
1829
+ badge.append(icon(lucide_1.ShieldCheck, "recovery-symbol"));
1830
+ const title = document.createElement("h2");
1831
+ title.className = "card-title";
1832
+ title.id = "oxvo-voice-recovery-title";
1833
+ title.textContent = this.messages.introTitle;
1834
+ const copy = document.createElement("p");
1835
+ copy.className = "card-copy";
1836
+ copy.id = "oxvo-voice-recovery-copy";
1837
+ copy.textContent = message.slice(0, 600);
1838
+ const actions = document.createElement("div");
1839
+ actions.className = "card-actions";
1840
+ const close = document.createElement("button");
1841
+ close.type = "button";
1842
+ close.className = "icon-button recovery-close";
1843
+ close.dataset.role = "recovery-close";
1844
+ close.setAttribute("aria-label", this.messages.close);
1845
+ close.append(icon(lucide_1.X, "close-symbol"));
1846
+ close.addEventListener("click", () => {
1847
+ this.recoveryCard.hidden = true;
1848
+ this.recoveryCard.replaceChildren();
1849
+ this.syncSubtitleOcclusion();
1850
+ });
1851
+ const heading = document.createElement("div");
1852
+ heading.className = "recovery-head";
1853
+ heading.append(badge, title, close);
1854
+ if (retryable) {
1855
+ const retry = document.createElement("button");
1856
+ retry.type = "button";
1857
+ retry.className = "button primary";
1858
+ retry.append(icon(lucide_1.RefreshCw, "retry-symbol"), document.createTextNode(this.messages.retry));
1859
+ retry.addEventListener("click", () => {
1860
+ this.recoveryCard.hidden = true;
1861
+ this.syncSubtitleOcclusion();
1862
+ this.callbacks.onRetry();
1863
+ });
1864
+ actions.append(retry);
1865
+ }
1866
+ const fallback = this.configuredFallback();
1867
+ if (fallback) {
1868
+ const fallbackButton = document.createElement("button");
1869
+ fallbackButton.type = "button";
1870
+ fallbackButton.className = "button";
1871
+ fallbackButton.dataset.role = "configured-fallback";
1872
+ fallbackButton.textContent =
1873
+ fallback === "standard_assist"
1874
+ ? this.messages.continueWithFullAssist
1875
+ : this.messages.requestHumanFallback;
1876
+ fallbackButton.addEventListener("click", () => {
1877
+ fallbackButton.disabled = true;
1878
+ this.recoveryCard.hidden = true;
1879
+ this.recoveryCard.replaceChildren();
1880
+ this.syncSubtitleOcclusion();
1881
+ this.onFallback(fallback);
1882
+ }, { once: true });
1883
+ actions.append(fallbackButton);
1884
+ }
1885
+ this.recoveryCard.setAttribute("aria-labelledby", title.id);
1886
+ this.recoveryCard.setAttribute("aria-describedby", copy.id);
1887
+ this.recoveryCard.replaceChildren(heading, copy);
1888
+ if (actions.childElementCount > 0)
1889
+ this.recoveryCard.append(actions);
1890
+ this.recoveryCard.hidden = false;
1891
+ this.syncSubtitleOcclusion();
1892
+ }
1893
+ syncSubtitleOcclusion() {
1894
+ const occluded = !this.safetyCard.hidden || !this.recoveryCard.hidden;
1895
+ this.captionStage.hidden = occluded;
1896
+ if (occluded) {
1897
+ this.host.style.setProperty("--voice-subtitle-height", "0px");
1898
+ return;
1899
+ }
1900
+ this.layoutCaptions();
1901
+ }
1902
+ configuredFallback() {
1903
+ if (this.voiceConfig.fallback === "offer_standard_assist" &&
1904
+ (this.config.capabilities?.text || this.config.capabilities?.voice)) {
1905
+ return "standard_assist";
1906
+ }
1907
+ if (this.voiceConfig.fallback === "offer_human_handoff" &&
1908
+ this.config.capabilities?.handoff) {
1909
+ return "human_handoff";
1910
+ }
1911
+ return null;
1912
+ }
1913
+ showTransientStatus(value, visibleMs) {
1914
+ if (this.statusTimer)
1915
+ clearTimeout(this.statusTimer);
1916
+ this.statusTimer = null;
1917
+ this.statusBubble.textContent = value.slice(0, 240);
1918
+ this.statusBubble.hidden = false;
1919
+ if (visibleMs > 0) {
1920
+ this.statusTimer = setTimeout(() => {
1921
+ this.statusTimer = null;
1922
+ this.hideStatus();
1923
+ }, visibleMs);
1924
+ }
1925
+ }
1926
+ hideStatus() {
1927
+ if (this.statusTimer)
1928
+ clearTimeout(this.statusTimer);
1929
+ this.statusTimer = null;
1930
+ this.statusBubble.hidden = true;
1931
+ this.statusBubble.textContent = "";
1932
+ }
1933
+ layoutCaptions() {
1934
+ const availableHeight = Math.max(180, this.captionStage.clientHeight || 680);
1935
+ while (this.subtitleTrack.childElementCount > 1 &&
1936
+ this.subtitleTrack.scrollHeight > availableHeight) {
1937
+ const oldest = this.subtitleTrack.firstElementChild;
1938
+ if (!oldest)
1939
+ break;
1940
+ this.discardCaption(oldest);
1941
+ }
1942
+ const currentLines = Array.from(this.subtitleTrack.querySelectorAll(".caption"));
1943
+ currentLines.forEach((line, index) => {
1944
+ line.classList.toggle("previous", index < currentLines.length - 1);
1945
+ });
1946
+ const trackHeight = this.subtitleTrack.childElementCount > 0
1947
+ ? Math.ceil(this.subtitleTrack.getBoundingClientRect().height ||
1948
+ this.subtitleTrack.offsetHeight)
1949
+ : 0;
1950
+ this.host.style.setProperty("--voice-subtitle-height", `${trackHeight}px`);
1951
+ }
1952
+ discardCaption(line) {
1953
+ const timer = this.captionTimers.get(line);
1954
+ if (timer)
1955
+ clearTimeout(timer);
1956
+ this.captionTimers.delete(line);
1957
+ this.captionResizeObserver?.unobserve(line);
1958
+ line.remove();
1959
+ }
1960
+ removeCaption(line) {
1961
+ const timer = this.captionTimers.get(line);
1962
+ if (timer)
1963
+ clearTimeout(timer);
1964
+ this.captionTimers.delete(line);
1965
+ if (!line.isConnected)
1966
+ return;
1967
+ line.classList.add("leaving");
1968
+ const removalTimer = setTimeout(() => {
1969
+ this.captionTimers.delete(line);
1970
+ this.captionResizeObserver?.unobserve(line);
1971
+ line.remove();
1972
+ this.layoutCaptions();
1973
+ }, 190);
1974
+ this.captionTimers.set(line, removalTimer);
1975
+ }
1976
+ clearCaptions() {
1977
+ for (const timer of this.captionTimers.values())
1978
+ clearTimeout(timer);
1979
+ this.captionTimers.clear();
1980
+ this.captionResizeObserver?.disconnect();
1981
+ this.subtitleTrack.replaceChildren();
1982
+ this.host.style.setProperty("--voice-subtitle-height", "0px");
1983
+ this.liveRegion.replaceChildren();
1984
+ }
1985
+ bindHighlightScrollTargets(target) {
1986
+ let ancestor = target;
1987
+ while (true) {
1988
+ if (ancestor.parentElement) {
1989
+ ancestor = ancestor.parentElement;
1990
+ }
1991
+ else {
1992
+ const root = ancestor.getRootNode();
1993
+ ancestor = root instanceof ShadowRoot ? root.host : null;
1994
+ }
1995
+ if (!ancestor)
1996
+ break;
1997
+ ancestor.addEventListener("scroll", this.scheduleHighlightLayout, {
1998
+ passive: true,
1999
+ });
2000
+ this.highlightScrollTargets.push(ancestor);
2001
+ }
2002
+ }
2003
+ unbindHighlightScrollTargets() {
2004
+ for (const target of this.highlightScrollTargets) {
2005
+ target.removeEventListener("scroll", this.scheduleHighlightLayout);
2006
+ }
2007
+ this.highlightScrollTargets = [];
2008
+ }
2009
+ renderHighlight() {
2010
+ const target = this.highlightTarget;
2011
+ if (!target?.isConnected) {
2012
+ this.highlightTarget = null;
2013
+ this.highlightResizeObserver?.disconnect();
2014
+ this.unbindHighlightScrollTargets();
2015
+ this.highlight.classList.remove("visible");
2016
+ return;
2017
+ }
2018
+ const targetRect = target.getBoundingClientRect();
2019
+ const targetStyle = getComputedStyle(target);
2020
+ if (targetRect.width < 1 ||
2021
+ targetRect.height < 1 ||
2022
+ targetStyle.display === "none" ||
2023
+ targetStyle.visibility === "hidden" ||
2024
+ Number.parseFloat(targetStyle.opacity || "1") <= 0) {
2025
+ this.highlight.classList.remove("visible");
2026
+ return;
2027
+ }
2028
+ const visual = window.visualViewport;
2029
+ const viewportLeft = visual?.offsetLeft ?? 0;
2030
+ const viewportTop = visual?.offsetTop ?? 0;
2031
+ const viewportRight = viewportLeft + (visual?.width ?? window.innerWidth);
2032
+ const viewportBottom = viewportTop + (visual?.height ?? window.innerHeight);
2033
+ const left = Math.max(viewportLeft, targetRect.left - 5);
2034
+ const top = Math.max(viewportTop, targetRect.top - 5);
2035
+ const right = Math.min(viewportRight, targetRect.right + 5);
2036
+ const bottom = Math.min(viewportBottom, targetRect.bottom + 5);
2037
+ if (right - left < 2 || bottom - top < 2) {
2038
+ this.highlight.classList.remove("visible");
2039
+ return;
2040
+ }
2041
+ this.highlight.style.left = `${left}px`;
2042
+ this.highlight.style.top = `${top}px`;
2043
+ this.highlight.style.width = `${right - left}px`;
2044
+ this.highlight.style.height = `${bottom - top}px`;
2045
+ this.highlight.classList.add("visible");
2046
+ }
2047
+ announce(value) {
2048
+ const line = document.createElement("span");
2049
+ line.textContent = value;
2050
+ this.liveRegion.append(line);
2051
+ while (this.liveRegion.children.length > MAX_LIVE_ANNOUNCEMENTS) {
2052
+ this.liveRegion.firstElementChild?.remove();
2053
+ }
2054
+ }
2055
+ ensureAudioContext() {
2056
+ if (this.audioContext) {
2057
+ void this.audioContext.resume().catch(() => undefined);
2058
+ return;
2059
+ }
2060
+ const AudioContextConstructor = window.AudioContext ||
2061
+ window
2062
+ .webkitAudioContext;
2063
+ if (!AudioContextConstructor)
2064
+ return;
2065
+ try {
2066
+ this.audioContext = new AudioContextConstructor({
2067
+ latencyHint: "interactive",
2068
+ });
2069
+ void this.audioContext.resume().catch(() => undefined);
2070
+ }
2071
+ catch {
2072
+ this.audioContext = null;
2073
+ }
2074
+ }
2075
+ connectOutputAnalyser(stream) {
2076
+ this.ensureAudioContext();
2077
+ const context = this.audioContext;
2078
+ if (!context)
2079
+ return;
2080
+ try {
2081
+ this.outputSource?.disconnect();
2082
+ this.outputAnalyser?.disconnect();
2083
+ this.outputAnalyser = context.createAnalyser();
2084
+ this.outputAnalyser.fftSize = 256;
2085
+ this.outputAnalyser.smoothingTimeConstant = 0.78;
2086
+ this.levelBuffer = new Uint8Array(this.outputAnalyser.fftSize);
2087
+ this.outputSource = context.createMediaStreamSource(stream);
2088
+ this.outputSource.connect(this.outputAnalyser);
2089
+ }
2090
+ catch {
2091
+ this.outputSource = null;
2092
+ this.outputAnalyser = null;
2093
+ this.levelBuffer = null;
2094
+ }
2095
+ }
2096
+ startLevelLoop() {
2097
+ if (this.levelFrame !== null ||
2098
+ !this.activeAnalyser() ||
2099
+ !this.levelBuffer ||
2100
+ document.hidden) {
2101
+ return;
2102
+ }
2103
+ const frame = (timestamp) => {
2104
+ this.levelFrame = null;
2105
+ const analyser = this.activeAnalyser();
2106
+ if (!this.active || document.hidden || !analyser || !this.levelBuffer) {
2107
+ this.host.style.setProperty("--voice-level", "0");
2108
+ return;
2109
+ }
2110
+ if (timestamp - this.lastLevelAt >= 50) {
2111
+ this.lastLevelAt = timestamp;
2112
+ analyser.getByteTimeDomainData(this.levelBuffer);
2113
+ let sum = 0;
2114
+ for (const sample of this.levelBuffer) {
2115
+ const normalized = (sample - 128) / 128;
2116
+ sum += normalized * normalized;
2117
+ }
2118
+ const rms = Math.sqrt(sum / this.levelBuffer.length);
2119
+ this.host.style.setProperty("--voice-level", Math.min(1, rms * 5.5).toFixed(3));
2120
+ }
2121
+ this.levelFrame = requestAnimationFrame(frame);
2122
+ };
2123
+ this.levelFrame = requestAnimationFrame(frame);
2124
+ }
2125
+ stopLevelLoop() {
2126
+ if (this.levelFrame !== null)
2127
+ cancelAnimationFrame(this.levelFrame);
2128
+ this.levelFrame = null;
2129
+ this.host.style.setProperty("--voice-level", "0");
2130
+ }
2131
+ stopAudio() {
2132
+ this.stopLevelLoop();
2133
+ this.audio.pause();
2134
+ const remoteStream = this.audio.srcObject;
2135
+ this.audio.srcObject = null;
2136
+ if (remoteStream &&
2137
+ typeof MediaStream !== "undefined" &&
2138
+ remoteStream instanceof MediaStream) {
2139
+ for (const track of remoteStream.getTracks())
2140
+ track.stop();
2141
+ }
2142
+ this.outputSource?.disconnect();
2143
+ this.outputSource = null;
2144
+ this.outputAnalyser?.disconnect();
2145
+ this.outputAnalyser = null;
2146
+ this.inputSource?.disconnect();
2147
+ this.inputSource = null;
2148
+ this.inputAnalyser?.disconnect();
2149
+ this.inputAnalyser = null;
2150
+ this.levelBuffer = null;
2151
+ if (this.audioContext)
2152
+ void this.audioContext.close().catch(() => undefined);
2153
+ this.audioContext = null;
2154
+ }
2155
+ activeAnalyser() {
2156
+ if (this.stateMachine.state === "active.listening")
2157
+ return this.inputAnalyser;
2158
+ if (this.stateMachine.state === "active.speaking" && !this.audio.paused) {
2159
+ return this.outputAnalyser;
2160
+ }
2161
+ return null;
2162
+ }
2163
+ legalLink(url, label) {
2164
+ const link = document.createElement("a");
2165
+ link.href = url;
2166
+ link.target = "_blank";
2167
+ link.rel = "noopener noreferrer";
2168
+ link.textContent = label;
2169
+ return link;
2170
+ }
2171
+ launcherLabel() {
2172
+ const identity = this.config.appearance?.brandName || "AI assistant";
2173
+ return `Talk to ${identity}`;
2174
+ }
2175
+ require(selector, required = true) {
2176
+ const value = this.shadow.querySelector(selector);
2177
+ if (!value && required)
2178
+ throw new Error(`Missing Voice Presence element: ${selector}`);
2179
+ return value;
2180
+ }
2181
+ }
2182
+ exports.VoicePresenceView = VoicePresenceView;