@usero/sdk 1.0.2 → 1.1.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.
|
@@ -145,7 +145,7 @@ async function uploadChunkWithRetry(apiUrl, sessionId, index, blob, logger, maxA
|
|
|
145
145
|
}
|
|
146
146
|
return false;
|
|
147
147
|
}
|
|
148
|
-
function buildIndicator(host, store,
|
|
148
|
+
function buildIndicator(host, store, callbacks) {
|
|
149
149
|
const root = host.attachShadow({ mode: "closed" });
|
|
150
150
|
const style = document.createElement("style");
|
|
151
151
|
style.textContent = `
|
|
@@ -160,19 +160,21 @@ function buildIndicator(host, store, onFinish, onToggleTasks) {
|
|
|
160
160
|
color: #fff;
|
|
161
161
|
}
|
|
162
162
|
.bar {
|
|
163
|
-
display: inline-flex; align-items: center; gap:
|
|
164
|
-
padding: 8px
|
|
165
|
-
background: rgba(17,17,17,0.
|
|
163
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
164
|
+
padding: 6px 8px 6px 6px;
|
|
165
|
+
background: rgba(17,17,17,0.82);
|
|
166
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
166
167
|
border-radius: 999px;
|
|
167
|
-
box-shadow: 0 8px 24px rgba(0,0,0,0.
|
|
168
|
-
backdrop-filter: blur(
|
|
168
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.22);
|
|
169
|
+
backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
|
|
169
170
|
max-width: 100%;
|
|
170
171
|
}
|
|
171
172
|
.panel {
|
|
172
|
-
background: rgba(17,17,17,0.
|
|
173
|
+
background: rgba(17,17,17,0.92);
|
|
174
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
173
175
|
border-radius: 14px; padding: 12px 14px 12px 8px;
|
|
174
176
|
line-height: 1.45;
|
|
175
|
-
box-shadow: 0 12px 32px rgba(0,0,0,0.
|
|
177
|
+
box-shadow: 0 12px 32px rgba(0,0,0,0.32);
|
|
176
178
|
max-height: min(60vh, 480px);
|
|
177
179
|
max-width: min(420px, calc(100vw - 32px));
|
|
178
180
|
width: max-content; overflow-y: auto;
|
|
@@ -181,29 +183,156 @@ function buildIndicator(host, store, onFinish, onToggleTasks) {
|
|
|
181
183
|
.panel ol { margin: 0; padding-left: 26px; }
|
|
182
184
|
.panel li { margin: 0 0 8px; }
|
|
183
185
|
.panel li:last-child { margin: 0; }
|
|
186
|
+
|
|
187
|
+
/* Mic chip: pill-within-pill with dot + label, doubles as mute toggle. */
|
|
188
|
+
.mic {
|
|
189
|
+
display: inline-flex; align-items: center; gap: 7px;
|
|
190
|
+
min-height: 32px; min-width: 44px;
|
|
191
|
+
padding: 0 11px 0 10px;
|
|
192
|
+
border-radius: 999px;
|
|
193
|
+
background: rgba(255,255,255,0.06);
|
|
194
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
195
|
+
color: #fff; font: inherit;
|
|
196
|
+
cursor: pointer; appearance: none;
|
|
197
|
+
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
|
198
|
+
}
|
|
199
|
+
.mic:hover { background: rgba(255,255,255,0.12); }
|
|
200
|
+
.mic:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
|
|
201
|
+
.mic[data-mic-state="muted"] {
|
|
202
|
+
background: rgba(251, 191, 36, 0.18);
|
|
203
|
+
border-color: rgba(251, 191, 36, 0.45);
|
|
204
|
+
color: #fcd34d;
|
|
205
|
+
}
|
|
206
|
+
.mic[data-mic-state="muted"]:hover { background: rgba(251, 191, 36, 0.26); }
|
|
207
|
+
.mic[data-mic-state="none"] {
|
|
208
|
+
background: rgba(255,255,255,0.04);
|
|
209
|
+
color: rgba(255,255,255,0.55);
|
|
210
|
+
cursor: default;
|
|
211
|
+
}
|
|
212
|
+
.mic[data-mic-state="none"]:hover { background: rgba(255,255,255,0.04); }
|
|
213
|
+
.mic-icon { width: 13px; height: 13px; display: inline-block; flex-shrink: 0; }
|
|
214
|
+
.mic-label { font-weight: 500; letter-spacing: 0.01em; white-space: nowrap; }
|
|
215
|
+
|
|
184
216
|
.dot {
|
|
185
|
-
width:
|
|
217
|
+
width: 7px; height: 7px; border-radius: 50%;
|
|
186
218
|
background: #ef4444;
|
|
187
219
|
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.6);
|
|
188
220
|
animation: pulse 1.6s ease-out infinite;
|
|
221
|
+
flex-shrink: 0;
|
|
189
222
|
}
|
|
190
223
|
.dot[data-state="no-audio"] { background: #fbbf24; animation: none; }
|
|
191
224
|
.dot[data-state="finishing"] { background: #fbbf24; animation: none; }
|
|
192
225
|
.dot[data-state="done"] { background: #10b981; animation: none; }
|
|
193
226
|
.dot[data-state="error"] { background: #ef4444; animation: none; }
|
|
194
|
-
|
|
195
|
-
.spacer { width: 1px; height: 16px; background: rgba(255,255,255,0.18); margin: 0 2px; }
|
|
227
|
+
|
|
196
228
|
.btn {
|
|
197
|
-
appearance: none; border: 0; background: rgba(255,255,255,0.
|
|
229
|
+
appearance: none; border: 0; background: rgba(255,255,255,0.10);
|
|
198
230
|
color: #fff; font: inherit; font-weight: 600;
|
|
199
|
-
padding: 6px 12px; border-radius: 999px; cursor: pointer;
|
|
200
|
-
transition: background 0.15s ease;
|
|
231
|
+
padding: 6px 12px; min-height: 32px; border-radius: 999px; cursor: pointer;
|
|
232
|
+
transition: background 0.15s ease, transform 0.06s ease;
|
|
233
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
201
234
|
}
|
|
202
|
-
.btn:hover { background: rgba(255,255,255,0.
|
|
235
|
+
.btn:hover { background: rgba(255,255,255,0.20); }
|
|
236
|
+
.btn:active { transform: scale(0.97); }
|
|
203
237
|
.btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
|
|
204
238
|
.btn[disabled] { opacity: 0.5; cursor: progress; }
|
|
205
239
|
.tasks-btn[aria-expanded="true"] { background: rgba(255,255,255,0.24); }
|
|
206
|
-
|
|
240
|
+
|
|
241
|
+
/* Note button: icon-only, matches mic chip footprint */
|
|
242
|
+
.note-btn {
|
|
243
|
+
width: 32px; min-height: 32px; padding: 0;
|
|
244
|
+
background: rgba(255,255,255,0.06);
|
|
245
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
246
|
+
border-radius: 999px;
|
|
247
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 4px;
|
|
248
|
+
color: #fff; font: inherit; cursor: pointer; appearance: none;
|
|
249
|
+
transition: background 0.15s ease, border-color 0.15s ease, width 0.18s ease;
|
|
250
|
+
overflow: hidden;
|
|
251
|
+
}
|
|
252
|
+
.note-btn:hover { background: rgba(255,255,255,0.14); }
|
|
253
|
+
.note-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
|
|
254
|
+
.note-btn[data-has-notes="true"] { width: auto; padding: 0 10px 0 9px; gap: 6px; }
|
|
255
|
+
.note-btn[aria-expanded="true"] { background: rgba(255,255,255,0.22); border-color: rgba(255,255,255,0.18); }
|
|
256
|
+
.note-icon { width: 14px; height: 14px; display: inline-block; }
|
|
257
|
+
.note-count { font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
258
|
+
|
|
259
|
+
.spacer { width: 1px; height: 18px; background: rgba(255,255,255,0.14); margin: 0 1px; }
|
|
260
|
+
|
|
261
|
+
@media (max-width: 480px) {
|
|
262
|
+
.bar { gap: 4px; padding: 5px 6px 5px 5px; }
|
|
263
|
+
.btn { padding: 7px 12px; min-height: 38px; }
|
|
264
|
+
.mic, .note-btn { min-height: 38px; }
|
|
265
|
+
.note-btn { width: 38px; }
|
|
266
|
+
.note-btn[data-has-notes="true"] { width: auto; }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* First-mute helper toast: sits above the pill, auto-dismisses */
|
|
270
|
+
.toast {
|
|
271
|
+
background: rgba(17,17,17,0.92);
|
|
272
|
+
border: 1px solid rgba(251, 191, 36, 0.45);
|
|
273
|
+
color: #fff;
|
|
274
|
+
padding: 9px 14px; border-radius: 12px;
|
|
275
|
+
max-width: min(340px, calc(100vw - 32px));
|
|
276
|
+
box-shadow: 0 12px 28px rgba(0,0,0,0.28);
|
|
277
|
+
text-align: center; line-height: 1.4;
|
|
278
|
+
animation: toast-in 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
279
|
+
}
|
|
280
|
+
.toast[data-leaving="true"] { animation: toast-out 0.24s ease forwards; }
|
|
281
|
+
.toast strong { color: #fcd34d; font-weight: 600; }
|
|
282
|
+
@keyframes toast-in {
|
|
283
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
284
|
+
to { opacity: 1; transform: translateY(0); }
|
|
285
|
+
}
|
|
286
|
+
@keyframes toast-out {
|
|
287
|
+
to { opacity: 0; transform: translateY(4px); }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* Notes popover */
|
|
291
|
+
.note-popover {
|
|
292
|
+
background: rgba(17,17,17,0.94);
|
|
293
|
+
border: 1px solid rgba(255,255,255,0.10);
|
|
294
|
+
border-radius: 14px; padding: 12px;
|
|
295
|
+
width: min(340px, calc(100vw - 32px));
|
|
296
|
+
box-shadow: 0 18px 40px rgba(0,0,0,0.36);
|
|
297
|
+
display: flex; flex-direction: column; gap: 10px;
|
|
298
|
+
animation: pop-in 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
299
|
+
}
|
|
300
|
+
.note-popover[hidden] { display: none; }
|
|
301
|
+
@keyframes pop-in {
|
|
302
|
+
from { opacity: 0; transform: translateY(6px) scale(0.98); }
|
|
303
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
304
|
+
}
|
|
305
|
+
.note-head {
|
|
306
|
+
color: rgba(255,255,255,0.7); font-size: 12px;
|
|
307
|
+
font-weight: 500; letter-spacing: 0.02em;
|
|
308
|
+
}
|
|
309
|
+
.note-textarea {
|
|
310
|
+
width: 100%; box-sizing: border-box;
|
|
311
|
+
min-height: 80px; resize: vertical;
|
|
312
|
+
padding: 10px 11px;
|
|
313
|
+
background: rgba(0,0,0,0.35);
|
|
314
|
+
border: 1px solid rgba(255,255,255,0.10);
|
|
315
|
+
border-radius: 10px;
|
|
316
|
+
color: #fff; font: inherit; font-size: 13.5px;
|
|
317
|
+
line-height: 1.45;
|
|
318
|
+
transition: border-color 0.15s ease;
|
|
319
|
+
}
|
|
320
|
+
.note-textarea:focus { outline: none; border-color: rgba(255,255,255,0.32); }
|
|
321
|
+
.note-textarea::placeholder { color: rgba(255,255,255,0.42); }
|
|
322
|
+
.note-actions {
|
|
323
|
+
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
|
324
|
+
}
|
|
325
|
+
.note-actions .hint {
|
|
326
|
+
color: rgba(255,255,255,0.45); font-size: 11px;
|
|
327
|
+
}
|
|
328
|
+
.note-actions .group { display: inline-flex; gap: 6px; }
|
|
329
|
+
.note-actions .btn { padding: 6px 12px; font-size: 12.5px; min-height: 32px; }
|
|
330
|
+
.btn-primary { background: #fff !important; color: #111; }
|
|
331
|
+
.btn-primary:hover { background: rgba(255,255,255,0.85) !important; }
|
|
332
|
+
.btn-ghost { background: transparent; color: rgba(255,255,255,0.7); }
|
|
333
|
+
.btn-ghost:hover { background: rgba(255,255,255,0.10); color: #fff; }
|
|
334
|
+
|
|
335
|
+
/* Thanks overlay + end-of-test note */
|
|
207
336
|
.thanks {
|
|
208
337
|
position: fixed; inset: 0;
|
|
209
338
|
display: grid; place-items: center;
|
|
@@ -218,12 +347,14 @@ function buildIndicator(host, store, onFinish, onToggleTasks) {
|
|
|
218
347
|
}
|
|
219
348
|
.thanks-card {
|
|
220
349
|
background: #fff; color: #111;
|
|
221
|
-
border-radius:
|
|
222
|
-
max-width:
|
|
350
|
+
border-radius: 18px; padding: 28px 24px;
|
|
351
|
+
max-width: 420px; width: 100%;
|
|
223
352
|
box-shadow: 0 20px 50px rgba(0,0,0,0.25);
|
|
353
|
+
text-align: left;
|
|
224
354
|
}
|
|
225
|
-
.thanks
|
|
226
|
-
.thanks
|
|
355
|
+
.thanks-card .head { text-align: center; }
|
|
356
|
+
.thanks h2 { margin: 0 0 6px; font-size: 20px; }
|
|
357
|
+
.thanks .lede { margin: 0 0 18px; font-size: 14px; line-height: 1.45; color: #4b5563; text-align: center; }
|
|
227
358
|
.thanks .check {
|
|
228
359
|
width: 44px; height: 44px; border-radius: 50%;
|
|
229
360
|
background: #10b981; color: #fff;
|
|
@@ -231,6 +362,50 @@ function buildIndicator(host, store, onFinish, onToggleTasks) {
|
|
|
231
362
|
margin: 0 auto 12px;
|
|
232
363
|
font-size: 22px;
|
|
233
364
|
}
|
|
365
|
+
.thanks .end-label {
|
|
366
|
+
display: block; margin: 0 0 8px;
|
|
367
|
+
font-size: 13px; font-weight: 500; color: #374151;
|
|
368
|
+
}
|
|
369
|
+
.thanks .end-textarea {
|
|
370
|
+
width: 100%; box-sizing: border-box;
|
|
371
|
+
min-height: 96px; resize: vertical;
|
|
372
|
+
padding: 11px 12px;
|
|
373
|
+
background: #f9fafb;
|
|
374
|
+
border: 1px solid #e5e7eb;
|
|
375
|
+
border-radius: 10px;
|
|
376
|
+
font: inherit; font-size: 14px; line-height: 1.5;
|
|
377
|
+
color: #111;
|
|
378
|
+
transition: border-color 0.15s ease, background 0.15s ease;
|
|
379
|
+
}
|
|
380
|
+
.thanks .end-textarea:focus {
|
|
381
|
+
outline: none; border-color: #111; background: #fff;
|
|
382
|
+
}
|
|
383
|
+
.thanks .end-textarea::placeholder { color: #9ca3af; }
|
|
384
|
+
.thanks .end-actions {
|
|
385
|
+
display: flex; gap: 10px; margin-top: 14px;
|
|
386
|
+
}
|
|
387
|
+
.thanks .end-actions button {
|
|
388
|
+
flex: 1;
|
|
389
|
+
appearance: none; border: 1px solid #e5e7eb;
|
|
390
|
+
background: #fff; color: #111;
|
|
391
|
+
padding: 11px 14px; border-radius: 10px;
|
|
392
|
+
font: inherit; font-weight: 600; font-size: 14px;
|
|
393
|
+
cursor: pointer;
|
|
394
|
+
transition: background 0.15s ease, border-color 0.15s ease;
|
|
395
|
+
}
|
|
396
|
+
.thanks .end-actions button:hover { background: #f3f4f6; }
|
|
397
|
+
.thanks .end-actions button.primary {
|
|
398
|
+
background: #111; color: #fff; border-color: #111;
|
|
399
|
+
}
|
|
400
|
+
.thanks .end-actions button.primary:hover { background: #1f2937; border-color: #1f2937; }
|
|
401
|
+
.thanks .end-actions button:focus-visible { outline: 2px solid #111; outline-offset: 2px; }
|
|
402
|
+
.thanks .end-hint {
|
|
403
|
+
margin: 10px 0 0; font-size: 11.5px; color: #9ca3af; text-align: center;
|
|
404
|
+
}
|
|
405
|
+
.thanks .end-sent {
|
|
406
|
+
margin-top: 14px; text-align: center; color: #4b5563; font-size: 13px;
|
|
407
|
+
}
|
|
408
|
+
|
|
234
409
|
@keyframes pulse {
|
|
235
410
|
0% { box-shadow: 0 0 0 0 rgba(239,68,68,0.55); }
|
|
236
411
|
70% { box-shadow: 0 0 0 10px rgba(239,68,68,0); }
|
|
@@ -238,6 +413,7 @@ function buildIndicator(host, store, onFinish, onToggleTasks) {
|
|
|
238
413
|
}
|
|
239
414
|
@media (prefers-reduced-motion: reduce) {
|
|
240
415
|
.dot { animation: none; }
|
|
416
|
+
.toast, .note-popover { animation: none; }
|
|
241
417
|
}
|
|
242
418
|
`;
|
|
243
419
|
const anchor = document.createElement("div");
|
|
@@ -245,34 +421,66 @@ function buildIndicator(host, store, onFinish, onToggleTasks) {
|
|
|
245
421
|
const panel = document.createElement("div");
|
|
246
422
|
panel.className = "panel";
|
|
247
423
|
panel.hidden = true;
|
|
424
|
+
const toastSlot = document.createElement("div");
|
|
425
|
+
toastSlot.className = "toast-slot";
|
|
426
|
+
const notePopover = document.createElement("div");
|
|
427
|
+
notePopover.className = "note-popover";
|
|
428
|
+
notePopover.hidden = true;
|
|
248
429
|
const bar = document.createElement("div");
|
|
249
430
|
bar.className = "bar";
|
|
250
431
|
bar.setAttribute("role", "status");
|
|
251
432
|
bar.setAttribute("aria-live", "polite");
|
|
433
|
+
const micBtn = document.createElement("button");
|
|
434
|
+
micBtn.type = "button";
|
|
435
|
+
micBtn.className = "mic";
|
|
436
|
+
micBtn.setAttribute("data-mic-state", "recording");
|
|
437
|
+
micBtn.setAttribute("aria-pressed", "false");
|
|
438
|
+
micBtn.setAttribute("aria-label", "Mute microphone");
|
|
252
439
|
const dot = document.createElement("span");
|
|
253
440
|
dot.className = "dot";
|
|
254
441
|
dot.setAttribute("data-state", store.indicatorState);
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
442
|
+
const micIcon = document.createElement("span");
|
|
443
|
+
micIcon.className = "mic-icon";
|
|
444
|
+
micIcon.innerHTML = MIC_ICON_SVG;
|
|
445
|
+
micIcon.setAttribute("aria-hidden", "true");
|
|
446
|
+
const micLabel = document.createElement("span");
|
|
447
|
+
micLabel.className = "mic-label";
|
|
448
|
+
micLabel.textContent = "Recording";
|
|
449
|
+
micBtn.appendChild(dot);
|
|
450
|
+
micBtn.appendChild(micIcon);
|
|
451
|
+
micBtn.appendChild(micLabel);
|
|
452
|
+
micBtn.addEventListener("click", callbacks.onToggleMute);
|
|
453
|
+
bar.appendChild(micBtn);
|
|
454
|
+
const noteBtn = document.createElement("button");
|
|
455
|
+
noteBtn.type = "button";
|
|
456
|
+
noteBtn.className = "note-btn";
|
|
457
|
+
noteBtn.setAttribute("aria-label", "Add a timestamped note");
|
|
458
|
+
noteBtn.setAttribute("aria-expanded", "false");
|
|
459
|
+
noteBtn.setAttribute("data-has-notes", "false");
|
|
460
|
+
noteBtn.innerHTML = `<span class="note-icon" aria-hidden="true">${NOTE_ICON_SVG}</span><span class="note-count" hidden></span>`;
|
|
461
|
+
noteBtn.addEventListener("click", callbacks.onOpenNote);
|
|
462
|
+
bar.appendChild(noteBtn);
|
|
258
463
|
const spacer = document.createElement("span");
|
|
259
464
|
spacer.className = "spacer";
|
|
260
|
-
bar.appendChild(dot);
|
|
261
|
-
bar.appendChild(label);
|
|
262
465
|
bar.appendChild(spacer);
|
|
263
466
|
const btn = document.createElement("button");
|
|
264
467
|
btn.type = "button";
|
|
265
468
|
btn.className = "btn finish-btn";
|
|
266
469
|
btn.textContent = "Finish";
|
|
267
|
-
btn.addEventListener("click", onFinish);
|
|
470
|
+
btn.addEventListener("click", callbacks.onFinish);
|
|
268
471
|
bar.appendChild(btn);
|
|
269
|
-
if (store.tasks.length > 0) installTasksToggle(bar, btn, store, onToggleTasks);
|
|
472
|
+
if (store.tasks.length > 0) installTasksToggle(bar, btn, store, callbacks.onToggleTasks);
|
|
270
473
|
anchor.appendChild(panel);
|
|
474
|
+
anchor.appendChild(toastSlot);
|
|
475
|
+
anchor.appendChild(notePopover);
|
|
271
476
|
anchor.appendChild(bar);
|
|
272
477
|
root.appendChild(style);
|
|
273
478
|
root.appendChild(anchor);
|
|
274
479
|
return root;
|
|
275
480
|
}
|
|
481
|
+
var MIC_ICON_SVG = `<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" width="13" height="13"><path d="M8 1.5a2 2 0 0 0-2 2v4a2 2 0 1 0 4 0v-4a2 2 0 0 0-2-2Z" fill="currentColor"/><path d="M4 7.5a4 4 0 0 0 8 0M8 11.5v3M5.5 14.5h5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>`;
|
|
482
|
+
var MIC_MUTED_ICON_SVG = `<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" width="13" height="13"><path d="M8 1.5a2 2 0 0 0-2 2v3.2L10 11V3.5a2 2 0 0 0-2-2Z" fill="currentColor"/><path d="M4 7.5a4 4 0 0 0 6.5 3.12M12 7.5a4 4 0 0 1-.3 1.5M8 11.5v3M5.5 14.5h5M2 2l12 12" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>`;
|
|
483
|
+
var NOTE_ICON_SVG = `<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path d="M3 3.5A1.5 1.5 0 0 1 4.5 2h7A1.5 1.5 0 0 1 13 3.5V10a1.5 1.5 0 0 1-1.5 1.5H7L4 14v-2.5h-.5A1.5 1.5 0 0 1 2 10V3.5A1.5 1.5 0 0 1 3.5 3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
276
484
|
function installTasksToggle(bar, finishBtn, store, onToggleTasks) {
|
|
277
485
|
const tasksBtn = document.createElement("button");
|
|
278
486
|
tasksBtn.type = "button";
|
|
@@ -315,53 +523,270 @@ function writeTasksPanelOpen(open) {
|
|
|
315
523
|
} catch {
|
|
316
524
|
}
|
|
317
525
|
}
|
|
526
|
+
function micChipState(store) {
|
|
527
|
+
if (store.indicatorState === "finishing" || store.indicatorState === "done" || store.indicatorState === "error") {
|
|
528
|
+
return "inactive";
|
|
529
|
+
}
|
|
530
|
+
if (!store.hasMicPermission) return "none";
|
|
531
|
+
return store.muted ? "muted" : "recording";
|
|
532
|
+
}
|
|
318
533
|
function renderIndicatorState(store) {
|
|
319
534
|
const root = store.indicatorRoot;
|
|
320
535
|
if (!root) return;
|
|
321
536
|
const dot = root.querySelector(".dot");
|
|
322
|
-
const
|
|
537
|
+
const mic = root.querySelector(".mic");
|
|
538
|
+
const micIcon = root.querySelector(".mic-icon");
|
|
539
|
+
const micLabel = root.querySelector(".mic-label");
|
|
323
540
|
const btn = root.querySelector(".finish-btn");
|
|
324
|
-
if (!(dot instanceof HTMLElement) || !(
|
|
541
|
+
if (!(dot instanceof HTMLElement) || !mic || !(micIcon instanceof HTMLElement) || !(micLabel instanceof HTMLElement) || !btn) return;
|
|
325
542
|
dot.setAttribute("data-state", store.indicatorState);
|
|
543
|
+
const chipState = micChipState(store);
|
|
544
|
+
mic.setAttribute("data-mic-state", chipState === "inactive" ? "none" : chipState);
|
|
326
545
|
switch (store.indicatorState) {
|
|
327
546
|
case "recording":
|
|
328
|
-
label.textContent = "Recording";
|
|
329
|
-
btn.textContent = "Finish";
|
|
330
|
-
btn.disabled = false;
|
|
331
|
-
break;
|
|
332
547
|
case "no-audio":
|
|
333
|
-
label.textContent = "No mic, replay only";
|
|
334
548
|
btn.textContent = "Finish";
|
|
335
549
|
btn.disabled = false;
|
|
336
550
|
break;
|
|
337
551
|
case "finishing":
|
|
338
|
-
label.textContent = "Saving";
|
|
339
552
|
btn.textContent = "Saving";
|
|
340
553
|
btn.disabled = true;
|
|
341
554
|
break;
|
|
342
555
|
case "done":
|
|
343
|
-
label.textContent = "Saved";
|
|
344
556
|
btn.textContent = "Done";
|
|
345
557
|
btn.disabled = true;
|
|
346
558
|
break;
|
|
347
559
|
case "error":
|
|
348
|
-
label.textContent = "Save failed";
|
|
349
560
|
btn.textContent = "Retry";
|
|
350
561
|
btn.disabled = false;
|
|
351
562
|
break;
|
|
352
563
|
}
|
|
564
|
+
switch (chipState) {
|
|
565
|
+
case "recording":
|
|
566
|
+
micIcon.innerHTML = MIC_ICON_SVG;
|
|
567
|
+
micLabel.textContent = "Recording";
|
|
568
|
+
mic.setAttribute("aria-label", "Mute microphone");
|
|
569
|
+
mic.setAttribute("aria-pressed", "false");
|
|
570
|
+
mic.removeAttribute("tabindex");
|
|
571
|
+
break;
|
|
572
|
+
case "muted":
|
|
573
|
+
micIcon.innerHTML = MIC_MUTED_ICON_SVG;
|
|
574
|
+
micLabel.textContent = "Muted";
|
|
575
|
+
mic.setAttribute("aria-label", "Unmute microphone");
|
|
576
|
+
mic.setAttribute("aria-pressed", "true");
|
|
577
|
+
mic.removeAttribute("tabindex");
|
|
578
|
+
break;
|
|
579
|
+
case "none":
|
|
580
|
+
micIcon.innerHTML = MIC_MUTED_ICON_SVG;
|
|
581
|
+
micLabel.textContent = "No mic, replay only";
|
|
582
|
+
mic.setAttribute("aria-label", "Microphone not granted, replay only");
|
|
583
|
+
mic.setAttribute("aria-pressed", "false");
|
|
584
|
+
mic.setAttribute("tabindex", "-1");
|
|
585
|
+
break;
|
|
586
|
+
case "inactive":
|
|
587
|
+
micIcon.innerHTML = MIC_ICON_SVG;
|
|
588
|
+
micLabel.textContent = store.indicatorState === "finishing" ? "Saving" : store.indicatorState === "done" ? "Saved" : "Save failed";
|
|
589
|
+
mic.setAttribute("aria-label", "Recording stopped");
|
|
590
|
+
mic.setAttribute("aria-pressed", "false");
|
|
591
|
+
mic.setAttribute("tabindex", "-1");
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function renderNotesCount(store) {
|
|
596
|
+
const root = store.indicatorRoot;
|
|
597
|
+
if (!root) return;
|
|
598
|
+
const noteBtn = root.querySelector(".note-btn");
|
|
599
|
+
const count = root.querySelector(".note-count");
|
|
600
|
+
if (!(noteBtn instanceof HTMLElement) || !(count instanceof HTMLElement)) return;
|
|
601
|
+
const n = store.notes.length;
|
|
602
|
+
noteBtn.setAttribute("data-has-notes", n > 0 ? "true" : "false");
|
|
603
|
+
if (n > 0) {
|
|
604
|
+
count.textContent = String(n);
|
|
605
|
+
count.hidden = false;
|
|
606
|
+
noteBtn.setAttribute("aria-label", `Add a timestamped note (${n} so far)`);
|
|
607
|
+
} else {
|
|
608
|
+
count.textContent = "";
|
|
609
|
+
count.hidden = true;
|
|
610
|
+
noteBtn.setAttribute("aria-label", "Add a timestamped note");
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function showMuteToast(store) {
|
|
614
|
+
if (store.muteToastShown) return;
|
|
615
|
+
store.muteToastShown = true;
|
|
616
|
+
const root = store.indicatorRoot;
|
|
617
|
+
if (!root) return;
|
|
618
|
+
const slot = root.querySelector(".toast-slot");
|
|
619
|
+
if (!(slot instanceof HTMLElement)) return;
|
|
620
|
+
slot.innerHTML = "";
|
|
621
|
+
const toast = document.createElement("div");
|
|
622
|
+
toast.className = "toast";
|
|
623
|
+
toast.setAttribute("role", "status");
|
|
624
|
+
toast.innerHTML = `<strong>Mic off.</strong> Screen is still recording. Tap to unmute.`;
|
|
625
|
+
slot.appendChild(toast);
|
|
626
|
+
window.setTimeout(() => {
|
|
627
|
+
toast.setAttribute("data-leaving", "true");
|
|
628
|
+
window.setTimeout(() => {
|
|
629
|
+
toast.remove();
|
|
630
|
+
}, 260);
|
|
631
|
+
}, 3e3);
|
|
632
|
+
}
|
|
633
|
+
function openNotePopover(store, onSave, onCancel) {
|
|
634
|
+
const root = store.indicatorRoot;
|
|
635
|
+
if (!root) return;
|
|
636
|
+
const pop = root.querySelector(".note-popover");
|
|
637
|
+
const noteBtn = root.querySelector(".note-btn");
|
|
638
|
+
if (!(pop instanceof HTMLElement) || !(noteBtn instanceof HTMLElement)) return;
|
|
639
|
+
store.notesPopoverOpen = true;
|
|
640
|
+
store.notePopoverAtMs = Date.now() - store.startedAt;
|
|
641
|
+
noteBtn.setAttribute("aria-expanded", "true");
|
|
642
|
+
pop.innerHTML = "";
|
|
643
|
+
const head = document.createElement("div");
|
|
644
|
+
head.className = "note-head";
|
|
645
|
+
head.innerHTML = `<span>Add a note</span>`;
|
|
646
|
+
const form = document.createElement("form");
|
|
647
|
+
form.style.cssText = "display:flex;flex-direction:column;gap:10px;margin:0;";
|
|
648
|
+
form.noValidate = true;
|
|
649
|
+
const ta = document.createElement("textarea");
|
|
650
|
+
ta.className = "note-textarea";
|
|
651
|
+
ta.placeholder = "What just happened? Confusing? Surprising? Broken?";
|
|
652
|
+
ta.rows = 3;
|
|
653
|
+
ta.setAttribute("aria-label", "Note text");
|
|
654
|
+
const actions = document.createElement("div");
|
|
655
|
+
actions.className = "note-actions";
|
|
656
|
+
const hint = document.createElement("span");
|
|
657
|
+
hint.className = "hint";
|
|
658
|
+
hint.innerHTML = '<kbd style="font-family:inherit">Cmd</kbd>+Enter to save';
|
|
659
|
+
const group = document.createElement("div");
|
|
660
|
+
group.className = "group";
|
|
661
|
+
const cancelBtn = document.createElement("button");
|
|
662
|
+
cancelBtn.type = "button";
|
|
663
|
+
cancelBtn.className = "btn btn-ghost";
|
|
664
|
+
cancelBtn.textContent = "Cancel";
|
|
665
|
+
const saveBtn = document.createElement("button");
|
|
666
|
+
saveBtn.type = "submit";
|
|
667
|
+
saveBtn.className = "btn btn-primary";
|
|
668
|
+
saveBtn.textContent = "Save";
|
|
669
|
+
group.appendChild(cancelBtn);
|
|
670
|
+
group.appendChild(saveBtn);
|
|
671
|
+
actions.appendChild(hint);
|
|
672
|
+
actions.appendChild(group);
|
|
673
|
+
form.appendChild(ta);
|
|
674
|
+
form.appendChild(actions);
|
|
675
|
+
pop.appendChild(head);
|
|
676
|
+
pop.appendChild(form);
|
|
677
|
+
pop.hidden = false;
|
|
678
|
+
const submit = () => {
|
|
679
|
+
const text = ta.value.trim();
|
|
680
|
+
if (!text) {
|
|
681
|
+
onCancel();
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
onSave(text);
|
|
685
|
+
};
|
|
686
|
+
form.addEventListener("submit", (e) => {
|
|
687
|
+
e.preventDefault();
|
|
688
|
+
submit();
|
|
689
|
+
});
|
|
690
|
+
cancelBtn.addEventListener("click", () => onCancel());
|
|
691
|
+
ta.addEventListener("keydown", (e) => {
|
|
692
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
693
|
+
e.preventDefault();
|
|
694
|
+
submit();
|
|
695
|
+
} else if (e.key === "Escape") {
|
|
696
|
+
e.preventDefault();
|
|
697
|
+
onCancel();
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
window.requestAnimationFrame(() => {
|
|
701
|
+
ta.focus({ preventScroll: true });
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
function closeNotePopover(store) {
|
|
705
|
+
const root = store.indicatorRoot;
|
|
706
|
+
if (!root) return;
|
|
707
|
+
const pop = root.querySelector(".note-popover");
|
|
708
|
+
const noteBtn = root.querySelector(".note-btn");
|
|
709
|
+
if (pop instanceof HTMLElement) {
|
|
710
|
+
pop.hidden = true;
|
|
711
|
+
pop.innerHTML = "";
|
|
712
|
+
}
|
|
713
|
+
if (noteBtn instanceof HTMLElement) noteBtn.setAttribute("aria-expanded", "false");
|
|
714
|
+
store.notesPopoverOpen = false;
|
|
715
|
+
store.notePopoverAtMs = null;
|
|
353
716
|
}
|
|
354
|
-
function showThanksScreen(root) {
|
|
717
|
+
function showThanksScreen(root, opts) {
|
|
355
718
|
const overlay = document.createElement("div");
|
|
356
719
|
overlay.className = "thanks";
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
720
|
+
const card = document.createElement("div");
|
|
721
|
+
card.className = "thanks-card";
|
|
722
|
+
const head = document.createElement("div");
|
|
723
|
+
head.className = "head";
|
|
724
|
+
head.innerHTML = `
|
|
725
|
+
<div class="check" aria-hidden="true">✓</div>
|
|
726
|
+
<h2>Thanks for testing</h2>
|
|
727
|
+
<p class="lede">Your session was saved. One last thing if you have a moment.</p>
|
|
728
|
+
`;
|
|
729
|
+
const form = document.createElement("form");
|
|
730
|
+
form.noValidate = true;
|
|
731
|
+
form.innerHTML = `
|
|
732
|
+
<label class="end-label" for="usero-end-note">Anything you would add?</label>
|
|
733
|
+
<textarea
|
|
734
|
+
id="usero-end-note"
|
|
735
|
+
class="end-textarea"
|
|
736
|
+
rows="4"
|
|
737
|
+
placeholder="Confusing bits, things you liked, what you'd change..."
|
|
738
|
+
></textarea>
|
|
739
|
+
<div class="end-actions">
|
|
740
|
+
<button type="button" class="skip">Skip</button>
|
|
741
|
+
<button type="submit" class="primary">Send</button>
|
|
362
742
|
</div>
|
|
743
|
+
<p class="end-hint">Cmd or Ctrl plus Enter to send. Either button is fine.</p>
|
|
363
744
|
`;
|
|
745
|
+
card.appendChild(head);
|
|
746
|
+
card.appendChild(form);
|
|
747
|
+
overlay.appendChild(card);
|
|
364
748
|
root.appendChild(overlay);
|
|
749
|
+
const ta = form.querySelector("#usero-end-note");
|
|
750
|
+
const skipBtn = form.querySelector("button.skip");
|
|
751
|
+
if (!ta || !skipBtn) return;
|
|
752
|
+
const swapToSent = (message) => {
|
|
753
|
+
form.remove();
|
|
754
|
+
const sent = document.createElement("p");
|
|
755
|
+
sent.className = "end-sent";
|
|
756
|
+
sent.textContent = message;
|
|
757
|
+
card.appendChild(sent);
|
|
758
|
+
};
|
|
759
|
+
const submit = async () => {
|
|
760
|
+
const text = ta.value.trim();
|
|
761
|
+
ta.disabled = true;
|
|
762
|
+
skipBtn.disabled = true;
|
|
763
|
+
const submitBtn = form.querySelector("button.primary");
|
|
764
|
+
if (submitBtn) submitBtn.disabled = true;
|
|
765
|
+
if (text) {
|
|
766
|
+
await opts.onSubmitNote(text);
|
|
767
|
+
swapToSent("Thanks. You can close this tab.");
|
|
768
|
+
} else {
|
|
769
|
+
opts.onSkip();
|
|
770
|
+
swapToSent("All good. You can close this tab.");
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
form.addEventListener("submit", (e) => {
|
|
774
|
+
e.preventDefault();
|
|
775
|
+
void submit();
|
|
776
|
+
});
|
|
777
|
+
skipBtn.addEventListener("click", () => {
|
|
778
|
+
ta.value = "";
|
|
779
|
+
void submit();
|
|
780
|
+
});
|
|
781
|
+
ta.addEventListener("keydown", (e) => {
|
|
782
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
783
|
+
e.preventDefault();
|
|
784
|
+
void submit();
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
window.requestAnimationFrame(() => {
|
|
788
|
+
ta.focus({ preventScroll: true });
|
|
789
|
+
});
|
|
365
790
|
}
|
|
366
791
|
function parseTasks(raw) {
|
|
367
792
|
if (!Array.isArray(raw)) return [];
|
|
@@ -388,12 +813,20 @@ async function createSession(apiUrl, slug, testerName) {
|
|
|
388
813
|
return null;
|
|
389
814
|
}
|
|
390
815
|
}
|
|
391
|
-
async function finaliseSession(apiUrl, sessionId, durationSeconds) {
|
|
816
|
+
async function finaliseSession(apiUrl, sessionId, durationSeconds, extras = {}) {
|
|
392
817
|
try {
|
|
818
|
+
const body = {
|
|
819
|
+
durationSeconds: Math.max(0, Math.round(durationSeconds))
|
|
820
|
+
};
|
|
821
|
+
if (extras.mutedSegments && extras.mutedSegments.length > 0) {
|
|
822
|
+
body.mutedSegments = extras.mutedSegments;
|
|
823
|
+
}
|
|
824
|
+
const trimmedEndNote = extras.endNote?.trim();
|
|
825
|
+
if (trimmedEndNote) body.endNote = trimmedEndNote;
|
|
393
826
|
const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/user-test-sessions/${encodeURIComponent(sessionId)}/finalise`, {
|
|
394
827
|
method: "POST",
|
|
395
828
|
headers: { "Content-Type": "application/json" },
|
|
396
|
-
body: JSON.stringify(
|
|
829
|
+
body: JSON.stringify(body),
|
|
397
830
|
keepalive: true
|
|
398
831
|
});
|
|
399
832
|
return res.ok;
|
|
@@ -401,6 +834,24 @@ async function finaliseSession(apiUrl, sessionId, durationSeconds) {
|
|
|
401
834
|
return false;
|
|
402
835
|
}
|
|
403
836
|
}
|
|
837
|
+
async function postNote(apiUrl, sessionId, atMs, text, logger) {
|
|
838
|
+
try {
|
|
839
|
+
const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/user-test-sessions/${encodeURIComponent(sessionId)}/notes`, {
|
|
840
|
+
method: "POST",
|
|
841
|
+
headers: { "Content-Type": "application/json" },
|
|
842
|
+
body: JSON.stringify({ atMs: Math.max(0, Math.round(atMs)), text }),
|
|
843
|
+
keepalive: true
|
|
844
|
+
});
|
|
845
|
+
if (!res.ok) {
|
|
846
|
+
logger.warn(`note POST rejected with ${res.status}`);
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
return true;
|
|
850
|
+
} catch (err) {
|
|
851
|
+
logger.warn("note POST failed", err);
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
404
855
|
async function flushPendingFromIdb(store, ctx) {
|
|
405
856
|
if (!store.sessionId) return;
|
|
406
857
|
const pending = await idbListChunks(store.sessionId);
|
|
@@ -449,6 +900,7 @@ async function startRecording(store, ctx) {
|
|
|
449
900
|
return;
|
|
450
901
|
}
|
|
451
902
|
store.stream = stream;
|
|
903
|
+
store.hasMicPermission = true;
|
|
452
904
|
const mimeType = pickMimeType();
|
|
453
905
|
let recorder;
|
|
454
906
|
try {
|
|
@@ -472,6 +924,34 @@ async function startRecording(store, ctx) {
|
|
|
472
924
|
});
|
|
473
925
|
recorder.start(store.options.chunkSeconds * 1e3);
|
|
474
926
|
}
|
|
927
|
+
function toggleMute(store) {
|
|
928
|
+
if (!store.stream || !store.hasMicPermission) return false;
|
|
929
|
+
const tracks = store.stream.getAudioTracks();
|
|
930
|
+
if (tracks.length === 0) return false;
|
|
931
|
+
const nowMs = Date.now() - store.startedAt;
|
|
932
|
+
if (!store.muted) {
|
|
933
|
+
for (const t of tracks) t.enabled = false;
|
|
934
|
+
store.muted = true;
|
|
935
|
+
store.mutedSinceMs = nowMs;
|
|
936
|
+
} else {
|
|
937
|
+
const startMs = store.mutedSinceMs ?? nowMs;
|
|
938
|
+
if (nowMs > startMs) {
|
|
939
|
+
store.mutedSegments.push({ startMs, endMs: nowMs });
|
|
940
|
+
}
|
|
941
|
+
store.mutedSinceMs = null;
|
|
942
|
+
store.muted = false;
|
|
943
|
+
for (const t of tracks) t.enabled = true;
|
|
944
|
+
}
|
|
945
|
+
return true;
|
|
946
|
+
}
|
|
947
|
+
function flushMuteIfActive(store) {
|
|
948
|
+
if (!store.muted || store.mutedSinceMs === null) return;
|
|
949
|
+
const nowMs = Date.now() - store.startedAt;
|
|
950
|
+
if (nowMs > store.mutedSinceMs) {
|
|
951
|
+
store.mutedSegments.push({ startMs: store.mutedSinceMs, endMs: nowMs });
|
|
952
|
+
}
|
|
953
|
+
store.mutedSinceMs = null;
|
|
954
|
+
}
|
|
475
955
|
function stopRecording(store) {
|
|
476
956
|
const recorder = store.recorder;
|
|
477
957
|
if (recorder && recorder.state !== "inactive") {
|
|
@@ -494,20 +974,34 @@ async function finishFlow(store, ctx, opts) {
|
|
|
494
974
|
if (store.cancelled) return;
|
|
495
975
|
if (store.indicatorState === "finishing" || store.indicatorState === "done") return;
|
|
496
976
|
store.indicatorState = "finishing";
|
|
977
|
+
flushMuteIfActive(store);
|
|
497
978
|
renderIndicatorState(store);
|
|
498
979
|
stopRecording(store);
|
|
499
980
|
await store.uploadQueue;
|
|
500
981
|
await flushPendingFromIdb(store, ctx);
|
|
501
982
|
const durationSeconds = (Date.now() - store.startedAt) / 1e3;
|
|
502
983
|
if (store.sessionId) {
|
|
503
|
-
const ok = await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds
|
|
984
|
+
const ok = await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds, {
|
|
985
|
+
mutedSegments: store.mutedSegments
|
|
986
|
+
});
|
|
504
987
|
store.indicatorState = ok ? "done" : "error";
|
|
505
988
|
} else {
|
|
506
989
|
store.indicatorState = "error";
|
|
507
990
|
}
|
|
508
991
|
renderIndicatorState(store);
|
|
509
992
|
if (opts.showThanks && store.indicatorRoot && store.indicatorState === "done") {
|
|
510
|
-
showThanksScreen(store.indicatorRoot
|
|
993
|
+
showThanksScreen(store.indicatorRoot, {
|
|
994
|
+
onSubmitNote: async (text) => {
|
|
995
|
+
if (!store.sessionId) return;
|
|
996
|
+
store.endNote = text;
|
|
997
|
+
await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds, {
|
|
998
|
+
mutedSegments: store.mutedSegments,
|
|
999
|
+
endNote: text
|
|
1000
|
+
});
|
|
1001
|
+
},
|
|
1002
|
+
onSkip: () => {
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
511
1005
|
}
|
|
512
1006
|
}
|
|
513
1007
|
function userTest(options = {}) {
|
|
@@ -544,7 +1038,16 @@ function userTest(options = {}) {
|
|
|
544
1038
|
tasks: [],
|
|
545
1039
|
tasksPanelOpen: readTasksPanelOpen(),
|
|
546
1040
|
outsidePointerHandler: null,
|
|
547
|
-
keydownHandler: null
|
|
1041
|
+
keydownHandler: null,
|
|
1042
|
+
hasMicPermission: false,
|
|
1043
|
+
muted: false,
|
|
1044
|
+
mutedSinceMs: null,
|
|
1045
|
+
mutedSegments: [],
|
|
1046
|
+
muteToastShown: false,
|
|
1047
|
+
notes: [],
|
|
1048
|
+
notesPopoverOpen: false,
|
|
1049
|
+
notePopoverAtMs: null,
|
|
1050
|
+
endNote: ""
|
|
548
1051
|
};
|
|
549
1052
|
ctx.setStore(store);
|
|
550
1053
|
const onFinish = () => {
|
|
@@ -557,23 +1060,59 @@ function userTest(options = {}) {
|
|
|
557
1060
|
renderTasksPanel(store);
|
|
558
1061
|
};
|
|
559
1062
|
const onToggleTasks = () => setPanelOpen(!store.tasksPanelOpen);
|
|
1063
|
+
const onToggleMute = () => {
|
|
1064
|
+
if (!store.hasMicPermission) return;
|
|
1065
|
+
const ok = toggleMute(store);
|
|
1066
|
+
if (!ok) return;
|
|
1067
|
+
if (store.muted) showMuteToast(store);
|
|
1068
|
+
renderIndicatorState(store);
|
|
1069
|
+
};
|
|
1070
|
+
const closeNote = () => closeNotePopover(store);
|
|
1071
|
+
const onOpenNote = () => {
|
|
1072
|
+
if (store.notesPopoverOpen) {
|
|
1073
|
+
closeNote();
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
openNotePopover(
|
|
1077
|
+
store,
|
|
1078
|
+
(text) => {
|
|
1079
|
+
const atMs = store.notePopoverAtMs ?? Math.max(0, Date.now() - store.startedAt);
|
|
1080
|
+
store.notes.push({ atMs, text });
|
|
1081
|
+
closeNote();
|
|
1082
|
+
renderNotesCount(store);
|
|
1083
|
+
if (store.sessionId) {
|
|
1084
|
+
void postNote(store.options.apiUrl, store.sessionId, atMs, text, ctx.logger);
|
|
1085
|
+
}
|
|
1086
|
+
},
|
|
1087
|
+
() => closeNote()
|
|
1088
|
+
);
|
|
1089
|
+
};
|
|
560
1090
|
if (!merged.hideIndicator) {
|
|
561
1091
|
const host = document.createElement("div");
|
|
562
1092
|
host.setAttribute("data-usero-user-test", "true");
|
|
563
1093
|
document.body.appendChild(host);
|
|
564
1094
|
store.indicator = host;
|
|
565
|
-
store.indicatorRoot = buildIndicator(host, store,
|
|
1095
|
+
store.indicatorRoot = buildIndicator(host, store, {
|
|
1096
|
+
onFinish,
|
|
1097
|
+
onToggleTasks,
|
|
1098
|
+
onToggleMute,
|
|
1099
|
+
onOpenNote
|
|
1100
|
+
});
|
|
566
1101
|
renderIndicatorState(store);
|
|
1102
|
+
renderNotesCount(store);
|
|
567
1103
|
}
|
|
568
1104
|
const outsidePointer = (event) => {
|
|
569
|
-
if (!store.tasksPanelOpen) return;
|
|
570
1105
|
const host = store.indicator;
|
|
571
1106
|
if (!host) return;
|
|
572
1107
|
const path = event.composedPath();
|
|
573
|
-
if (
|
|
1108
|
+
if (path.includes(host)) return;
|
|
1109
|
+
if (store.tasksPanelOpen) setPanelOpen(false);
|
|
1110
|
+
if (store.notesPopoverOpen) closeNote();
|
|
574
1111
|
};
|
|
575
1112
|
const onKeydown = (event) => {
|
|
576
|
-
if (event.key
|
|
1113
|
+
if (event.key !== "Escape") return;
|
|
1114
|
+
if (store.tasksPanelOpen) setPanelOpen(false);
|
|
1115
|
+
if (store.notesPopoverOpen) closeNote();
|
|
577
1116
|
};
|
|
578
1117
|
store.outsidePointerHandler = outsidePointer;
|
|
579
1118
|
store.keydownHandler = onKeydown;
|