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