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