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