@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
package/lib/ui.js ADDED
@@ -0,0 +1,2349 @@
1
+ import { AudioLines, Captions, CaptionsOff, CircleStop, Ellipsis, Eye, EyeOff, Hand, Maximize2, MessageSquareText, Mic, MicOff, Minimize2, Pause, Play, RotateCcw, Send, Square, UserRound, X, createElement as createLucideElement, } from "lucide";
2
+ import { defaultMessages } from "./messages.js";
3
+ const PANEL_POSITION_KEY = "__oxvo_ai_live_assist_panel_position_v1";
4
+ const PANEL_VIEW_KEY = "__oxvo_ai_live_assist_panel_view_v1";
5
+ const PANEL_VISIBILITY_KEY = "__oxvo_ai_live_assist_panel_visibility_v1";
6
+ const SESSION_START_KEY = "__oxvo_ai_live_assist_session_start_v1";
7
+ const COMPACT_CAPTION_LIFETIME_MS = 8000;
8
+ const COMPACT_CAPTION_EXIT_MS = 220;
9
+ const AMBIENT_IDLE_GRACE_MS = 700;
10
+ const style = `
11
+ :host {
12
+ all: initial;
13
+ position: fixed;
14
+ inset: 0;
15
+ z-index: 2147483647;
16
+ pointer-events: none;
17
+ color-scheme: light;
18
+ font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
19
+ letter-spacing: 0;
20
+ --assist-text: #111827;
21
+ --assist-muted: #6b7280;
22
+ --assist-border: #e5e7eb;
23
+ --assist-surface: #ffffff;
24
+ --assist-surface-muted: #f9fafb;
25
+ }
26
+ *, *::before, *::after { box-sizing: border-box; letter-spacing: 0; }
27
+ button, input, summary { font: inherit; }
28
+ button { min-height: 40px; border-radius: 10px; cursor: pointer; }
29
+ [hidden] { display: none !important; }
30
+ button:focus-visible, input:focus-visible, summary:focus-visible, a:focus-visible {
31
+ outline: 3px solid color-mix(in srgb, var(--accent) 45%, white);
32
+ outline-offset: 2px;
33
+ }
34
+ .launcher {
35
+ position: fixed;
36
+ bottom: max(20px, env(safe-area-inset-bottom));
37
+ inset-inline-end: 20px;
38
+ display: inline-flex;
39
+ align-items: center;
40
+ gap: 8px;
41
+ min-height: 48px;
42
+ max-width: min(260px, calc(100vw - 40px));
43
+ padding: 0 16px;
44
+ color: white;
45
+ background: var(--accent);
46
+ border: 1px solid color-mix(in srgb, var(--accent) 70%, black);
47
+ border-radius: 12px;
48
+ box-shadow: 0 8px 24px rgba(15, 23, 42, .2);
49
+ pointer-events: auto;
50
+ font-size: 14px;
51
+ font-weight: 650;
52
+ z-index: 2;
53
+ }
54
+ .launcher::before {
55
+ content: '';
56
+ width: 9px;
57
+ height: 9px;
58
+ flex: 0 0 9px;
59
+ border: 2px solid white;
60
+ border-radius: 50%;
61
+ }
62
+ .launcher[data-active="true"]::after {
63
+ content: '';
64
+ position: absolute;
65
+ top: -4px;
66
+ inset-inline-end: -4px;
67
+ width: 13px;
68
+ height: 13px;
69
+ background: #dc2626;
70
+ border: 2px solid white;
71
+ border-radius: 50%;
72
+ animation: live-badge-pulse 1.35s ease-out infinite;
73
+ }
74
+ .panel {
75
+ position: fixed;
76
+ bottom: max(20px, env(safe-area-inset-bottom));
77
+ inset-inline-end: 20px;
78
+ display: none;
79
+ width: min(var(--panel-width), calc(100vw - 24px));
80
+ max-height: min(680px, calc(100vh - 40px));
81
+ color: var(--assist-text);
82
+ background: var(--assist-surface);
83
+ border: 1px solid var(--assist-border);
84
+ border-radius: 16px;
85
+ box-shadow: 0 3px 6px rgba(15, 23, 42, .08);
86
+ overflow: hidden;
87
+ pointer-events: auto;
88
+ z-index: 2;
89
+ }
90
+ .panel[data-anchor="left"] { inset-inline-start: 20px; inset-inline-end: auto; }
91
+ .panel.open { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; }
92
+ .panel.compact.open {
93
+ grid-template-rows: auto;
94
+ overflow: visible;
95
+ background: transparent;
96
+ border-color: transparent;
97
+ box-shadow: none;
98
+ }
99
+ .panel.compact .body, .panel.compact .controls { display: none !important; }
100
+ .panel.compact .header {
101
+ position: relative;
102
+ z-index: 2;
103
+ background: var(--assist-surface-muted);
104
+ border: 1px solid var(--assist-border);
105
+ border-radius: 12px;
106
+ box-shadow: 0 8px 24px rgba(15, 23, 42, .16);
107
+ }
108
+ .compact-captions {
109
+ position: absolute;
110
+ inset-inline: 0;
111
+ bottom: calc(100% + 8px);
112
+ display: none;
113
+ max-height: min(520px, calc(100dvh - 120px));
114
+ flex-direction: column;
115
+ align-items: flex-start;
116
+ justify-content: flex-end;
117
+ gap: 6px;
118
+ pointer-events: none;
119
+ }
120
+ .panel.compact.open .compact-captions { display: flex; }
121
+ .compact-caption {
122
+ --compact-caption-slot: 0;
123
+ position: relative;
124
+ display: -webkit-box;
125
+ box-sizing: border-box;
126
+ width: fit-content;
127
+ min-width: 0;
128
+ min-height: 30px;
129
+ max-height: 162px;
130
+ max-width: 92%;
131
+ padding: 6px 9px;
132
+ color: #1f2933;
133
+ background: rgba(255, 255, 255, .98);
134
+ border: 1px solid var(--assist-border);
135
+ border-radius: 10px;
136
+ box-shadow: 0 6px 18px rgba(15, 23, 42, .14);
137
+ font-size: 12px;
138
+ line-height: 15px;
139
+ overflow: hidden;
140
+ overflow-wrap: anywhere;
141
+ text-overflow: ellipsis;
142
+ white-space: normal;
143
+ word-break: break-word;
144
+ pointer-events: none;
145
+ cursor: default;
146
+ transition: opacity .18s ease;
147
+ -webkit-box-orient: vertical;
148
+ -webkit-line-clamp: 10;
149
+ }
150
+ .compact-caption.long { width: 92%; }
151
+ .compact-caption:focus-visible {
152
+ outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
153
+ outline-offset: 2px;
154
+ }
155
+ .compact-caption.assistant { align-self: flex-start; border-start-start-radius: 3px; }
156
+ .compact-caption.user { align-self: flex-end; color: white; background: #315064; border-color: #315064; }
157
+ .compact-caption.entering { animation: compact-caption-in .22s ease-out both; }
158
+ .compact-caption.pending {
159
+ display: flex;
160
+ width: 32px;
161
+ height: 26px;
162
+ min-height: 26px;
163
+ max-height: 26px;
164
+ align-items: center;
165
+ justify-content: center;
166
+ padding: 0 7px;
167
+ color: #64707d;
168
+ background: rgba(255, 255, 255, .96);
169
+ border-color: rgba(148, 163, 184, .28);
170
+ border-radius: 8px;
171
+ box-shadow: 0 3px 10px rgba(15, 23, 42, .09);
172
+ pointer-events: none;
173
+ cursor: default;
174
+ }
175
+ .compact-caption.user.pending {
176
+ color: rgba(255, 255, 255, .9);
177
+ background: #405a6a;
178
+ border-color: #405a6a;
179
+ }
180
+ .compact-caption.pending .typing-dots {
181
+ width: 14px;
182
+ height: 4px;
183
+ }
184
+ .compact-caption.pending .typing-dots span {
185
+ width: 3px;
186
+ height: 3px;
187
+ }
188
+ .compact-caption.leaving { opacity: 0; }
189
+ .header {
190
+ display: grid;
191
+ grid-template-columns: minmax(0, 1fr) auto;
192
+ align-items: center;
193
+ gap: 10px;
194
+ min-height: 60px;
195
+ padding: 10px 12px;
196
+ background: var(--assist-surface-muted);
197
+ border-bottom: 1px solid #f1f5f9;
198
+ }
199
+ .identity {
200
+ min-width: 0;
201
+ cursor: grab;
202
+ touch-action: none;
203
+ user-select: none;
204
+ }
205
+ .identity.dragging { cursor: grabbing; }
206
+ .identity strong, .identity span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
207
+ .identity strong { color: var(--assist-text); font-size: 14px; font-weight: 600; line-height: 19px; }
208
+ .identity span { margin-top: 2px; color: var(--assist-muted); font-size: 11px; line-height: 15px; }
209
+ .header-actions { display: flex; align-items: center; gap: 6px; }
210
+ .duration {
211
+ min-width: 52px;
212
+ padding: 4px 8px;
213
+ color: var(--assist-text);
214
+ background: var(--assist-surface);
215
+ border: 1px solid var(--assist-border);
216
+ border-radius: 999px;
217
+ font-size: 12px;
218
+ line-height: 18px;
219
+ text-align: center;
220
+ white-space: nowrap;
221
+ }
222
+ .end, .restart, .view-toggle, .close {
223
+ display: inline-grid;
224
+ width: 34px;
225
+ min-width: 34px;
226
+ min-height: 34px;
227
+ place-items: center;
228
+ padding: 0;
229
+ position: relative;
230
+ }
231
+ .end {
232
+ color: #c73838;
233
+ background: var(--assist-surface);
234
+ border: 1px solid var(--assist-border);
235
+ border-radius: 9px;
236
+ }
237
+ .end:hover { background: #fef2f2; border-color: #fecaca; }
238
+ .restart, .view-toggle, .close {
239
+ color: #4b5563;
240
+ background: var(--assist-surface);
241
+ border: 1px solid var(--assist-border);
242
+ }
243
+ .restart:hover, .view-toggle:hover, .close:hover { background: #f3f4f6; }
244
+ .button-icon, .menu-icon, .turn-icon {
245
+ display: inline-grid;
246
+ flex: 0 0 auto;
247
+ place-items: center;
248
+ line-height: 0;
249
+ }
250
+ .button-icon svg, .menu-icon svg, .turn-icon svg { display: block; }
251
+ .body { min-height: 0; padding: 12px; overflow: auto; }
252
+ .intro { display: grid; gap: 12px; }
253
+ .intro h2 { margin: 0; font-size: 17px; line-height: 23px; }
254
+ .boundary {
255
+ padding: 9px 10px;
256
+ color: #374151;
257
+ background: var(--assist-surface-muted);
258
+ border-inline-start: 3px solid #64748b;
259
+ border-radius: 0 8px 8px 0;
260
+ font-size: 11px;
261
+ line-height: 17px;
262
+ }
263
+ .consents { display: grid; gap: 8px; }
264
+ .consent {
265
+ display: grid;
266
+ grid-template-columns: 18px minmax(0, 1fr);
267
+ gap: 9px;
268
+ align-items: start;
269
+ color: #29333d;
270
+ font-size: 12px;
271
+ line-height: 17px;
272
+ }
273
+ .consent input { width: 16px; height: 16px; margin: 1px 0 0; accent-color: var(--accent); }
274
+ .legal-trigger {
275
+ justify-self: start;
276
+ min-height: 30px;
277
+ padding: 0;
278
+ color: #185ea8;
279
+ background: transparent;
280
+ border: 0;
281
+ border-radius: 4px;
282
+ font-size: 11px;
283
+ font-weight: 600;
284
+ }
285
+ .legal-overlay {
286
+ position: absolute;
287
+ inset: 0;
288
+ z-index: 4;
289
+ display: grid;
290
+ place-items: center;
291
+ padding: 14px;
292
+ background: rgba(15, 23, 42, .36);
293
+ }
294
+ .legal-dialog {
295
+ width: min(100%, 420px);
296
+ max-height: calc(100% - 12px);
297
+ overflow: auto;
298
+ padding: 14px;
299
+ color: var(--assist-text);
300
+ background: var(--assist-surface);
301
+ border: 1px solid var(--assist-border);
302
+ border-radius: 12px;
303
+ box-shadow: 0 12px 32px rgba(15, 23, 42, .2);
304
+ }
305
+ .legal-header { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px; }
306
+ .legal-header h3 { margin: 0; font-size: 14px; line-height: 20px; }
307
+ .legal-close { display: inline-grid; width: 32px; min-width: 32px; min-height: 32px; place-items: center; padding: 0; color: #4b5563; background: white; border: 1px solid var(--assist-border); }
308
+ .legal-dialog p { margin: 10px 0; color: #4b5563; font-size: 11px; line-height: 17px; }
309
+ .legal-dialog ul { display: grid; gap: 6px; margin: 10px 0; padding-inline-start: 18px; color: #374151; font-size: 11px; line-height: 16px; }
310
+ .legal-links { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 12px; }
311
+ .legal-links a { color: #185ea8; font-size: 11px; font-weight: 600; }
312
+ .mode-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
313
+ .primary, .secondary {
314
+ padding: 0 12px;
315
+ font-size: 12px;
316
+ font-weight: 600;
317
+ }
318
+ .primary { color: white; background: var(--accent); border: 1px solid color-mix(in srgb, var(--accent) 70%, black); }
319
+ .secondary { color: #27313b; background: white; border: 1px solid var(--assist-border); }
320
+ .primary:disabled, .secondary:disabled { cursor: not-allowed; opacity: .5; }
321
+ .conversation { display: none; min-height: 260px; }
322
+ .conversation.visible { display: block; }
323
+ .captions { display: flex; flex-direction: column; gap: 8px; min-height: 140px; max-height: 390px; overflow: auto; }
324
+ .line { max-width: 88%; padding: 8px 10px; border-radius: 12px; font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
325
+ .line.assistant { align-self: flex-start; color: #1f2933; background: #f1f5f9; border-start-start-radius: 3px; }
326
+ .line.user { align-self: flex-end; color: white; background: #315064; }
327
+ .line.pending {
328
+ display: inline-flex;
329
+ width: 38px;
330
+ min-height: 30px;
331
+ align-items: center;
332
+ justify-content: center;
333
+ padding: 6px 8px;
334
+ }
335
+ .line.settled { animation: message-settle .16s ease-out; }
336
+ .typing-dots {
337
+ display: inline-flex;
338
+ width: 18px;
339
+ height: 6px;
340
+ align-items: center;
341
+ justify-content: space-between;
342
+ }
343
+ .typing-dots span {
344
+ width: 4px;
345
+ height: 4px;
346
+ background: currentColor;
347
+ border-radius: 50%;
348
+ opacity: .32;
349
+ animation: typing-dot 1.05s ease-in-out infinite;
350
+ }
351
+ .typing-dots span:nth-child(2) { animation-delay: .14s; }
352
+ .typing-dots span:nth-child(3) { animation-delay: .28s; }
353
+ .empty { display: grid; min-height: 140px; place-items: center; color: #6a7480; font-size: 12px; text-align: center; }
354
+ .controls {
355
+ position: relative;
356
+ z-index: 4;
357
+ display: none;
358
+ padding: 10px 12px max(10px, env(safe-area-inset-bottom));
359
+ background: var(--assist-surface);
360
+ border-top: 1px solid #f1f5f9;
361
+ }
362
+ .controls.visible { display: block; }
363
+ .composer-shell { display: grid; gap: 8px; }
364
+ .composer { display: flex; align-items: center; gap: 7px; min-width: 0; }
365
+ .composer input {
366
+ flex: 1 1 auto;
367
+ min-width: 0;
368
+ min-height: 42px;
369
+ padding: 0 10px;
370
+ border: 1px solid #cbd5e1;
371
+ border-radius: 9px;
372
+ color: #17212b;
373
+ background: white;
374
+ }
375
+ .composer input:disabled { color: #94a3b8; background: #f8fafc; }
376
+ .send {
377
+ display: inline-grid;
378
+ flex: 0 0 42px;
379
+ width: 42px;
380
+ min-width: 42px;
381
+ min-height: 42px;
382
+ place-items: center;
383
+ padding: 0;
384
+ border-radius: 9px;
385
+ }
386
+ .more-controls { position: relative; flex: 0 0 42px; }
387
+ .more-controls summary {
388
+ display: grid;
389
+ width: 42px;
390
+ min-height: 42px;
391
+ place-items: center;
392
+ color: var(--assist-text);
393
+ background: var(--assist-surface-muted);
394
+ border: 1px solid var(--assist-border);
395
+ border-radius: 9px;
396
+ cursor: pointer;
397
+ list-style: none;
398
+ }
399
+ .more-controls summary::-webkit-details-marker { display: none; }
400
+ .more-controls summary:hover,
401
+ .more-controls[open] summary {
402
+ color: var(--accent);
403
+ background: color-mix(in srgb, var(--accent) 7%, white);
404
+ border-color: color-mix(in srgb, var(--accent) 32%, white);
405
+ }
406
+ .more-menu {
407
+ position: absolute;
408
+ inset-inline-start: 0;
409
+ bottom: calc(100% + 8px);
410
+ z-index: 5;
411
+ display: grid;
412
+ width: min(276px, calc(100vw - 40px));
413
+ max-height: min(336px, calc(100vh - 150px));
414
+ gap: 2px;
415
+ padding: 6px;
416
+ overflow-y: auto;
417
+ background: var(--assist-surface);
418
+ border: 1px solid var(--assist-border);
419
+ border-radius: 8px;
420
+ box-shadow: 0 12px 30px rgba(15, 23, 42, .18);
421
+ }
422
+ .menu-control {
423
+ display: flex;
424
+ width: 100%;
425
+ min-height: 40px;
426
+ align-items: center;
427
+ gap: 10px;
428
+ padding: 7px 9px;
429
+ color: #27313b;
430
+ background: transparent;
431
+ border: 0;
432
+ border-radius: 6px;
433
+ font-size: 12px;
434
+ font-weight: 550;
435
+ line-height: 16px;
436
+ text-align: start;
437
+ }
438
+ .menu-control:hover { background: #f3f5f6; }
439
+ .menu-control[aria-pressed="true"] {
440
+ color: color-mix(in srgb, var(--accent) 82%, #111827);
441
+ background: color-mix(in srgb, var(--accent) 7%, white);
442
+ }
443
+ .menu-icon {
444
+ width: 26px;
445
+ height: 26px;
446
+ color: #64707d;
447
+ background: #f3f5f6;
448
+ border-radius: 6px;
449
+ }
450
+ .menu-control[aria-pressed="true"] .menu-icon {
451
+ color: var(--accent);
452
+ background: color-mix(in srgb, var(--accent) 10%, white);
453
+ }
454
+ .control-label { min-width: 0; overflow-wrap: anywhere; }
455
+ .manual-turn-controls {
456
+ display: grid;
457
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
458
+ gap: 7px;
459
+ }
460
+ .turn-action {
461
+ display: inline-flex;
462
+ min-width: 0;
463
+ min-height: 44px;
464
+ align-items: center;
465
+ justify-content: center;
466
+ gap: 7px;
467
+ padding: 7px 9px;
468
+ border-radius: 8px;
469
+ font-size: 11px;
470
+ font-weight: 650;
471
+ line-height: 15px;
472
+ text-align: center;
473
+ }
474
+ .turn-action .control-label { overflow-wrap: normal; }
475
+ .turn-action[aria-pressed="true"] { box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 18%, transparent); }
476
+ .turn-action:disabled { cursor: not-allowed; opacity: .48; }
477
+ .tooltip {
478
+ position: fixed;
479
+ z-index: 6;
480
+ visibility: hidden;
481
+ max-width: min(220px, calc(100vw - 16px));
482
+ padding: 6px 8px;
483
+ color: #f8fafc;
484
+ background: #202832;
485
+ border: 1px solid rgba(255, 255, 255, .1);
486
+ border-radius: 6px;
487
+ box-shadow: 0 6px 18px rgba(15, 23, 42, .2);
488
+ opacity: 0;
489
+ pointer-events: none;
490
+ font-size: 11px;
491
+ font-weight: 500;
492
+ line-height: 15px;
493
+ text-align: center;
494
+ transform: translateY(2px);
495
+ transition: opacity .12s ease, transform .12s ease, visibility 0s linear .12s;
496
+ }
497
+ .tooltip.visible {
498
+ visibility: visible;
499
+ opacity: 1;
500
+ transform: translateY(0);
501
+ transition-delay: 0s;
502
+ }
503
+ .controls.error { display: none !important; }
504
+ audio { display: none; }
505
+ @keyframes compact-caption-in { from { opacity: 0; } to { opacity: 1; } }
506
+ @keyframes message-settle { from { opacity: .55; transform: translateY(2px); } to { opacity: 1; transform: translateY(0); } }
507
+ @keyframes attribution-in { from { opacity: 0; transform: translateY(3px); } }
508
+ @keyframes typing-dot {
509
+ 0%, 65%, 100% { opacity: .28; transform: translateY(0); }
510
+ 32% { opacity: .78; transform: translateY(-1px); }
511
+ }
512
+ @keyframes live-badge-pulse {
513
+ 0% { box-shadow: 0 0 0 0 rgba(220, 38, 38, .55); }
514
+ 70% { box-shadow: 0 0 0 7px rgba(220, 38, 38, 0); }
515
+ 100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0); }
516
+ }
517
+ .status-card { display: none; padding: 12px; color: #26313b; background: var(--assist-surface-muted); border: 1px solid var(--assist-border); border-radius: 12px; font-size: 12px; line-height: 18px; }
518
+ .status-card.visible { display: block; }
519
+ .confirmation { display: none; padding: 12px; border: 2px solid var(--accent); border-radius: 12px; background: white; }
520
+ .confirmation.visible { display: block; }
521
+ .confirmation h3 { margin: 0 0 6px; font-size: 14px; line-height: 20px; }
522
+ .confirmation p { margin: 4px 0; color: #4b5563; font-size: 12px; line-height: 17px; }
523
+ .confirmation strong { color: #17212b; }
524
+ .confirmation-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px; }
525
+ .confirmation [data-role="approve"], .visual-request [data-role="visual-share"] { background: #16a34a; border-color: #16a34a; }
526
+ .confirmation [data-role="decline"], .visual-request [data-role="visual-decline"] { color: #b91c1c; background: #fef2f2; border-color: #fecaca; }
527
+ .visual-request { display: none; padding: 12px; border: 2px solid #55758c; border-radius: 12px; background: white; }
528
+ .visual-request.visible { display: block; }
529
+ .visual-request h3 { margin: 0 0 6px; font-size: 14px; line-height: 20px; }
530
+ .visual-request p { margin: 4px 0; color: #4b5563; font-size: 12px; line-height: 17px; }
531
+ .feedback { display: none; }
532
+ .feedback.visible { display: grid; gap: 12px; margin-top: 10px; padding: 8px 0 4px; }
533
+ .attribution-bubble {
534
+ width: max-content;
535
+ max-width: 100%;
536
+ padding: 8px 10px;
537
+ color: var(--assist-muted);
538
+ background: var(--assist-surface-muted);
539
+ border: 1px solid var(--assist-border);
540
+ border-radius: 8px;
541
+ box-shadow: 0 4px 14px rgba(15, 23, 42, .08);
542
+ font-size: 12px;
543
+ line-height: 18px;
544
+ animation: attribution-in .18s ease-out both;
545
+ }
546
+ .attribution-bubble a {
547
+ color: var(--assist-text);
548
+ font-weight: 650;
549
+ text-decoration: underline;
550
+ text-underline-offset: 2px;
551
+ }
552
+ .feedback-options { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 7px; }
553
+ .ambient {
554
+ position: fixed;
555
+ inset: 0;
556
+ z-index: 1;
557
+ display: none;
558
+ overflow: hidden;
559
+ pointer-events: none;
560
+ background:
561
+ linear-gradient(to left, rgba(34, 211, 238, .048), rgba(139, 92, 246, .02) 6%, transparent 16%),
562
+ linear-gradient(to top, rgba(236, 72, 153, .046), rgba(34, 211, 238, .017) 7%, transparent 18%);
563
+ }
564
+ .ambient.active { display: block; }
565
+ .ambient::before,
566
+ .ambient::after {
567
+ content: '';
568
+ position: absolute;
569
+ inset-inline-end: 0;
570
+ bottom: 0;
571
+ pointer-events: none;
572
+ }
573
+ .ambient::before {
574
+ width: min(30vw, 420px);
575
+ height: 100%;
576
+ background: linear-gradient(to bottom, transparent 0%, transparent 46%, rgba(34, 211, 238, .018) 66%, rgba(139, 92, 246, .064) 84%, rgba(236, 72, 153, .13) 100%);
577
+ -webkit-mask-image: linear-gradient(to right, transparent 0%, rgba(0, 0, 0, .16) 46%, black 100%);
578
+ mask-image: linear-gradient(to right, transparent 0%, rgba(0, 0, 0, .16) 46%, black 100%);
579
+ opacity: .42;
580
+ }
581
+ .ambient::after {
582
+ width: 100%;
583
+ height: min(18vh, 160px);
584
+ background: linear-gradient(to right, transparent 0%, transparent 34%, rgba(34, 211, 238, .026) 62%, rgba(139, 92, 246, .065) 82%, rgba(236, 72, 153, .12) 100%);
585
+ -webkit-mask-image: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, .18) 38%, black 100%);
586
+ mask-image: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, .18) 38%, black 100%);
587
+ opacity: .46;
588
+ }
589
+ .ambient.active::before { animation: ambient-edge-breathe 6.4s ease-in-out infinite; }
590
+ .ambient.active::after { animation: ambient-edge-shift 8.6s ease-in-out infinite alternate; }
591
+ .highlight {
592
+ position: fixed;
593
+ display: none;
594
+ pointer-events: none;
595
+ border: 3px solid var(--accent);
596
+ border-radius: 5px;
597
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 18%, transparent);
598
+ transition: inset .12s ease, width .12s ease, height .12s ease;
599
+ z-index: 3;
600
+ }
601
+ .highlight.visible { display: block; }
602
+ .highlight.success { border-color: #0f8a80; }
603
+ .highlight.failure { border-color: #c73838; }
604
+ .sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
605
+ @keyframes ambient-edge-breathe {
606
+ 0%, 100% { opacity: .34; }
607
+ 50% { opacity: .48; }
608
+ }
609
+ @keyframes ambient-edge-shift {
610
+ from { opacity: .38; }
611
+ to { opacity: .5; }
612
+ }
613
+ @media (prefers-reduced-motion: reduce) {
614
+ *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; transition: none !important; }
615
+ }
616
+ @media (prefers-reduced-transparency: reduce) {
617
+ .panel { background: #fff; }
618
+ .ambient {
619
+ background:
620
+ linear-gradient(to left, color-mix(in srgb, var(--accent) 3%, transparent), transparent 16%),
621
+ linear-gradient(to top, color-mix(in srgb, var(--accent) 3%, transparent), transparent 16%);
622
+ }
623
+ .ambient::before, .ambient::after { display: none; }
624
+ }
625
+ @media (forced-colors: active) {
626
+ .ambient { display: none !important; }
627
+ .launcher, .primary { border: 2px solid ButtonText; }
628
+ .highlight { border: 3px solid Highlight; box-shadow: none; }
629
+ }
630
+ @media (max-width: 520px) {
631
+ .launcher { inset-inline: 12px; width: calc(100vw - 24px); justify-content: center; }
632
+ .panel {
633
+ inset: auto 8px max(8px, env(safe-area-inset-bottom)) 8px !important;
634
+ width: auto;
635
+ max-height: calc(100vh - 16px - env(safe-area-inset-bottom));
636
+ }
637
+ .identity { cursor: default; }
638
+ .header-actions { gap: 4px; }
639
+ .duration { min-width: 48px; padding-inline: 6px; }
640
+ .feedback-options { grid-template-columns: 1fr; }
641
+ .mode-actions { grid-template-columns: 1fr; }
642
+ }
643
+ `;
644
+ export class WidgetView {
645
+ constructor(config, overrides, callbacks) {
646
+ this.callbacks = callbacks;
647
+ this.ambient = null;
648
+ this.muted = false;
649
+ this.manualTurn = false;
650
+ this.pushActive = false;
651
+ this.voiceActive = false;
652
+ this.automaticTurnsAvailable = false;
653
+ this.paused = false;
654
+ this.captionsEnabled = true;
655
+ this.visualPaused = false;
656
+ this.active = false;
657
+ this.compact = false;
658
+ this.panelViewInitialized = false;
659
+ this.launchUnavailable = false;
660
+ this.activeElsewhere = false;
661
+ this.currentConfirmation = null;
662
+ this.currentVisualRequest = null;
663
+ this.highlightedElement = null;
664
+ this.dragState = null;
665
+ this.tooltipTarget = null;
666
+ this.tooltipTimer = null;
667
+ this.ambientHideTimer = null;
668
+ this.attributionTimer = null;
669
+ this.sessionStartedAt = null;
670
+ this.durationInterval = null;
671
+ this.compactCaptionTimers = new Map();
672
+ this.assistantState = "ready";
673
+ this.onTooltipMouseOver = (rawEvent) => {
674
+ const event = rawEvent;
675
+ const target = this.tooltipTrigger(event);
676
+ if (!target)
677
+ return;
678
+ if (event.relatedTarget instanceof Node &&
679
+ target.contains(event.relatedTarget))
680
+ return;
681
+ this.showTooltip(target);
682
+ };
683
+ this.onTooltipMouseOut = (rawEvent) => {
684
+ const event = rawEvent;
685
+ const target = this.tooltipTrigger(event);
686
+ if (!target)
687
+ return;
688
+ if (event.relatedTarget instanceof Node &&
689
+ target.contains(event.relatedTarget))
690
+ return;
691
+ if (this.tooltipTarget === target || this.tooltipTimer !== null)
692
+ this.hideTooltip();
693
+ };
694
+ this.onTooltipFocusIn = (rawEvent) => {
695
+ const event = rawEvent;
696
+ const target = this.tooltipTrigger(event);
697
+ if (target)
698
+ this.showTooltip(target, true);
699
+ };
700
+ this.onTooltipFocusOut = (rawEvent) => {
701
+ const event = rawEvent;
702
+ const target = this.tooltipTrigger(event);
703
+ if (!target)
704
+ return;
705
+ if (event.relatedTarget instanceof Node &&
706
+ target.contains(event.relatedTarget))
707
+ return;
708
+ this.hideTooltip();
709
+ };
710
+ this.onDocumentPointerDown = (event) => {
711
+ const details = this.require(".more-controls");
712
+ if (!details.open || event.composedPath().includes(details))
713
+ return;
714
+ this.closeControlsMenu();
715
+ };
716
+ this.refreshHighlight = () => {
717
+ this.hideTooltip();
718
+ if (this.highlightedElement) {
719
+ const status = this.highlight.classList.contains("success")
720
+ ? "success"
721
+ : this.highlight.classList.contains("failure")
722
+ ? "failure"
723
+ : "focus";
724
+ this.updateHighlight(this.highlightedElement, status);
725
+ }
726
+ };
727
+ this.onResize = () => {
728
+ this.hideTooltip();
729
+ this.clampPanelPosition();
730
+ this.refreshHighlight();
731
+ };
732
+ this.onDragStart = (event) => {
733
+ if (window.innerWidth <= 520 || event.button !== 0)
734
+ return;
735
+ const handle = event.currentTarget;
736
+ const rect = this.panel.getBoundingClientRect();
737
+ this.dragState = {
738
+ pointerId: event.pointerId,
739
+ startX: event.clientX,
740
+ startY: event.clientY,
741
+ left: rect.left,
742
+ top: rect.top,
743
+ };
744
+ handle.classList.add("dragging");
745
+ handle.setPointerCapture(event.pointerId);
746
+ event.preventDefault();
747
+ };
748
+ this.onDragMove = (event) => {
749
+ const drag = this.dragState;
750
+ if (!drag || drag.pointerId !== event.pointerId)
751
+ return;
752
+ const rect = this.panel.getBoundingClientRect();
753
+ const maxLeft = Math.max(8, window.innerWidth - rect.width - 8);
754
+ const maxTop = Math.max(8, window.innerHeight - rect.height - 8);
755
+ const left = Math.min(maxLeft, Math.max(8, drag.left + event.clientX - drag.startX));
756
+ const top = Math.min(maxTop, Math.max(8, drag.top + event.clientY - drag.startY));
757
+ this.setPanelPosition(left, top);
758
+ };
759
+ this.onDragEnd = (event) => {
760
+ const drag = this.dragState;
761
+ if (!drag || drag.pointerId !== event.pointerId)
762
+ return;
763
+ const handle = event.currentTarget;
764
+ if (handle.hasPointerCapture(event.pointerId)) {
765
+ handle.releasePointerCapture(event.pointerId);
766
+ }
767
+ handle.classList.remove("dragging");
768
+ this.dragState = null;
769
+ const rect = this.panel.getBoundingClientRect();
770
+ this.savePanelPosition(rect.left, rect.top);
771
+ };
772
+ this.config = config;
773
+ this.messages = { ...defaultMessages, ...overrides };
774
+ this.host = document.createElement("div");
775
+ this.host.dataset.oxvoAiLiveAssistRoot = "true";
776
+ this.host.setAttribute("aria-live", "off");
777
+ this.shadow = this.host.attachShadow({ mode: "closed" });
778
+ const sheet = document.createElement("style");
779
+ sheet.textContent = style;
780
+ this.shadow.append(sheet);
781
+ this.shadow.append(this.build());
782
+ document.documentElement.append(this.host);
783
+ this.launcher = this.require(".launcher");
784
+ this.panel = this.require(".panel");
785
+ this.body = this.require(".body");
786
+ this.status = this.require('[data-role="status"]');
787
+ this.duration = this.require('[data-role="duration"]');
788
+ this.intro = this.require(".intro");
789
+ this.conversation = this.require(".conversation");
790
+ this.captions = this.require(".captions");
791
+ this.compactCaptions = this.require(".compact-captions");
792
+ this.controls = this.require(".controls");
793
+ this.confirmation = this.require(".confirmation");
794
+ this.visualRequest = this.require(".visual-request");
795
+ this.feedback = this.require(".feedback");
796
+ this.attribution = this.require('[data-role="attribution"]');
797
+ this.highlight = this.require(".highlight");
798
+ this.tooltip = this.require(".tooltip");
799
+ this.audio = this.require("audio");
800
+ this.bindPlaybackState();
801
+ this.bind();
802
+ this.applyConfig();
803
+ }
804
+ setRemoteStream(stream) {
805
+ this.audio.srcObject = stream;
806
+ void this.audio.play().catch(() => {
807
+ this.setStatus(this.messages.audioPaused);
808
+ this.emitPlaybackState("blocked");
809
+ });
810
+ }
811
+ bindPlaybackState() {
812
+ this.audio.addEventListener("playing", () => this.emitPlaybackState("playing"));
813
+ this.audio.addEventListener("waiting", () => this.emitPlaybackState("waiting"));
814
+ this.audio.addEventListener("pause", () => this.emitPlaybackState("paused"));
815
+ this.audio.addEventListener("ended", () => this.emitPlaybackState("ended"));
816
+ }
817
+ emitPlaybackState(state) {
818
+ this.callbacks.onPlaybackState({
819
+ state,
820
+ audible: state === "playing" && !this.audio.muted && this.audio.volume > 0,
821
+ utteranceId: null,
822
+ sampledAt: new Date().toISOString(),
823
+ });
824
+ }
825
+ setStatus(value) {
826
+ this.status.textContent = value;
827
+ this.status.setAttribute("aria-label", value);
828
+ if (this.active)
829
+ this.syncLauncherState();
830
+ }
831
+ message(key) {
832
+ return this.messages[key];
833
+ }
834
+ setAssistantState(state) {
835
+ this.assistantState = state;
836
+ this.setStatus(this.messages[state]);
837
+ if (state === "listening") {
838
+ this.clearPendingCaptions("assistant");
839
+ this.ensurePendingCaption("user", this.messages.listening);
840
+ }
841
+ else if (state === "thinking" || state === "speaking") {
842
+ this.suspendPendingCaption("user");
843
+ this.ensurePendingCaption("assistant", this.messages[state]);
844
+ }
845
+ else {
846
+ this.clearPendingCaptions();
847
+ }
848
+ if (state === "speaking" && this.audio.paused) {
849
+ void this.audio
850
+ .play()
851
+ .catch(() => this.setStatus(this.messages.audioPaused));
852
+ }
853
+ this.setAmbientActive(state !== "ready");
854
+ this.syncLauncherState();
855
+ }
856
+ showConnecting(resume = false, mode = "text") {
857
+ this.active = true;
858
+ this.hideAttribution();
859
+ this.assistantState = "connecting";
860
+ this.voiceActive = mode === "voice";
861
+ this.panel.classList.toggle("voice-session", this.voiceActive);
862
+ this.clearPendingCaptions();
863
+ if (!resume) {
864
+ this.clearPanelSessionState();
865
+ this.resetPanelPosition();
866
+ }
867
+ this.initializePanelView();
868
+ if (resume && this.readPanelVisibility() === "hidden") {
869
+ this.panel.classList.remove("open");
870
+ this.launcher.hidden = false;
871
+ }
872
+ else {
873
+ this.openPanel();
874
+ }
875
+ this.controls.classList.remove("error");
876
+ this.require('[data-role="notice"]').classList.remove("visible");
877
+ this.require('[data-role="retry"]').hidden = true;
878
+ this.intro.hidden = true;
879
+ this.conversation.classList.add("visible");
880
+ this.controls.classList.add("visible");
881
+ this.setComposerEnabled(false);
882
+ this.setStatus(resume ? this.messages.reconnecting : this.messages.connecting);
883
+ this.syncHeaderActions();
884
+ this.startDuration(resume);
885
+ this.setAmbientActive(true);
886
+ }
887
+ showActive(welcomeMessage) {
888
+ this.active = true;
889
+ this.hideAttribution();
890
+ this.assistantState = "ready";
891
+ this.initializePanelView();
892
+ this.controls.classList.remove("error");
893
+ this.require('[data-role="notice"]').classList.remove("visible");
894
+ this.require('[data-role="retry"]').hidden = true;
895
+ this.syncHeaderActions();
896
+ this.feedback.classList.remove("visible");
897
+ this.intro.hidden = true;
898
+ this.conversation.classList.add("visible");
899
+ this.controls.classList.add("visible");
900
+ this.setComposerEnabled(true);
901
+ this.setStatus(this.messages.ready);
902
+ this.clearPendingCaptions();
903
+ this.setAmbientActive(false);
904
+ if (this.sessionStartedAt === null)
905
+ this.startDuration(false);
906
+ if (welcomeMessage)
907
+ this.addCaption("assistant", welcomeMessage, true);
908
+ if (!this.panel.classList.contains("open"))
909
+ return;
910
+ if (this.compact) {
911
+ this.require('[data-role="view-toggle"]').focus();
912
+ }
913
+ else if (this.voiceActive) {
914
+ this.require('[data-role="mute"]').focus();
915
+ }
916
+ else {
917
+ this.require('[data-role="text-input"]').focus();
918
+ }
919
+ }
920
+ addCaption(role, text, final) {
921
+ if (!this.captionsEnabled)
922
+ return;
923
+ if (!final && role === "user" && this.assistantState !== "listening") {
924
+ return;
925
+ }
926
+ this.captions.querySelector('[data-role="empty"]')?.remove();
927
+ const safe = text.slice(0, 16000);
928
+ if (!final) {
929
+ this.ensurePendingCaption(role, role === "user" ? this.messages.listening : this.messages.thinking);
930
+ return;
931
+ }
932
+ if (!safe)
933
+ return;
934
+ let line = this.captions.querySelector(`[data-caption="${role}-partial"]`);
935
+ if (!line) {
936
+ line = document.createElement("div");
937
+ this.captions.append(line);
938
+ }
939
+ line.className = `line ${role} settled`;
940
+ line.hidden = false;
941
+ line.removeAttribute("aria-label");
942
+ line.textContent = safe;
943
+ line.dataset.caption = `${role}-${crypto.randomUUID()}`;
944
+ while (this.captions.children.length > 20)
945
+ this.captions.firstElementChild?.remove();
946
+ this.captions.scrollTop = this.captions.scrollHeight;
947
+ this.addCompactCaption(role, safe, true);
948
+ }
949
+ ensurePendingCaption(role, label) {
950
+ if (!this.captionsEnabled)
951
+ return;
952
+ this.captions.querySelector('[data-role="empty"]')?.remove();
953
+ let line = this.captions.querySelector(`[data-caption="${role}-partial"]`);
954
+ if (!line) {
955
+ line = document.createElement("div");
956
+ line.dataset.caption = `${role}-partial`;
957
+ this.captions.append(line);
958
+ }
959
+ line.className = `line ${role} pending`;
960
+ line.hidden = false;
961
+ this.renderPendingDots(line, label);
962
+ this.captions.scrollTop = this.captions.scrollHeight;
963
+ this.addCompactCaption(role, "", false, label);
964
+ }
965
+ renderPendingDots(line, label) {
966
+ line.setAttribute("aria-label", label);
967
+ if (line.querySelector(".typing-dots"))
968
+ return;
969
+ const dots = document.createElement("span");
970
+ dots.className = "typing-dots";
971
+ dots.setAttribute("aria-hidden", "true");
972
+ dots.append(document.createElement("span"), document.createElement("span"), document.createElement("span"));
973
+ line.replaceChildren(dots);
974
+ }
975
+ clearPendingCaptions(role) {
976
+ const suffix = role ? `${role}-partial` : "-partial";
977
+ this.captions
978
+ .querySelectorAll(role ? `[data-caption="${suffix}"]` : '[data-caption$="-partial"]')
979
+ .forEach((line) => line.remove());
980
+ this.compactCaptions
981
+ .querySelectorAll(role
982
+ ? `[data-compact-caption="${suffix}"]`
983
+ : '[data-compact-caption$="-partial"]')
984
+ .forEach((line) => this.removeCompactCaption(line));
985
+ }
986
+ suspendPendingCaption(role) {
987
+ const line = this.captions.querySelector(`[data-caption="${role}-partial"]`);
988
+ if (line)
989
+ line.hidden = true;
990
+ this.compactCaptions
991
+ .querySelectorAll(`[data-compact-caption="${role}-partial"]`)
992
+ .forEach((pending) => this.removeCompactCaption(pending));
993
+ }
994
+ addCompactCaption(role, text, final, pendingLabel = role === "user"
995
+ ? this.messages.listening
996
+ : this.messages.thinking) {
997
+ this.discardExitingCompactCaptions();
998
+ const partialKey = `${role}-partial`;
999
+ let line = this.compactCaptions.querySelector(`[data-compact-caption="${partialKey}"]`);
1000
+ const isNew = !line;
1001
+ if (!line) {
1002
+ line = document.createElement("div");
1003
+ line.dataset.compactCaption = partialKey;
1004
+ const activeAssistant = final && role === "user"
1005
+ ? this.compactCaptions.querySelector('[data-compact-caption="assistant-partial"]')
1006
+ : null;
1007
+ if (activeAssistant) {
1008
+ this.compactCaptions.insertBefore(line, activeAssistant);
1009
+ }
1010
+ else {
1011
+ this.compactCaptions.append(line);
1012
+ }
1013
+ }
1014
+ if (final) {
1015
+ line.className = `compact-caption ${role}`;
1016
+ line.classList.toggle("long", text.length > 48);
1017
+ line.removeAttribute("aria-label");
1018
+ line.removeAttribute("data-tooltip");
1019
+ line.removeAttribute("tabindex");
1020
+ line.textContent = text;
1021
+ line.dataset.compactCaption = `${role}-${crypto.randomUUID()}`;
1022
+ }
1023
+ else {
1024
+ line.className = `compact-caption ${role} pending`;
1025
+ line.removeAttribute("data-tooltip");
1026
+ line.removeAttribute("tabindex");
1027
+ this.renderPendingDots(line, pendingLabel);
1028
+ }
1029
+ if (isNew) {
1030
+ line.classList.add("entering");
1031
+ line.addEventListener("animationend", () => line.classList.remove("entering"), {
1032
+ once: true,
1033
+ });
1034
+ }
1035
+ this.scheduleCompactCaptionRemoval(line);
1036
+ while (this.compactCaptions.children.length > 3) {
1037
+ const oldest = this.compactCaptions
1038
+ .firstElementChild;
1039
+ if (!oldest)
1040
+ break;
1041
+ this.removeCompactCaption(oldest);
1042
+ }
1043
+ this.layoutCompactCaptions();
1044
+ }
1045
+ scheduleCompactCaptionRemoval(line) {
1046
+ const existing = this.compactCaptionTimers.get(line);
1047
+ if (existing)
1048
+ clearTimeout(existing);
1049
+ const lifetimeTimer = setTimeout(() => {
1050
+ line.classList.add("leaving");
1051
+ const exitTimer = setTimeout(() => {
1052
+ this.compactCaptionTimers.delete(line);
1053
+ line.remove();
1054
+ this.layoutCompactCaptions();
1055
+ }, COMPACT_CAPTION_EXIT_MS);
1056
+ this.compactCaptionTimers.set(line, exitTimer);
1057
+ }, COMPACT_CAPTION_LIFETIME_MS);
1058
+ this.compactCaptionTimers.set(line, lifetimeTimer);
1059
+ }
1060
+ discardExitingCompactCaptions() {
1061
+ this.compactCaptions
1062
+ .querySelectorAll(".compact-caption.leaving")
1063
+ .forEach((line) => {
1064
+ const timer = this.compactCaptionTimers.get(line);
1065
+ if (timer)
1066
+ clearTimeout(timer);
1067
+ this.compactCaptionTimers.delete(line);
1068
+ line.remove();
1069
+ });
1070
+ }
1071
+ removeCompactCaption(line) {
1072
+ const timer = this.compactCaptionTimers.get(line);
1073
+ if (timer)
1074
+ clearTimeout(timer);
1075
+ this.compactCaptionTimers.delete(line);
1076
+ line.remove();
1077
+ this.layoutCompactCaptions();
1078
+ }
1079
+ layoutCompactCaptions() {
1080
+ const lines = [...this.compactCaptions.children]
1081
+ .filter((element) => element instanceof HTMLElement)
1082
+ .filter((line) => !line.classList.contains("leaving"));
1083
+ lines.reverse().forEach((line, slot) => {
1084
+ line.style.setProperty("--compact-caption-slot", String(slot));
1085
+ });
1086
+ }
1087
+ clearCompactCaptions() {
1088
+ for (const timer of this.compactCaptionTimers.values())
1089
+ clearTimeout(timer);
1090
+ this.compactCaptionTimers.clear();
1091
+ this.compactCaptions.replaceChildren();
1092
+ }
1093
+ showConfirmation(value) {
1094
+ this.currentConfirmation = value;
1095
+ this.setCompact(false, false);
1096
+ this.openPanel();
1097
+ this.confirmation.classList.add("visible");
1098
+ this.confirmation.querySelector('[data-role="action"]').textContent =
1099
+ value.action;
1100
+ this.confirmation.querySelector('[data-role="target"]').textContent =
1101
+ value.targetLabel;
1102
+ this.confirmation.querySelector('[data-role="reason"]').textContent =
1103
+ value.reason;
1104
+ this.confirmation.querySelector('[data-role="effect"]').textContent =
1105
+ value.expectedEffect;
1106
+ this.setStatus(this.messages.actionRequest);
1107
+ this.setAmbientActive(true);
1108
+ this.syncHeaderActions();
1109
+ this.confirmation.scrollIntoView({ block: "nearest" });
1110
+ this.require('[data-role="approve"]').focus();
1111
+ }
1112
+ showActionState(status) {
1113
+ this.confirmation.classList.remove("visible");
1114
+ this.currentConfirmation = null;
1115
+ const key = status === "running"
1116
+ ? "actionRunning"
1117
+ : status === "verified"
1118
+ ? "actionVerified"
1119
+ : "actionFailed";
1120
+ this.setStatus(this.messages[key]);
1121
+ if (status === "running") {
1122
+ this.setAmbientActive(true);
1123
+ }
1124
+ else {
1125
+ this.setAmbientActive(false);
1126
+ }
1127
+ this.syncHeaderActions();
1128
+ }
1129
+ dismissActionState() {
1130
+ this.confirmation.classList.remove("visible");
1131
+ this.currentConfirmation = null;
1132
+ this.setStatus(this.messages.ready);
1133
+ this.setAmbientActive(false);
1134
+ this.syncHeaderActions();
1135
+ }
1136
+ showVisualRequest(value) {
1137
+ this.currentVisualRequest = value;
1138
+ this.setCompact(false, false);
1139
+ this.openPanel();
1140
+ this.visualRequest.classList.add("visible");
1141
+ this.require('[data-role="visual-reason"]').textContent = value.reason;
1142
+ this.setStatus(this.messages.visualRequest);
1143
+ this.syncHeaderActions();
1144
+ this.require('[data-role="visual-share"]').focus();
1145
+ }
1146
+ showVisualResult(shared) {
1147
+ this.visualRequest.classList.remove("visible");
1148
+ this.currentVisualRequest = null;
1149
+ this.setStatus(shared ? this.messages.visualShared : this.messages.visualUnavailable);
1150
+ this.syncHeaderActions();
1151
+ }
1152
+ setVisualAvailable(available, paused = false) {
1153
+ const button = this.require('[data-role="visual-pause"]');
1154
+ button.hidden = !available;
1155
+ this.setVisualPaused(paused);
1156
+ }
1157
+ setVisualPaused(paused) {
1158
+ this.visualPaused = paused;
1159
+ const button = this.require('[data-role="visual-pause"]');
1160
+ this.setControlLabel("visual-pause", paused ? this.messages.resumeVisual : this.messages.pauseVisual);
1161
+ this.setControlIcon("visual-pause", paused ? Eye : EyeOff);
1162
+ button.setAttribute("aria-pressed", String(paused));
1163
+ if (paused && this.currentVisualRequest) {
1164
+ const request = this.currentVisualRequest;
1165
+ this.visualRequest.classList.remove("visible");
1166
+ this.currentVisualRequest = null;
1167
+ this.callbacks.onVisualContext(request, false);
1168
+ this.syncHeaderActions();
1169
+ }
1170
+ }
1171
+ setPaused(value) {
1172
+ this.paused = value;
1173
+ this.assistantState = value ? "paused" : "ready";
1174
+ this.setControlLabel("pause", value ? this.messages.resumeActions : this.messages.pauseActions);
1175
+ this.setControlIcon("pause", value ? Play : Pause);
1176
+ this.require('[data-role="pause"]').setAttribute("aria-pressed", String(value));
1177
+ this.setStatus(value ? this.messages.paused : this.messages.ready);
1178
+ if (value)
1179
+ this.setAmbientActive(false, true);
1180
+ }
1181
+ setMuted(value) {
1182
+ this.muted = value;
1183
+ this.setControlLabel("mute", value ? this.messages.unmute : this.messages.mute);
1184
+ this.setControlIcon("mute", value ? MicOff : Mic);
1185
+ this.require('[data-role="mute"]').setAttribute("aria-pressed", String(value));
1186
+ this.require('[data-role="push"]').disabled = value;
1187
+ }
1188
+ configureVoiceControls(voiceActive, manualTurn, automaticTurnsAvailable) {
1189
+ const wasVoiceActive = this.voiceActive;
1190
+ this.voiceActive = voiceActive;
1191
+ this.panel.classList.toggle("voice-session", voiceActive);
1192
+ this.automaticTurnsAvailable = automaticTurnsAvailable;
1193
+ for (const role of ["mute", "interrupt", "turn-mode", "push"]) {
1194
+ this.require(`[data-role="${role}"]`).hidden =
1195
+ !voiceActive;
1196
+ }
1197
+ this.require('[data-role="text-only"]').hidden =
1198
+ !voiceActive;
1199
+ this.setManualTurn(manualTurn);
1200
+ if (!this.active)
1201
+ return;
1202
+ if (wasVoiceActive && !voiceActive) {
1203
+ this.setCompact(false);
1204
+ return;
1205
+ }
1206
+ this.initializePanelView();
1207
+ }
1208
+ setManualTurn(value) {
1209
+ this.manualTurn = value;
1210
+ const mode = this.require('[data-role="turn-mode"]');
1211
+ mode.hidden = !this.voiceActive || value;
1212
+ this.setControlLabel("turn-mode", value ? this.messages.useAutomaticTurns : this.messages.usePushToTalk);
1213
+ this.setControlIcon("turn-mode", value ? AudioLines : Hand);
1214
+ mode.setAttribute("aria-pressed", String(value));
1215
+ const push = this.require('[data-role="push"]');
1216
+ push.hidden = !this.voiceActive || !value;
1217
+ const manualControls = this.require('[data-role="manual-turn-controls"]');
1218
+ manualControls.hidden = !this.voiceActive || !value;
1219
+ const automatic = this.require('[data-role="automatic-turns"]');
1220
+ automatic.disabled = !this.automaticTurnsAvailable;
1221
+ this.setControlLabel("automatic-turns", this.automaticTurnsAvailable
1222
+ ? this.messages.useAutomaticTurns
1223
+ : this.messages.automaticTurnsUnavailable);
1224
+ this.setControlIcon("automatic-turns", AudioLines);
1225
+ if (!value)
1226
+ this.setPushActive(false);
1227
+ }
1228
+ setPushActive(value) {
1229
+ this.pushActive = value;
1230
+ const button = this.require('[data-role="push"]');
1231
+ this.setControlLabel("push", value ? this.messages.sendVoiceTurn : this.messages.startSpeaking);
1232
+ this.setControlIcon("push", value ? Send : Mic);
1233
+ button.setAttribute("aria-pressed", String(value));
1234
+ if (value) {
1235
+ this.assistantState = "listening";
1236
+ this.setStatus(this.messages.listening);
1237
+ }
1238
+ }
1239
+ interruptPlayback() {
1240
+ this.audio.pause();
1241
+ this.setPushActive(false);
1242
+ this.assistantState = "ready";
1243
+ this.clearPendingCaptions("assistant");
1244
+ this.setStatus(this.messages.ready);
1245
+ this.setAmbientActive(false);
1246
+ }
1247
+ showError(message, retryable = false, sessionActive = this.active) {
1248
+ this.active = sessionActive;
1249
+ this.clearPendingCaptions();
1250
+ this.setCompact(false, false);
1251
+ this.openPanel();
1252
+ this.intro.hidden = true;
1253
+ this.conversation.classList.remove("visible");
1254
+ this.controls.classList.remove("visible");
1255
+ this.controls.classList.toggle("error", this.active);
1256
+ this.setComposerEnabled(false);
1257
+ const card = this.require('[data-role="notice"]');
1258
+ card.classList.add("visible");
1259
+ card.textContent = message || this.messages.error;
1260
+ const retry = this.require('[data-role="retry"]');
1261
+ retry.hidden = !retryable;
1262
+ this.setStatus(this.messages.error);
1263
+ this.assistantState = "error";
1264
+ this.stopDuration(!sessionActive);
1265
+ if (!sessionActive) {
1266
+ this.duration.hidden = true;
1267
+ this.duration.textContent = "00:00";
1268
+ }
1269
+ this.syncHeaderActions();
1270
+ this.setAmbientActive(false, true);
1271
+ }
1272
+ showEnded(message) {
1273
+ this.active = false;
1274
+ this.assistantState = "ended";
1275
+ this.paused = false;
1276
+ this.currentConfirmation = null;
1277
+ this.currentVisualRequest = null;
1278
+ this.clearPendingCaptions();
1279
+ this.setCompact(false, false);
1280
+ this.clearCompactCaptions();
1281
+ this.clearPanelSessionState();
1282
+ this.conversation.classList.remove("visible");
1283
+ this.controls.classList.remove("visible");
1284
+ this.controls.classList.remove("error");
1285
+ this.confirmation.classList.remove("visible");
1286
+ this.visualRequest.classList.remove("visible");
1287
+ this.setComposerEnabled(false);
1288
+ this.require('[data-role="notice"]').classList.remove("visible");
1289
+ this.feedback.classList.add("visible");
1290
+ this.showAttribution();
1291
+ this.setStatus(message || this.messages.ended);
1292
+ this.stopDuration(true);
1293
+ this.syncHeaderActions();
1294
+ this.clearAmbientHideTimer();
1295
+ this.ambient?.remove();
1296
+ this.ambient = null;
1297
+ this.updateHighlight(null, "focus");
1298
+ }
1299
+ showLimit(message) {
1300
+ this.setComposerEnabled(false);
1301
+ this.setPaused(true);
1302
+ this.assistantState = "paused";
1303
+ this.setStatus(message || this.messages.limitReached);
1304
+ }
1305
+ showActiveElsewhere(active) {
1306
+ this.activeElsewhere = active;
1307
+ const card = this.require('[data-role="elsewhere"]');
1308
+ card.classList.toggle("visible", active);
1309
+ card.textContent = this.messages.activeElsewhere;
1310
+ this.syncStartAvailability();
1311
+ }
1312
+ showLaunchUnavailable(message) {
1313
+ this.launchUnavailable = true;
1314
+ this.active = false;
1315
+ this.assistantState = "unavailable";
1316
+ this.setCompact(false, false);
1317
+ this.clearCompactCaptions();
1318
+ this.clearPanelSessionState();
1319
+ this.resetPanelPosition();
1320
+ this.intro.hidden = false;
1321
+ this.conversation.classList.remove("visible");
1322
+ this.controls.classList.remove("visible");
1323
+ this.controls.classList.remove("error");
1324
+ this.setComposerEnabled(false);
1325
+ this.stopDuration(true);
1326
+ this.duration.hidden = true;
1327
+ this.duration.textContent = "00:00";
1328
+ const card = this.require('[data-role="availability"]');
1329
+ card.classList.add("visible");
1330
+ card.textContent = message;
1331
+ this.setStatus(message);
1332
+ this.syncStartAvailability();
1333
+ this.syncHeaderActions();
1334
+ }
1335
+ updateHighlight(element, status) {
1336
+ this.highlightedElement = element;
1337
+ this.highlight.className = `highlight ${status}`;
1338
+ if (!element || !element.isConnected)
1339
+ return;
1340
+ const rect = element.getBoundingClientRect();
1341
+ this.highlight.style.left = `${Math.max(0, rect.left - 4)}px`;
1342
+ this.highlight.style.top = `${Math.max(0, rect.top - 4)}px`;
1343
+ this.highlight.style.width = `${Math.max(8, rect.width + 8)}px`;
1344
+ this.highlight.style.height = `${Math.max(8, rect.height + 8)}px`;
1345
+ this.highlight.classList.add("visible");
1346
+ }
1347
+ safetySurfaceVisible() {
1348
+ if (!this.panel.classList.contains("open") ||
1349
+ document.visibilityState === "hidden") {
1350
+ return false;
1351
+ }
1352
+ const surface = this.confirmation.classList.contains("visible")
1353
+ ? this.confirmation
1354
+ : this.visualRequest.classList.contains("visible")
1355
+ ? this.visualRequest
1356
+ : this.panel;
1357
+ const rect = surface.getBoundingClientRect();
1358
+ const computed = getComputedStyle(surface);
1359
+ if (rect.width < 2 ||
1360
+ rect.height < 2 ||
1361
+ rect.bottom <= 0 ||
1362
+ rect.right <= 0 ||
1363
+ rect.top >= window.innerHeight ||
1364
+ rect.left >= window.innerWidth ||
1365
+ computed.display === "none" ||
1366
+ computed.visibility === "hidden" ||
1367
+ Number.parseFloat(computed.opacity || "1") <= 0) {
1368
+ return false;
1369
+ }
1370
+ const points = [
1371
+ [rect.left + rect.width / 2, rect.top + Math.min(rect.height / 2, 24)],
1372
+ [
1373
+ rect.left + Math.min(rect.width - 2, 16),
1374
+ rect.top + Math.min(rect.height - 2, 16),
1375
+ ],
1376
+ ];
1377
+ const shadowFromPoint = this.shadow.elementFromPoint?.bind(this.shadow);
1378
+ return points.some(([rawX, rawY]) => {
1379
+ const x = Math.max(0, Math.min(window.innerWidth - 1, rawX));
1380
+ const y = Math.max(0, Math.min(window.innerHeight - 1, rawY));
1381
+ const documentTop = document.elementFromPoint(x, y);
1382
+ if (documentTop !== this.host && !this.panel.contains(documentTop)) {
1383
+ return false;
1384
+ }
1385
+ const shadowTop = shadowFromPoint?.(x, y) ?? null;
1386
+ return !shadowTop || this.panel.contains(shadowTop);
1387
+ });
1388
+ }
1389
+ destroy() {
1390
+ window.removeEventListener("resize", this.onResize);
1391
+ window.removeEventListener("scroll", this.refreshHighlight);
1392
+ document.removeEventListener("pointerdown", this.onDocumentPointerDown, true);
1393
+ this.hideTooltip();
1394
+ this.audio.pause();
1395
+ this.audio.srcObject = null;
1396
+ this.stopDuration(false);
1397
+ this.clearCompactCaptions();
1398
+ this.clearAmbientHideTimer();
1399
+ this.hideAttribution();
1400
+ this.host.remove();
1401
+ this.currentConfirmation = null;
1402
+ this.currentVisualRequest = null;
1403
+ this.highlightedElement = null;
1404
+ }
1405
+ build() {
1406
+ const fragment = document.createDocumentFragment();
1407
+ const launcher = document.createElement("button");
1408
+ launcher.type = "button";
1409
+ launcher.className = "launcher";
1410
+ launcher.dataset.role = "launcher";
1411
+ const highlight = document.createElement("div");
1412
+ highlight.className = "highlight";
1413
+ highlight.setAttribute("aria-hidden", "true");
1414
+ const tooltip = document.createElement("div");
1415
+ tooltip.className = "tooltip";
1416
+ tooltip.setAttribute("role", "tooltip");
1417
+ tooltip.setAttribute("aria-hidden", "true");
1418
+ const panel = document.createElement("section");
1419
+ panel.className = "panel";
1420
+ panel.setAttribute("role", "dialog");
1421
+ panel.setAttribute("aria-modal", "false");
1422
+ panel.setAttribute("aria-label", this.messages.introTitle);
1423
+ panel.innerHTML = `
1424
+ <div class="compact-captions" data-role="compact-captions" aria-live="polite" aria-atomic="false"></div>
1425
+ <header class="header">
1426
+ <div class="identity" data-role="drag-handle"><strong data-role="brand"></strong><span data-role="status" aria-live="polite"></span></div>
1427
+ <div class="header-actions">
1428
+ <span class="duration" data-role="duration" aria-label="Session duration" hidden>00:00</span>
1429
+ <button type="button" class="end icon-button" data-role="end" data-tooltip hidden><span class="button-icon" data-icon aria-hidden="true"></span></button>
1430
+ <button type="button" class="view-toggle icon-button" data-role="view-toggle" data-tooltip aria-controls="oxvo-ai-live-assist-content" hidden><span class="button-icon" data-icon aria-hidden="true"></span></button>
1431
+ <button type="button" class="restart icon-button" data-role="restart" data-tooltip hidden><span class="button-icon" data-icon aria-hidden="true"></span></button>
1432
+ <button type="button" class="close icon-button" data-role="close" data-tooltip><span class="button-icon" data-icon aria-hidden="true"></span></button>
1433
+ </div>
1434
+ </header>
1435
+ <div class="legal-overlay" data-role="legal-overlay" hidden>
1436
+ <section class="legal-dialog" role="dialog" aria-modal="true" aria-labelledby="oxvo-ai-legal-title">
1437
+ <div class="legal-header"><h3 id="oxvo-ai-legal-title" data-role="legal-title"></h3><button type="button" class="legal-close icon-button" data-role="legal-close" data-tooltip><span class="button-icon" data-icon aria-hidden="true"></span></button></div>
1438
+ <p data-role="legal-disclosure"></p>
1439
+ <div class="boundary" data-role="boundary"></div>
1440
+ <ul data-role="legal-scopes"></ul>
1441
+ <div class="legal-links"><a data-role="privacy" target="_blank" rel="noopener noreferrer"></a><a data-role="terms" target="_blank" rel="noopener noreferrer"></a></div>
1442
+ </section>
1443
+ </div>
1444
+ <div class="body" id="oxvo-ai-live-assist-content">
1445
+ <div class="intro">
1446
+ <h2></h2>
1447
+ <div class="consents" data-role="consent-controls">
1448
+ <label class="consent"><input type="checkbox" data-role="consent-agreement" data-scope="ai_disclosure" checked><span data-role="consent-label"></span></label>
1449
+ <button type="button" class="legal-trigger" data-role="legal-open"></button>
1450
+ </div>
1451
+ <div class="status-card" data-role="availability"></div>
1452
+ <div class="status-card" data-role="elsewhere"></div>
1453
+ <div class="mode-actions">
1454
+ <button type="button" class="secondary" data-role="start-text"></button>
1455
+ <button type="button" class="primary" data-role="start-voice"></button>
1456
+ </div>
1457
+ </div>
1458
+ <div class="conversation">
1459
+ <div class="captions" aria-live="polite"><div class="empty" data-role="empty"></div></div>
1460
+ </div>
1461
+ <div class="confirmation" role="alertdialog" aria-modal="true">
1462
+ <h3></h3>
1463
+ <p><strong data-role="action"></strong> · <span data-role="target"></span></p>
1464
+ <p data-role="reason"></p><p data-role="effect"></p>
1465
+ <div class="confirmation-actions"><button type="button" class="secondary" data-role="decline"></button><button type="button" class="primary" data-role="approve"></button></div>
1466
+ </div>
1467
+ <div class="visual-request" role="alertdialog" aria-modal="true">
1468
+ <h3></h3>
1469
+ <p data-role="visual-reason"></p>
1470
+ <div class="confirmation-actions"><button type="button" class="secondary" data-role="visual-decline"></button><button type="button" class="primary" data-role="visual-share"></button></div>
1471
+ </div>
1472
+ <div class="status-card" data-role="notice"></div>
1473
+ <button type="button" class="secondary" data-role="retry" hidden></button>
1474
+ <div class="feedback">
1475
+ <div class="attribution-bubble" data-role="attribution" role="status" hidden>Powered by <a href="https://oxvo.com/" target="_blank" rel="noopener noreferrer">OXVO.com</a></div>
1476
+ <strong></strong>
1477
+ <div class="feedback-options">
1478
+ <button type="button" class="secondary" data-outcome="resolved"></button>
1479
+ <button type="button" class="secondary" data-outcome="partially_resolved"></button>
1480
+ <button type="button" class="secondary" data-outcome="not_resolved"></button>
1481
+ </div>
1482
+ </div>
1483
+ </div>
1484
+ <footer class="controls">
1485
+ <div class="composer-shell">
1486
+ <form class="composer" data-role="composer" aria-busy="true">
1487
+ <details class="more-controls"><summary data-role="more" data-tooltip aria-haspopup="true" aria-expanded="false"><span class="button-icon" data-icon aria-hidden="true"></span></summary><div class="more-menu" aria-label="Session controls">
1488
+ <button type="button" class="menu-control" data-role="mute"><span class="menu-icon" data-icon aria-hidden="true"></span><span class="control-label"></span></button>
1489
+ <button type="button" class="menu-control" data-role="interrupt"><span class="menu-icon" data-icon aria-hidden="true"></span><span class="control-label"></span></button>
1490
+ <button type="button" class="menu-control" data-role="turn-mode"><span class="menu-icon" data-icon aria-hidden="true"></span><span class="control-label"></span></button>
1491
+ <button type="button" class="menu-control" data-role="pause"><span class="menu-icon" data-icon aria-hidden="true"></span><span class="control-label"></span></button>
1492
+ <button type="button" class="menu-control" data-role="captions"><span class="menu-icon" data-icon aria-hidden="true"></span><span class="control-label"></span></button>
1493
+ <button type="button" class="menu-control" data-role="text-only"><span class="menu-icon" data-icon aria-hidden="true"></span><span class="control-label"></span></button>
1494
+ <button type="button" class="menu-control" data-role="visual-pause" hidden><span class="menu-icon" data-icon aria-hidden="true"></span><span class="control-label"></span></button>
1495
+ <button type="button" class="menu-control" data-role="human"><span class="menu-icon" data-icon aria-hidden="true"></span><span class="control-label"></span></button>
1496
+ </div></details>
1497
+ <input data-role="text-input" maxlength="4000" autocomplete="off" disabled>
1498
+ <button type="submit" class="send icon-button primary" data-role="send" data-tooltip disabled><span class="button-icon" data-icon aria-hidden="true"></span></button>
1499
+ </form>
1500
+ <div class="manual-turn-controls" data-role="manual-turn-controls" hidden>
1501
+ <button type="button" class="turn-action primary" data-role="push" hidden><span class="turn-icon" data-icon aria-hidden="true"></span><span class="control-label"></span></button>
1502
+ <button type="button" class="turn-action secondary" data-role="automatic-turns"><span class="turn-icon" data-icon aria-hidden="true"></span><span class="control-label"></span></button>
1503
+ </div>
1504
+ </div>
1505
+ </footer>
1506
+ <audio autoplay></audio>
1507
+ `;
1508
+ fragment.append(launcher, highlight, panel, tooltip);
1509
+ return fragment;
1510
+ }
1511
+ bind() {
1512
+ this.launcher.addEventListener("click", () => this.openPanel());
1513
+ this.require('[data-role="end"]').addEventListener("click", () => {
1514
+ if (this.active)
1515
+ this.callbacks.onEnd();
1516
+ });
1517
+ this.require('[data-role="restart"]').addEventListener("click", () => {
1518
+ if (this.active || this.assistantState !== "ended")
1519
+ return;
1520
+ this.resetForNextSession();
1521
+ const preferred = this.config.capabilities?.voice
1522
+ ? '[data-role="start-voice"]'
1523
+ : '[data-role="start-text"]';
1524
+ this.require(preferred).focus();
1525
+ });
1526
+ this.require('[data-role="close"]').addEventListener("click", () => this.closePanel());
1527
+ this.require('[data-role="view-toggle"]').addEventListener("click", () => {
1528
+ if (!this.active || this.currentConfirmation || this.currentVisualRequest)
1529
+ return;
1530
+ this.setCompact(!this.compact);
1531
+ });
1532
+ this.require('[data-role="legal-open"]').addEventListener("click", () => this.openLegalDetails());
1533
+ this.require('[data-role="legal-close"]').addEventListener("click", () => this.closeLegalDetails());
1534
+ this.require('[data-role="legal-overlay"]').addEventListener("click", (event) => {
1535
+ if (event.target === event.currentTarget)
1536
+ this.closeLegalDetails();
1537
+ });
1538
+ this.require('[data-role="consent-agreement"]').addEventListener("change", () => this.syncStartAvailability());
1539
+ const dragHandle = this.require('[data-role="drag-handle"]');
1540
+ dragHandle.addEventListener("pointerdown", this.onDragStart);
1541
+ dragHandle.addEventListener("pointermove", this.onDragMove);
1542
+ dragHandle.addEventListener("pointerup", this.onDragEnd);
1543
+ dragHandle.addEventListener("pointercancel", this.onDragEnd);
1544
+ this.require('[data-role="start-text"]').addEventListener("click", () => this.start("text"));
1545
+ this.require('[data-role="start-voice"]').addEventListener("click", () => this.start("voice"));
1546
+ this.require('[data-role="mute"]').addEventListener("click", () => {
1547
+ this.setMuted(!this.muted);
1548
+ this.callbacks.onMute(this.muted);
1549
+ });
1550
+ this.require('[data-role="interrupt"]').addEventListener("click", () => {
1551
+ this.interruptPlayback();
1552
+ this.callbacks.onInterrupt();
1553
+ });
1554
+ this.require('[data-role="turn-mode"]').addEventListener("click", () => {
1555
+ const manual = !this.manualTurn;
1556
+ if (!manual && !this.automaticTurnsAvailable)
1557
+ return;
1558
+ this.callbacks.onVoiceTurnMode(manual);
1559
+ });
1560
+ this.require('[data-role="automatic-turns"]').addEventListener("click", () => {
1561
+ if (!this.manualTurn || !this.automaticTurnsAvailable)
1562
+ return;
1563
+ this.callbacks.onVoiceTurnMode(false);
1564
+ });
1565
+ this.require('[data-role="push"]').addEventListener("click", () => {
1566
+ if (!this.manualTurn)
1567
+ return;
1568
+ this.callbacks.onPushToTalk(!this.pushActive);
1569
+ });
1570
+ this.require('[data-role="pause"]').addEventListener("click", () => {
1571
+ this.setPaused(!this.paused);
1572
+ this.callbacks.onPause(this.paused);
1573
+ });
1574
+ this.require('[data-role="captions"]').addEventListener("click", () => {
1575
+ this.captionsEnabled = !this.captionsEnabled;
1576
+ this.setControlLabel("captions", this.captionsEnabled
1577
+ ? this.messages.captionsOn
1578
+ : this.messages.captionsOff);
1579
+ this.setControlIcon("captions", this.captionsEnabled ? CaptionsOff : Captions);
1580
+ this.require('[data-role="captions"]').setAttribute("aria-pressed", String(!this.captionsEnabled));
1581
+ this.captions.hidden = !this.captionsEnabled;
1582
+ this.compactCaptions.hidden = !this.captionsEnabled;
1583
+ if (!this.captionsEnabled) {
1584
+ this.clearPendingCaptions();
1585
+ this.clearCompactCaptions();
1586
+ }
1587
+ this.callbacks.onCaptions(this.captionsEnabled);
1588
+ });
1589
+ this.require('[data-role="text-only"]').addEventListener("click", () => this.callbacks.onTextOnly());
1590
+ this.require('[data-role="visual-pause"]').addEventListener("click", () => {
1591
+ this.setVisualPaused(!this.visualPaused);
1592
+ this.callbacks.onVisualPause(this.visualPaused);
1593
+ });
1594
+ this.require('[data-role="human"]').addEventListener("click", () => this.callbacks.onHuman());
1595
+ const controlsMenu = this.require(".more-menu");
1596
+ controlsMenu.addEventListener("click", (event) => {
1597
+ if (!event.target.closest("button"))
1598
+ return;
1599
+ queueMicrotask(() => this.closeControlsMenu());
1600
+ });
1601
+ const moreControls = this.require(".more-controls");
1602
+ const moreTrigger = this.require('[data-role="more"]');
1603
+ moreTrigger.addEventListener("click", (event) => {
1604
+ event.preventDefault();
1605
+ moreControls.open = !moreControls.open;
1606
+ moreTrigger.setAttribute("aria-expanded", String(moreControls.open));
1607
+ this.hideTooltip();
1608
+ });
1609
+ moreControls.addEventListener("toggle", () => {
1610
+ this.require('[data-role="more"]').setAttribute("aria-expanded", String(moreControls.open));
1611
+ this.hideTooltip();
1612
+ });
1613
+ this.require('[data-role="composer"]').addEventListener("submit", (event) => {
1614
+ event.preventDefault();
1615
+ const input = this.require('[data-role="text-input"]');
1616
+ if (input.disabled)
1617
+ return;
1618
+ const value = input.value.trim();
1619
+ if (!value)
1620
+ return;
1621
+ input.value = "";
1622
+ this.addCaption("user", value, true);
1623
+ this.callbacks.onText(value);
1624
+ });
1625
+ this.require('[data-role="approve"]').addEventListener("click", () => this.resolveConfirmation(true));
1626
+ this.require('[data-role="decline"]').addEventListener("click", () => this.resolveConfirmation(false));
1627
+ this.require('[data-role="visual-share"]').addEventListener("click", () => this.resolveVisualContext(true));
1628
+ this.require('[data-role="visual-decline"]').addEventListener("click", () => this.resolveVisualContext(false));
1629
+ this.require('[data-role="retry"]').addEventListener("click", () => this.callbacks.onRetry());
1630
+ this.shadow
1631
+ .querySelectorAll("[data-outcome]")
1632
+ .forEach((button) => {
1633
+ button.addEventListener("click", () => {
1634
+ const outcome = button.dataset.outcome;
1635
+ this.callbacks.onFeedback(outcome);
1636
+ this.resetForNextSession();
1637
+ this.closePanel();
1638
+ });
1639
+ });
1640
+ this.host.addEventListener("keydown", (event) => {
1641
+ if (event.key !== "Escape")
1642
+ return;
1643
+ if (this.closeControlsMenu(true)) {
1644
+ event.preventDefault();
1645
+ return;
1646
+ }
1647
+ if (!this.require('[data-role="legal-overlay"]').hidden) {
1648
+ this.closeLegalDetails();
1649
+ return;
1650
+ }
1651
+ if (this.active)
1652
+ this.require('[data-role="end"]').focus();
1653
+ else
1654
+ this.closePanel();
1655
+ });
1656
+ this.shadow.addEventListener("mouseover", this.onTooltipMouseOver);
1657
+ this.shadow.addEventListener("mouseout", this.onTooltipMouseOut);
1658
+ this.shadow.addEventListener("focusin", this.onTooltipFocusIn);
1659
+ this.shadow.addEventListener("focusout", this.onTooltipFocusOut);
1660
+ document.addEventListener("pointerdown", this.onDocumentPointerDown, true);
1661
+ window.addEventListener("resize", this.onResize, { passive: true });
1662
+ window.addEventListener("scroll", this.refreshHighlight, { passive: true });
1663
+ }
1664
+ applyConfig() {
1665
+ const appearance = this.config.appearance;
1666
+ const accent = appearance?.accent && /^#[0-9a-f]{6}$/i.test(appearance.accent)
1667
+ ? appearance.accent
1668
+ : "#2563eb";
1669
+ this.host.style.setProperty("--accent", accent);
1670
+ this.host.style.setProperty("--panel-width", `${Math.min(560, Math.max(320, appearance?.panelWidth ?? 380))}px`);
1671
+ this.host.dir = /^(ar|fa|he|ur)(-|$)/i.test(this.config.locale?.default ?? "")
1672
+ ? "rtl"
1673
+ : "ltr";
1674
+ this.panel.dataset.anchor =
1675
+ appearance?.position === "left" ? "left" : "right";
1676
+ if (appearance?.position === "left") {
1677
+ this.launcher.style.insetInlineStart = "20px";
1678
+ this.launcher.style.insetInlineEnd = "auto";
1679
+ }
1680
+ this.launcher.textContent =
1681
+ appearance?.launcherLabel || this.messages.launcher;
1682
+ this.launcher.dataset.label = this.launcher.textContent;
1683
+ this.require('[data-role="brand"]').textContent =
1684
+ appearance?.brandName || this.messages.introTitle;
1685
+ this.status.textContent = this.messages.ready;
1686
+ this.require(".intro h2").textContent = this.messages.introTitle;
1687
+ this.require('[data-role="consent-label"]').textContent =
1688
+ this.messages.consentAgreement;
1689
+ this.require('[data-role="legal-open"]').textContent =
1690
+ this.messages.legalDetails;
1691
+ this.require('[data-role="legal-title"]').textContent =
1692
+ this.messages.legalTitle;
1693
+ this.require('[data-role="legal-disclosure"]').textContent =
1694
+ this.config.consent?.disclosure || this.messages.disclosure;
1695
+ this.require('[data-role="boundary"]').textContent =
1696
+ this.messages.safetyBoundary;
1697
+ this.require('[data-role="privacy"]').textContent =
1698
+ this.messages.privacyPolicy;
1699
+ this.require('[data-role="terms"]').textContent =
1700
+ this.messages.termsOfService;
1701
+ this.require('[data-role="empty"]').textContent =
1702
+ this.messages.emptyConversation;
1703
+ const end = this.require('[data-role="end"]');
1704
+ end.setAttribute("aria-label", this.messages.end);
1705
+ this.setTooltip(end, this.messages.end);
1706
+ this.setControlIcon("end", Square);
1707
+ const restart = this.require('[data-role="restart"]');
1708
+ restart.setAttribute("aria-label", this.messages.restart);
1709
+ this.setTooltip(restart, this.messages.restart);
1710
+ this.setControlIcon("restart", RotateCcw);
1711
+ const legalClose = this.require('[data-role="legal-close"]');
1712
+ legalClose.setAttribute("aria-label", this.messages.legalClose);
1713
+ this.setTooltip(legalClose, this.messages.legalClose);
1714
+ this.setControlIcon("legal-close", X);
1715
+ const close = this.require('[data-role="close"]');
1716
+ close.setAttribute("aria-label", this.messages.close);
1717
+ this.setTooltip(close, this.messages.close);
1718
+ this.setControlIcon("close", X);
1719
+ const viewToggle = this.require('[data-role="view-toggle"]');
1720
+ viewToggle.setAttribute("aria-label", this.messages.minimize);
1721
+ this.setTooltip(viewToggle, this.messages.minimize);
1722
+ this.setControlIcon("view-toggle", Minimize2);
1723
+ this.require('[data-role="start-text"]').textContent =
1724
+ this.messages.startText;
1725
+ this.require('[data-role="start-voice"]').textContent =
1726
+ this.messages.startVoice;
1727
+ this.require('[data-role="start-text"]').hidden =
1728
+ !this.config.capabilities?.text;
1729
+ this.require('[data-role="start-voice"]').hidden =
1730
+ !this.config.capabilities?.voice;
1731
+ const send = this.require('[data-role="send"]');
1732
+ send.setAttribute("aria-label", this.messages.send);
1733
+ this.setTooltip(send, this.messages.send);
1734
+ this.setControlIcon("send", Send);
1735
+ this.require('[data-role="text-input"]').placeholder =
1736
+ this.messages.messagePlaceholder;
1737
+ this.setControlLabel("mute", this.messages.mute);
1738
+ this.setControlIcon("mute", Mic);
1739
+ this.setControlLabel("interrupt", this.messages.interrupt);
1740
+ this.setControlIcon("interrupt", CircleStop);
1741
+ this.setControlLabel("turn-mode", this.messages.usePushToTalk);
1742
+ this.setControlIcon("turn-mode", Hand);
1743
+ this.setControlLabel("push", this.messages.startSpeaking);
1744
+ this.setControlIcon("push", Mic);
1745
+ this.configureVoiceControls(false, false, false);
1746
+ this.setControlLabel("pause", this.messages.pauseActions);
1747
+ this.setControlIcon("pause", Pause);
1748
+ this.setControlLabel("captions", this.messages.captionsOn);
1749
+ this.setControlIcon("captions", CaptionsOff);
1750
+ this.setControlLabel("text-only", this.messages.textOnly);
1751
+ this.setControlIcon("text-only", MessageSquareText);
1752
+ this.setControlLabel("visual-pause", this.messages.pauseVisual);
1753
+ this.setControlIcon("visual-pause", EyeOff);
1754
+ this.setControlLabel("human", this.messages.requestHuman);
1755
+ this.setControlIcon("human", UserRound);
1756
+ const more = this.require('[data-role="more"]');
1757
+ more.setAttribute("aria-label", this.messages.moreControls);
1758
+ this.setTooltip(more, this.messages.moreControls);
1759
+ this.setControlIcon("more", Ellipsis);
1760
+ this.require('[data-role="pause"]').hidden =
1761
+ !this.config.capabilities?.browserControl;
1762
+ this.require('[data-role="captions"]').hidden =
1763
+ !this.config.capabilities?.captions;
1764
+ this.require('[data-role="human"]').hidden =
1765
+ !this.config.capabilities?.handoff;
1766
+ this.require('[data-role="approve"]').textContent = this.messages.approve;
1767
+ this.require('[data-role="decline"]').textContent = this.messages.decline;
1768
+ this.require(".confirmation h3").textContent = this.messages.actionRequest;
1769
+ this.require(".visual-request h3").textContent =
1770
+ this.messages.visualRequest;
1771
+ this.require('[data-role="visual-share"]').textContent =
1772
+ this.messages.shareVisual;
1773
+ this.require('[data-role="visual-decline"]').textContent =
1774
+ this.messages.declineVisual;
1775
+ this.require('[data-role="retry"]').textContent = this.messages.retry;
1776
+ this.require(".feedback > strong").textContent =
1777
+ this.messages.feedbackQuestion;
1778
+ this.require('[data-outcome="resolved"]').textContent =
1779
+ this.messages.resolved;
1780
+ this.require('[data-outcome="partially_resolved"]').textContent =
1781
+ this.messages.partiallyResolved;
1782
+ this.require('[data-outcome="not_resolved"]').textContent =
1783
+ this.messages.notResolved;
1784
+ this.renderLegalScopes();
1785
+ const privacy = this.require('[data-role="privacy"]');
1786
+ if (this.config.consent?.policyUrl)
1787
+ privacy.href = this.config.consent.policyUrl;
1788
+ else
1789
+ privacy.hidden = true;
1790
+ const terms = this.require('[data-role="terms"]');
1791
+ if (this.config.consent?.termsUrl)
1792
+ terms.href = this.config.consent.termsUrl;
1793
+ else
1794
+ terms.hidden = true;
1795
+ this.require('[data-role="legal-open"]').hidden =
1796
+ privacy.hidden && terms.hidden && !this.config.consent?.disclosure;
1797
+ if (this.hasActiveSessionMarker())
1798
+ this.restorePanelPosition();
1799
+ else
1800
+ this.resetPanelPosition();
1801
+ this.syncStartAvailability();
1802
+ this.syncHeaderActions();
1803
+ }
1804
+ renderLegalScopes() {
1805
+ const container = this.require('[data-role="legal-scopes"]');
1806
+ container.replaceChildren();
1807
+ const offered = new Set(this.config.consent?.optionalScopes ?? []);
1808
+ const labels = {
1809
+ microphone: "microphoneConsent",
1810
+ page_context: "pageConsent",
1811
+ browser_control: "controlConsent",
1812
+ visual_context: "visualConsent",
1813
+ handoff_context: "handoffConsent",
1814
+ retained_transcript: "retentionConsent",
1815
+ };
1816
+ const order = [
1817
+ "microphone",
1818
+ "page_context",
1819
+ "browser_control",
1820
+ "visual_context",
1821
+ "handoff_context",
1822
+ "retained_transcript",
1823
+ ];
1824
+ for (const scope of order) {
1825
+ const messageKey = labels[scope];
1826
+ if (!offered.has(scope) || !messageKey)
1827
+ continue;
1828
+ const item = document.createElement("li");
1829
+ item.textContent = this.messages[messageKey];
1830
+ container.append(item);
1831
+ }
1832
+ }
1833
+ start(mode) {
1834
+ if (this.launchUnavailable || this.activeElsewhere)
1835
+ return;
1836
+ const agreement = this.require('[data-role="consent-agreement"]');
1837
+ if (!agreement.checked) {
1838
+ agreement.focus();
1839
+ return;
1840
+ }
1841
+ const offered = new Set([
1842
+ ...(this.config.consent?.requiredScopes ?? []),
1843
+ ...(this.config.consent?.optionalScopes ?? []),
1844
+ ]);
1845
+ const scopes = ["ai_disclosure", "text"];
1846
+ for (const scope of offered) {
1847
+ if (scope !== "ai_disclosure" &&
1848
+ scope !== "text" &&
1849
+ (scope !== "microphone" || mode === "voice") &&
1850
+ !scopes.includes(scope)) {
1851
+ scopes.push(scope);
1852
+ }
1853
+ }
1854
+ if (mode === "voice" && !scopes.includes("microphone"))
1855
+ return;
1856
+ if (scopes.includes("browser_control") &&
1857
+ !scopes.includes("page_context")) {
1858
+ scopes.splice(scopes.indexOf("browser_control"), 1);
1859
+ }
1860
+ this.callbacks.onStart(mode, scopes);
1861
+ }
1862
+ resolveConfirmation(approved) {
1863
+ const value = this.currentConfirmation;
1864
+ if (!value || Date.parse(value.expiresAt) <= Date.now()) {
1865
+ this.showActionState("failed");
1866
+ return;
1867
+ }
1868
+ this.confirmation.classList.remove("visible");
1869
+ this.currentConfirmation = null;
1870
+ this.syncHeaderActions();
1871
+ this.callbacks.onConfirmation(value.actionId, value.proposalChecksum, approved);
1872
+ }
1873
+ setComposerEnabled(enabled) {
1874
+ const composer = this.require('[data-role="composer"]');
1875
+ const input = this.require('[data-role="text-input"]');
1876
+ const send = this.require('[data-role="send"]');
1877
+ input.disabled = !enabled;
1878
+ send.disabled = !enabled;
1879
+ composer.setAttribute("aria-busy", String(!enabled));
1880
+ }
1881
+ syncStartAvailability() {
1882
+ const unavailable = this.launchUnavailable || this.activeElsewhere;
1883
+ const agreement = this.require('[data-role="consent-agreement"]');
1884
+ const disabled = unavailable || !agreement.checked;
1885
+ this.require('[data-role="start-text"]').disabled =
1886
+ disabled;
1887
+ this.require('[data-role="start-voice"]').disabled =
1888
+ disabled;
1889
+ this.require('[data-role="consent-controls"]').hidden =
1890
+ unavailable;
1891
+ this.require(".mode-actions").hidden = unavailable;
1892
+ }
1893
+ resolveVisualContext(approved) {
1894
+ const value = this.currentVisualRequest;
1895
+ if (!value)
1896
+ return;
1897
+ this.visualRequest.classList.remove("visible");
1898
+ this.currentVisualRequest = null;
1899
+ this.syncHeaderActions();
1900
+ this.callbacks.onVisualContext(value, approved);
1901
+ }
1902
+ openPanel() {
1903
+ this.panel.classList.add("open");
1904
+ this.launcher.hidden = true;
1905
+ if (this.active)
1906
+ this.writePanelVisibility("open");
1907
+ this.syncHeaderActions();
1908
+ requestAnimationFrame(() => this.clampPanelPosition());
1909
+ }
1910
+ closePanel() {
1911
+ this.closeControlsMenu();
1912
+ this.hideTooltip();
1913
+ this.closeLegalDetails();
1914
+ if (this.currentConfirmation)
1915
+ this.resolveConfirmation(false);
1916
+ if (this.currentVisualRequest)
1917
+ this.resolveVisualContext(false);
1918
+ this.panel.classList.remove("open");
1919
+ this.launcher.hidden = false;
1920
+ if (this.active)
1921
+ this.writePanelVisibility("hidden");
1922
+ else
1923
+ this.removeStoredPanelVisibility();
1924
+ this.syncLauncherState();
1925
+ this.launcher.focus();
1926
+ }
1927
+ initializePanelView() {
1928
+ if (!this.active || this.panelViewInitialized)
1929
+ return;
1930
+ const stored = this.readPanelView();
1931
+ this.panelViewInitialized = true;
1932
+ this.setCompact(stored ? stored === "compact" : this.voiceActive);
1933
+ }
1934
+ setCompact(value, persist = true) {
1935
+ this.closeControlsMenu();
1936
+ this.hideTooltip();
1937
+ this.compact = this.active ? value : false;
1938
+ this.panel.classList.toggle("compact", this.compact);
1939
+ if (persist && this.active) {
1940
+ this.panelViewInitialized = true;
1941
+ try {
1942
+ sessionStorage.setItem(PANEL_VIEW_KEY, this.compact ? "compact" : "expanded");
1943
+ }
1944
+ catch {
1945
+ // The current view still works when session storage is unavailable.
1946
+ }
1947
+ }
1948
+ this.syncHeaderActions();
1949
+ requestAnimationFrame(() => this.clampPanelPosition());
1950
+ }
1951
+ readPanelView() {
1952
+ try {
1953
+ const stored = sessionStorage.getItem(PANEL_VIEW_KEY);
1954
+ return stored === "compact" || stored === "expanded" ? stored : null;
1955
+ }
1956
+ catch {
1957
+ return null;
1958
+ }
1959
+ }
1960
+ readPanelVisibility() {
1961
+ try {
1962
+ const stored = sessionStorage.getItem(PANEL_VISIBILITY_KEY);
1963
+ return stored === "open" || stored === "hidden" ? stored : null;
1964
+ }
1965
+ catch {
1966
+ return null;
1967
+ }
1968
+ }
1969
+ writePanelVisibility(value) {
1970
+ try {
1971
+ sessionStorage.setItem(PANEL_VISIBILITY_KEY, value);
1972
+ }
1973
+ catch {
1974
+ // Panel visibility remains usable when session storage is unavailable.
1975
+ }
1976
+ }
1977
+ removeStoredPanelVisibility() {
1978
+ try {
1979
+ sessionStorage.removeItem(PANEL_VISIBILITY_KEY);
1980
+ }
1981
+ catch {
1982
+ // Ignore unavailable session storage.
1983
+ }
1984
+ }
1985
+ clearPanelSessionState() {
1986
+ this.panelViewInitialized = false;
1987
+ try {
1988
+ sessionStorage.removeItem(PANEL_VIEW_KEY);
1989
+ sessionStorage.removeItem(PANEL_VISIBILITY_KEY);
1990
+ }
1991
+ catch {
1992
+ // Ignore unavailable session storage.
1993
+ }
1994
+ }
1995
+ syncLauncherState() {
1996
+ const label = this.launcher.dataset.label || this.messages.launcher;
1997
+ this.launcher.dataset.active = String(this.active);
1998
+ this.launcher.dataset.assistantState = this.active
1999
+ ? this.assistantState
2000
+ : "idle";
2001
+ const status = this.status.textContent?.trim();
2002
+ this.launcher.setAttribute("aria-label", this.active
2003
+ ? `${label}. ${this.messages.activeCall}${status ? `. ${status}` : ""}`
2004
+ : label);
2005
+ }
2006
+ cancelPendingConfirmation() {
2007
+ if (!this.resolvePendingConfirmation(false))
2008
+ return false;
2009
+ this.setStatus(this.messages.ready);
2010
+ return true;
2011
+ }
2012
+ resolvePendingConfirmation(approved) {
2013
+ if (!this.currentConfirmation)
2014
+ return false;
2015
+ this.resolveConfirmation(approved);
2016
+ return true;
2017
+ }
2018
+ ensureAmbient() {
2019
+ if (this.ambient || this.config.appearance?.glassEffect === false)
2020
+ return;
2021
+ this.ambient = document.createElement("div");
2022
+ this.ambient.className = "ambient";
2023
+ this.ambient.dataset.anchor =
2024
+ this.config.appearance?.position === "left" ? "left" : "right";
2025
+ this.ambient.setAttribute("aria-hidden", "true");
2026
+ this.shadow.insertBefore(this.ambient, this.highlight);
2027
+ }
2028
+ setAmbientActive(active, immediate = false) {
2029
+ this.clearAmbientHideTimer();
2030
+ if (active) {
2031
+ this.ensureAmbient();
2032
+ this.ambient?.classList.add("active");
2033
+ return;
2034
+ }
2035
+ if (!this.ambient)
2036
+ return;
2037
+ if (immediate) {
2038
+ this.ambient.classList.remove("active");
2039
+ return;
2040
+ }
2041
+ this.ambientHideTimer = setTimeout(() => {
2042
+ this.ambientHideTimer = null;
2043
+ this.ambient?.classList.remove("active");
2044
+ }, AMBIENT_IDLE_GRACE_MS);
2045
+ }
2046
+ clearAmbientHideTimer() {
2047
+ if (this.ambientHideTimer === null)
2048
+ return;
2049
+ clearTimeout(this.ambientHideTimer);
2050
+ this.ambientHideTimer = null;
2051
+ }
2052
+ startDuration(resume) {
2053
+ if (this.durationInterval !== null)
2054
+ return;
2055
+ let startedAt = Date.now();
2056
+ if (resume) {
2057
+ try {
2058
+ const stored = Number(sessionStorage.getItem(SESSION_START_KEY));
2059
+ if (Number.isFinite(stored) && stored > 0 && stored <= startedAt) {
2060
+ startedAt = stored;
2061
+ }
2062
+ }
2063
+ catch {
2064
+ // A timer still works when session storage is unavailable.
2065
+ }
2066
+ }
2067
+ this.sessionStartedAt = startedAt;
2068
+ this.duration.hidden = false;
2069
+ try {
2070
+ sessionStorage.setItem(SESSION_START_KEY, String(startedAt));
2071
+ }
2072
+ catch {
2073
+ // A timer still works when session storage is unavailable.
2074
+ }
2075
+ this.renderDuration();
2076
+ this.durationInterval = setInterval(() => this.renderDuration(), 500);
2077
+ }
2078
+ renderDuration() {
2079
+ if (this.sessionStartedAt === null)
2080
+ return;
2081
+ const elapsedSeconds = Math.max(0, Math.floor((Date.now() - this.sessionStartedAt) / 1000));
2082
+ const minutes = Math.floor(elapsedSeconds / 60);
2083
+ const seconds = elapsedSeconds % 60;
2084
+ this.duration.textContent = `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
2085
+ }
2086
+ stopDuration(clearStoredStart) {
2087
+ if (this.durationInterval !== null) {
2088
+ clearInterval(this.durationInterval);
2089
+ this.durationInterval = null;
2090
+ }
2091
+ this.renderDuration();
2092
+ this.sessionStartedAt = null;
2093
+ if (!clearStoredStart)
2094
+ return;
2095
+ try {
2096
+ sessionStorage.removeItem(SESSION_START_KEY);
2097
+ }
2098
+ catch {
2099
+ // Ignore unavailable session storage.
2100
+ }
2101
+ }
2102
+ resetForNextSession() {
2103
+ this.active = false;
2104
+ this.assistantState = "ready";
2105
+ this.panel.classList.remove("voice-session");
2106
+ this.voiceActive = false;
2107
+ this.setCompact(false, false);
2108
+ this.clearCompactCaptions();
2109
+ this.clearPanelSessionState();
2110
+ this.resetPanelPosition();
2111
+ this.controls.classList.remove("error");
2112
+ this.feedback.classList.remove("visible");
2113
+ this.hideAttribution();
2114
+ this.intro.hidden = false;
2115
+ this.require('[data-role="notice"]').classList.remove("visible");
2116
+ this.require('[data-role="retry"]').hidden = true;
2117
+ this.duration.hidden = true;
2118
+ this.duration.textContent = "00:00";
2119
+ this.captions.replaceChildren();
2120
+ const empty = document.createElement("div");
2121
+ empty.className = "empty";
2122
+ empty.dataset.role = "empty";
2123
+ empty.textContent = this.messages.emptyConversation;
2124
+ this.captions.append(empty);
2125
+ this.setStatus(this.messages.ready);
2126
+ this.syncStartAvailability();
2127
+ this.syncHeaderActions();
2128
+ }
2129
+ showAttribution() {
2130
+ this.hideAttribution();
2131
+ if (this.config.appearance?.attribution?.enabled === false)
2132
+ return;
2133
+ this.attribution.hidden = false;
2134
+ this.attributionTimer = setTimeout(() => {
2135
+ this.attributionTimer = null;
2136
+ this.attribution.hidden = true;
2137
+ }, 7000);
2138
+ }
2139
+ hideAttribution() {
2140
+ if (this.attributionTimer)
2141
+ clearTimeout(this.attributionTimer);
2142
+ this.attributionTimer = null;
2143
+ this.attribution.hidden = true;
2144
+ }
2145
+ openLegalDetails() {
2146
+ const overlay = this.require('[data-role="legal-overlay"]');
2147
+ overlay.hidden = false;
2148
+ this.require('[data-role="legal-close"]').focus();
2149
+ }
2150
+ closeLegalDetails() {
2151
+ const overlay = this.require('[data-role="legal-overlay"]');
2152
+ if (overlay.hidden)
2153
+ return;
2154
+ overlay.hidden = true;
2155
+ this.require('[data-role="legal-open"]').focus();
2156
+ }
2157
+ setControlLabel(role, label) {
2158
+ const button = this.require(`[data-role="${role}"]`);
2159
+ const text = button.querySelector(".control-label");
2160
+ if (text)
2161
+ text.textContent = label;
2162
+ else
2163
+ button.textContent = label;
2164
+ button.setAttribute("aria-label", label);
2165
+ button.removeAttribute("title");
2166
+ if (button.hasAttribute("data-tooltip"))
2167
+ this.setTooltip(button, label);
2168
+ }
2169
+ setControlIcon(role, icon) {
2170
+ const control = this.require(`[data-role="${role}"]`);
2171
+ const slot = control.querySelector("[data-icon]");
2172
+ if (!slot)
2173
+ return;
2174
+ slot.replaceChildren(createLucideElement(icon, {
2175
+ class: "lucide",
2176
+ width: 16,
2177
+ height: 16,
2178
+ "stroke-width": 1.8,
2179
+ "aria-hidden": "true",
2180
+ focusable: "false",
2181
+ }));
2182
+ }
2183
+ setTooltip(target, label) {
2184
+ target.dataset.tooltip = label;
2185
+ target.removeAttribute("title");
2186
+ }
2187
+ showTooltip(target, immediate = false) {
2188
+ const label = target.dataset.tooltip?.trim();
2189
+ if (!label ||
2190
+ target.hidden ||
2191
+ target.getAttribute("aria-hidden") === "true" ||
2192
+ this.tooltipSuppressed(target))
2193
+ return;
2194
+ this.hideTooltip();
2195
+ const render = () => {
2196
+ this.tooltipTimer = null;
2197
+ if (!target.isConnected || this.tooltipSuppressed(target))
2198
+ return;
2199
+ this.tooltipTarget = target;
2200
+ this.tooltip.textContent = label;
2201
+ this.tooltip.style.left = "8px";
2202
+ this.tooltip.style.top = "-1000px";
2203
+ this.tooltip.classList.add("visible");
2204
+ this.tooltip.setAttribute("aria-hidden", "false");
2205
+ const targetRect = target.getBoundingClientRect();
2206
+ const tooltipRect = this.tooltip.getBoundingClientRect();
2207
+ const gap = 8;
2208
+ let top = targetRect.top - tooltipRect.height - gap;
2209
+ let placement = "top";
2210
+ if (top < 8) {
2211
+ top = targetRect.bottom + gap;
2212
+ placement = "bottom";
2213
+ }
2214
+ const left = Math.min(Math.max(8, targetRect.left + (targetRect.width - tooltipRect.width) / 2), Math.max(8, window.innerWidth - tooltipRect.width - 8));
2215
+ this.tooltip.dataset.placement = placement;
2216
+ this.tooltip.style.left = `${Math.round(left)}px`;
2217
+ this.tooltip.style.top = `${Math.round(top)}px`;
2218
+ };
2219
+ if (immediate)
2220
+ render();
2221
+ else
2222
+ this.tooltipTimer = setTimeout(render, 180);
2223
+ }
2224
+ tooltipSuppressed(target) {
2225
+ return (this.shadow.querySelector(".more-controls[open]") !== null ||
2226
+ (target.matches('[data-role="more"]') &&
2227
+ target.closest(".more-controls")?.hasAttribute("open") === true));
2228
+ }
2229
+ hideTooltip() {
2230
+ if (this.tooltipTimer !== null) {
2231
+ clearTimeout(this.tooltipTimer);
2232
+ this.tooltipTimer = null;
2233
+ }
2234
+ this.tooltipTarget = null;
2235
+ this.tooltip?.classList.remove("visible");
2236
+ this.tooltip?.setAttribute("aria-hidden", "true");
2237
+ }
2238
+ tooltipTrigger(event) {
2239
+ const target = event.target;
2240
+ return target instanceof Element
2241
+ ? target.closest("[data-tooltip]")
2242
+ : null;
2243
+ }
2244
+ closeControlsMenu(focusTrigger = false) {
2245
+ const details = this.require(".more-controls");
2246
+ if (!details.open)
2247
+ return false;
2248
+ details.open = false;
2249
+ const trigger = this.require('[data-role="more"]');
2250
+ trigger.setAttribute("aria-expanded", "false");
2251
+ if (focusTrigger)
2252
+ trigger.focus();
2253
+ return true;
2254
+ }
2255
+ setPanelPosition(left, top) {
2256
+ this.panel.style.insetInlineStart = "auto";
2257
+ this.panel.style.insetInlineEnd = "auto";
2258
+ this.panel.style.right = "auto";
2259
+ this.panel.style.bottom = "auto";
2260
+ this.panel.style.left = `${Math.round(left)}px`;
2261
+ this.panel.style.top = `${Math.round(top)}px`;
2262
+ }
2263
+ savePanelPosition(left, top) {
2264
+ try {
2265
+ sessionStorage.setItem(PANEL_POSITION_KEY, JSON.stringify({ left: Math.round(left), top: Math.round(top) }));
2266
+ }
2267
+ catch {
2268
+ // The panel remains draggable when storage is unavailable.
2269
+ }
2270
+ }
2271
+ hasActiveSessionMarker() {
2272
+ try {
2273
+ const startedAt = Number(sessionStorage.getItem(SESSION_START_KEY));
2274
+ return (Number.isFinite(startedAt) && startedAt > 0 && startedAt <= Date.now());
2275
+ }
2276
+ catch {
2277
+ return false;
2278
+ }
2279
+ }
2280
+ resetPanelPosition() {
2281
+ for (const property of [
2282
+ "inset-inline-start",
2283
+ "inset-inline-end",
2284
+ "left",
2285
+ "right",
2286
+ "top",
2287
+ "bottom",
2288
+ ]) {
2289
+ this.panel.style.removeProperty(property);
2290
+ }
2291
+ try {
2292
+ sessionStorage.removeItem(PANEL_POSITION_KEY);
2293
+ }
2294
+ catch {
2295
+ // The default anchored position still applies without session storage.
2296
+ }
2297
+ }
2298
+ restorePanelPosition() {
2299
+ if (window.innerWidth <= 520)
2300
+ return;
2301
+ try {
2302
+ const parsed = JSON.parse(sessionStorage.getItem(PANEL_POSITION_KEY) || "null");
2303
+ if (parsed &&
2304
+ typeof parsed.left === "number" &&
2305
+ Number.isFinite(parsed.left) &&
2306
+ typeof parsed.top === "number" &&
2307
+ Number.isFinite(parsed.top)) {
2308
+ this.setPanelPosition(parsed.left, parsed.top);
2309
+ }
2310
+ }
2311
+ catch {
2312
+ // Ignore malformed or unavailable session storage.
2313
+ }
2314
+ }
2315
+ clampPanelPosition() {
2316
+ if (window.innerWidth <= 520 || !this.panel.style.left)
2317
+ return;
2318
+ const rect = this.panel.getBoundingClientRect();
2319
+ const left = Math.min(Math.max(8, window.innerWidth - rect.width - 8), Math.max(8, rect.left));
2320
+ const top = Math.min(Math.max(8, window.innerHeight - rect.height - 8), Math.max(8, rect.top));
2321
+ this.setPanelPosition(left, top);
2322
+ this.savePanelPosition(left, top);
2323
+ }
2324
+ syncHeaderActions() {
2325
+ this.require('[data-role="end"]').hidden = !this.active;
2326
+ const viewToggle = this.require('[data-role="view-toggle"]');
2327
+ viewToggle.hidden = !this.active;
2328
+ viewToggle.disabled = Boolean(this.currentConfirmation || this.currentVisualRequest);
2329
+ const viewLabel = this.compact
2330
+ ? this.messages.maximize
2331
+ : this.messages.minimize;
2332
+ viewToggle.setAttribute("aria-label", viewLabel);
2333
+ viewToggle.setAttribute("aria-expanded", String(!this.compact));
2334
+ this.setTooltip(viewToggle, viewLabel);
2335
+ this.setControlIcon("view-toggle", this.compact ? Maximize2 : Minimize2);
2336
+ this.require('[data-role="restart"]').hidden =
2337
+ this.active || this.assistantState !== "ended";
2338
+ const close = this.require('[data-role="close"]');
2339
+ close.setAttribute("aria-label", this.messages.close);
2340
+ this.setTooltip(close, this.messages.close);
2341
+ this.syncLauncherState();
2342
+ }
2343
+ require(selector) {
2344
+ const element = this.shadow.querySelector(selector);
2345
+ if (!element)
2346
+ throw new Error(`Missing AI Live Assist UI element: ${selector}`);
2347
+ return element;
2348
+ }
2349
+ }