@usero/sdk 1.0.2 → 1.1.1
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,295 @@ 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
|
+
}
|
|
353
612
|
}
|
|
354
|
-
function
|
|
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
|
+
const outer = window.setTimeout(() => {
|
|
627
|
+
if (!toast.isConnected) return;
|
|
628
|
+
toast.setAttribute("data-leaving", "true");
|
|
629
|
+
const inner = window.setTimeout(() => {
|
|
630
|
+
if (toast.isConnected) toast.remove();
|
|
631
|
+
}, 260);
|
|
632
|
+
store.muteToastTimers.push(inner);
|
|
633
|
+
}, 3e3);
|
|
634
|
+
store.muteToastTimers.push(outer);
|
|
635
|
+
}
|
|
636
|
+
function openNotePopover(store, onSave, onCancel) {
|
|
637
|
+
const root = store.indicatorRoot;
|
|
638
|
+
if (!root) return;
|
|
639
|
+
const pop = root.querySelector(".note-popover");
|
|
640
|
+
const noteBtn = root.querySelector(".note-btn");
|
|
641
|
+
if (!(pop instanceof HTMLElement) || !(noteBtn instanceof HTMLElement)) return;
|
|
642
|
+
store.notesPopoverOpen = true;
|
|
643
|
+
store.notePopoverAtMs = Date.now() - store.startedAt;
|
|
644
|
+
noteBtn.setAttribute("aria-expanded", "true");
|
|
645
|
+
pop.innerHTML = "";
|
|
646
|
+
const head = document.createElement("div");
|
|
647
|
+
head.className = "note-head";
|
|
648
|
+
head.innerHTML = `<span>Add a note</span>`;
|
|
649
|
+
const form = document.createElement("form");
|
|
650
|
+
form.style.cssText = "display:flex;flex-direction:column;gap:10px;margin:0;";
|
|
651
|
+
form.noValidate = true;
|
|
652
|
+
const ta = document.createElement("textarea");
|
|
653
|
+
ta.className = "note-textarea";
|
|
654
|
+
ta.placeholder = "What just happened? Confusing? Surprising? Broken?";
|
|
655
|
+
ta.rows = 3;
|
|
656
|
+
ta.setAttribute("aria-label", "Note text");
|
|
657
|
+
const actions = document.createElement("div");
|
|
658
|
+
actions.className = "note-actions";
|
|
659
|
+
const hint = document.createElement("span");
|
|
660
|
+
hint.className = "hint";
|
|
661
|
+
hint.innerHTML = '<kbd style="font-family:inherit">Cmd</kbd>+Enter to save';
|
|
662
|
+
const group = document.createElement("div");
|
|
663
|
+
group.className = "group";
|
|
664
|
+
const cancelBtn = document.createElement("button");
|
|
665
|
+
cancelBtn.type = "button";
|
|
666
|
+
cancelBtn.className = "btn btn-ghost";
|
|
667
|
+
cancelBtn.textContent = "Cancel";
|
|
668
|
+
const saveBtn = document.createElement("button");
|
|
669
|
+
saveBtn.type = "submit";
|
|
670
|
+
saveBtn.className = "btn btn-primary";
|
|
671
|
+
saveBtn.textContent = "Save";
|
|
672
|
+
group.appendChild(cancelBtn);
|
|
673
|
+
group.appendChild(saveBtn);
|
|
674
|
+
actions.appendChild(hint);
|
|
675
|
+
actions.appendChild(group);
|
|
676
|
+
form.appendChild(ta);
|
|
677
|
+
form.appendChild(actions);
|
|
678
|
+
pop.appendChild(head);
|
|
679
|
+
pop.appendChild(form);
|
|
680
|
+
pop.hidden = false;
|
|
681
|
+
const submit = () => {
|
|
682
|
+
const text = ta.value.trim();
|
|
683
|
+
if (!text) {
|
|
684
|
+
onCancel();
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
onSave(text);
|
|
688
|
+
};
|
|
689
|
+
form.addEventListener("submit", (e) => {
|
|
690
|
+
e.preventDefault();
|
|
691
|
+
submit();
|
|
692
|
+
});
|
|
693
|
+
cancelBtn.addEventListener("click", () => onCancel());
|
|
694
|
+
ta.addEventListener("keydown", (e) => {
|
|
695
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
696
|
+
e.preventDefault();
|
|
697
|
+
submit();
|
|
698
|
+
} else if (e.key === "Escape") {
|
|
699
|
+
e.preventDefault();
|
|
700
|
+
onCancel();
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
window.requestAnimationFrame(() => {
|
|
704
|
+
ta.focus({ preventScroll: true });
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
function closeNotePopover(store) {
|
|
708
|
+
const root = store.indicatorRoot;
|
|
709
|
+
if (!root) return;
|
|
710
|
+
const pop = root.querySelector(".note-popover");
|
|
711
|
+
const noteBtn = root.querySelector(".note-btn");
|
|
712
|
+
if (pop instanceof HTMLElement) {
|
|
713
|
+
pop.hidden = true;
|
|
714
|
+
pop.innerHTML = "";
|
|
715
|
+
}
|
|
716
|
+
if (noteBtn instanceof HTMLElement) noteBtn.setAttribute("aria-expanded", "false");
|
|
717
|
+
store.notesPopoverOpen = false;
|
|
718
|
+
store.notePopoverAtMs = null;
|
|
719
|
+
}
|
|
720
|
+
function showThanksScreen(root, opts) {
|
|
355
721
|
const overlay = document.createElement("div");
|
|
356
722
|
overlay.className = "thanks";
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
723
|
+
const card = document.createElement("div");
|
|
724
|
+
card.className = "thanks-card";
|
|
725
|
+
const head = document.createElement("div");
|
|
726
|
+
head.className = "head";
|
|
727
|
+
head.innerHTML = `
|
|
728
|
+
<div class="check" aria-hidden="true">✓</div>
|
|
729
|
+
<h2>Thanks for testing</h2>
|
|
730
|
+
<p class="lede">Your session was saved. One last thing if you have a moment.</p>
|
|
731
|
+
`;
|
|
732
|
+
const form = document.createElement("form");
|
|
733
|
+
form.noValidate = true;
|
|
734
|
+
form.innerHTML = `
|
|
735
|
+
<label class="end-label" for="usero-end-note">Anything you would add?</label>
|
|
736
|
+
<textarea
|
|
737
|
+
id="usero-end-note"
|
|
738
|
+
class="end-textarea"
|
|
739
|
+
rows="4"
|
|
740
|
+
placeholder="Confusing bits, things you liked, what you'd change..."
|
|
741
|
+
></textarea>
|
|
742
|
+
<div class="end-actions">
|
|
743
|
+
<button type="button" class="skip">Skip</button>
|
|
744
|
+
<button type="submit" class="primary">Send</button>
|
|
362
745
|
</div>
|
|
746
|
+
<p class="end-hint">Cmd or Ctrl plus Enter to send. Either button is fine.</p>
|
|
363
747
|
`;
|
|
748
|
+
card.appendChild(head);
|
|
749
|
+
card.appendChild(form);
|
|
750
|
+
overlay.appendChild(card);
|
|
364
751
|
root.appendChild(overlay);
|
|
752
|
+
const ta = form.querySelector("#usero-end-note");
|
|
753
|
+
const skipBtn = form.querySelector("button.skip");
|
|
754
|
+
if (!ta || !skipBtn) return;
|
|
755
|
+
const swapToSent = (message) => {
|
|
756
|
+
form.remove();
|
|
757
|
+
const sent = document.createElement("p");
|
|
758
|
+
sent.className = "end-sent";
|
|
759
|
+
sent.textContent = message;
|
|
760
|
+
card.appendChild(sent);
|
|
761
|
+
};
|
|
762
|
+
const ERROR_CLASS = "end-error";
|
|
763
|
+
const showError = (message) => {
|
|
764
|
+
const prior = form.querySelector(`.${ERROR_CLASS}`);
|
|
765
|
+
if (prior) prior.remove();
|
|
766
|
+
const err = document.createElement("p");
|
|
767
|
+
err.className = ERROR_CLASS;
|
|
768
|
+
err.textContent = message;
|
|
769
|
+
err.setAttribute("role", "alert");
|
|
770
|
+
err.style.cssText = "margin:10px 0 0;font-size:12.5px;color:#b91c1c;text-align:center;";
|
|
771
|
+
form.appendChild(err);
|
|
772
|
+
};
|
|
773
|
+
const submit = async () => {
|
|
774
|
+
const text = ta.value.trim();
|
|
775
|
+
ta.disabled = true;
|
|
776
|
+
skipBtn.disabled = true;
|
|
777
|
+
const submitBtn = form.querySelector("button.primary");
|
|
778
|
+
if (submitBtn) submitBtn.disabled = true;
|
|
779
|
+
if (text) {
|
|
780
|
+
try {
|
|
781
|
+
await Promise.race([
|
|
782
|
+
Promise.resolve(opts.onSubmitNote(text)),
|
|
783
|
+
new Promise((_, reject) => {
|
|
784
|
+
window.setTimeout(() => reject(new Error("timeout")), 3e4);
|
|
785
|
+
})
|
|
786
|
+
]);
|
|
787
|
+
swapToSent("Thanks. You can close this tab.");
|
|
788
|
+
} catch {
|
|
789
|
+
ta.disabled = false;
|
|
790
|
+
skipBtn.disabled = false;
|
|
791
|
+
if (submitBtn) submitBtn.disabled = false;
|
|
792
|
+
showError("Couldn't save your note. Try again?");
|
|
793
|
+
}
|
|
794
|
+
} else {
|
|
795
|
+
swapToSent("All good. You can close this tab.");
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
form.addEventListener("submit", (e) => {
|
|
799
|
+
e.preventDefault();
|
|
800
|
+
void submit();
|
|
801
|
+
});
|
|
802
|
+
skipBtn.addEventListener("click", () => {
|
|
803
|
+
ta.value = "";
|
|
804
|
+
void submit();
|
|
805
|
+
});
|
|
806
|
+
ta.addEventListener("keydown", (e) => {
|
|
807
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
808
|
+
e.preventDefault();
|
|
809
|
+
void submit();
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
window.requestAnimationFrame(() => {
|
|
813
|
+
ta.focus({ preventScroll: true });
|
|
814
|
+
});
|
|
365
815
|
}
|
|
366
816
|
function parseTasks(raw) {
|
|
367
817
|
if (!Array.isArray(raw)) return [];
|
|
@@ -388,12 +838,26 @@ async function createSession(apiUrl, slug, testerName) {
|
|
|
388
838
|
return null;
|
|
389
839
|
}
|
|
390
840
|
}
|
|
391
|
-
async function finaliseSession(apiUrl, sessionId, durationSeconds) {
|
|
841
|
+
async function finaliseSession(apiUrl, sessionId, durationSeconds, extras = {}) {
|
|
392
842
|
try {
|
|
843
|
+
const body = {
|
|
844
|
+
durationSeconds: Math.max(0, Math.round(durationSeconds))
|
|
845
|
+
};
|
|
846
|
+
if (extras.mutedSegments && extras.mutedSegments.length > 0) {
|
|
847
|
+
body.mutedSegments = extras.mutedSegments;
|
|
848
|
+
}
|
|
849
|
+
const trimmedEndNote = extras.endNote?.trim();
|
|
850
|
+
if (trimmedEndNote) body.endNote = trimmedEndNote;
|
|
851
|
+
if (extras.notes && extras.notes.length > 0) {
|
|
852
|
+
body.notes = extras.notes.slice(0, 200).map((n) => ({
|
|
853
|
+
atMs: Math.max(0, Math.round(n.atMs)),
|
|
854
|
+
text: n.text
|
|
855
|
+
}));
|
|
856
|
+
}
|
|
393
857
|
const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/user-test-sessions/${encodeURIComponent(sessionId)}/finalise`, {
|
|
394
858
|
method: "POST",
|
|
395
859
|
headers: { "Content-Type": "application/json" },
|
|
396
|
-
body: JSON.stringify(
|
|
860
|
+
body: JSON.stringify(body),
|
|
397
861
|
keepalive: true
|
|
398
862
|
});
|
|
399
863
|
return res.ok;
|
|
@@ -401,6 +865,36 @@ async function finaliseSession(apiUrl, sessionId, durationSeconds) {
|
|
|
401
865
|
return false;
|
|
402
866
|
}
|
|
403
867
|
}
|
|
868
|
+
async function postNoteOnce(apiUrl, sessionId, atMs, text, logger) {
|
|
869
|
+
try {
|
|
870
|
+
const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/user-test-sessions/${encodeURIComponent(sessionId)}/notes`, {
|
|
871
|
+
method: "POST",
|
|
872
|
+
headers: { "Content-Type": "application/json" },
|
|
873
|
+
body: JSON.stringify({ atMs: Math.max(0, Math.round(atMs)), text }),
|
|
874
|
+
keepalive: true
|
|
875
|
+
});
|
|
876
|
+
if (!res.ok) {
|
|
877
|
+
logger.warn(`note POST rejected with ${res.status}`);
|
|
878
|
+
return { ok: false, transient: res.status >= 500 || res.status === 408 || res.status === 429 };
|
|
879
|
+
}
|
|
880
|
+
let id;
|
|
881
|
+
try {
|
|
882
|
+
const json = await res.json();
|
|
883
|
+
if (typeof json.id === "string") id = json.id;
|
|
884
|
+
} catch {
|
|
885
|
+
}
|
|
886
|
+
return { ok: true, id, transient: false };
|
|
887
|
+
} catch (err) {
|
|
888
|
+
logger.warn("note POST failed", err);
|
|
889
|
+
return { ok: false, transient: true };
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
async function postNoteWithRetry(apiUrl, sessionId, atMs, text, logger) {
|
|
893
|
+
const first = await postNoteOnce(apiUrl, sessionId, atMs, text, logger);
|
|
894
|
+
if (first.ok || !first.transient) return first;
|
|
895
|
+
await new Promise((resolve) => setTimeout(resolve, 400 + Math.floor(Math.random() * 200)));
|
|
896
|
+
return postNoteOnce(apiUrl, sessionId, atMs, text, logger);
|
|
897
|
+
}
|
|
404
898
|
async function flushPendingFromIdb(store, ctx) {
|
|
405
899
|
if (!store.sessionId) return;
|
|
406
900
|
const pending = await idbListChunks(store.sessionId);
|
|
@@ -449,6 +943,7 @@ async function startRecording(store, ctx) {
|
|
|
449
943
|
return;
|
|
450
944
|
}
|
|
451
945
|
store.stream = stream;
|
|
946
|
+
store.hasMicPermission = true;
|
|
452
947
|
const mimeType = pickMimeType();
|
|
453
948
|
let recorder;
|
|
454
949
|
try {
|
|
@@ -472,6 +967,34 @@ async function startRecording(store, ctx) {
|
|
|
472
967
|
});
|
|
473
968
|
recorder.start(store.options.chunkSeconds * 1e3);
|
|
474
969
|
}
|
|
970
|
+
function toggleMute(store) {
|
|
971
|
+
if (!store.stream || !store.hasMicPermission) return false;
|
|
972
|
+
const tracks = store.stream.getAudioTracks();
|
|
973
|
+
if (tracks.length === 0) return false;
|
|
974
|
+
const nowMs = Date.now() - store.startedAt;
|
|
975
|
+
if (!store.muted) {
|
|
976
|
+
for (const t of tracks) t.enabled = false;
|
|
977
|
+
store.muted = true;
|
|
978
|
+
store.mutedSinceMs = nowMs;
|
|
979
|
+
} else {
|
|
980
|
+
const startMs = store.mutedSinceMs ?? nowMs;
|
|
981
|
+
if (nowMs > startMs) {
|
|
982
|
+
store.mutedSegments.push({ startMs, endMs: nowMs });
|
|
983
|
+
}
|
|
984
|
+
store.mutedSinceMs = null;
|
|
985
|
+
store.muted = false;
|
|
986
|
+
for (const t of tracks) t.enabled = true;
|
|
987
|
+
}
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
function flushMuteIfActive(store) {
|
|
991
|
+
if (!store.muted || store.mutedSinceMs === null) return;
|
|
992
|
+
const nowMs = Date.now() - store.startedAt;
|
|
993
|
+
if (nowMs > store.mutedSinceMs) {
|
|
994
|
+
store.mutedSegments.push({ startMs: store.mutedSinceMs, endMs: nowMs });
|
|
995
|
+
}
|
|
996
|
+
store.mutedSinceMs = null;
|
|
997
|
+
}
|
|
475
998
|
function stopRecording(store) {
|
|
476
999
|
const recorder = store.recorder;
|
|
477
1000
|
if (recorder && recorder.state !== "inactive") {
|
|
@@ -492,22 +1015,50 @@ function stopRecording(store) {
|
|
|
492
1015
|
}
|
|
493
1016
|
async function finishFlow(store, ctx, opts) {
|
|
494
1017
|
if (store.cancelled) return;
|
|
1018
|
+
if (store.finishFlowRan) return;
|
|
495
1019
|
if (store.indicatorState === "finishing" || store.indicatorState === "done") return;
|
|
1020
|
+
store.finishFlowRan = true;
|
|
496
1021
|
store.indicatorState = "finishing";
|
|
1022
|
+
flushMuteIfActive(store);
|
|
497
1023
|
renderIndicatorState(store);
|
|
498
1024
|
stopRecording(store);
|
|
499
1025
|
await store.uploadQueue;
|
|
500
1026
|
await flushPendingFromIdb(store, ctx);
|
|
501
1027
|
const durationSeconds = (Date.now() - store.startedAt) / 1e3;
|
|
502
1028
|
if (store.sessionId) {
|
|
503
|
-
const
|
|
1029
|
+
const unackedNotes = store.notes.filter((n) => !n.acked).map((n) => ({ atMs: n.atMs, text: n.text }));
|
|
1030
|
+
const ok = await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds, {
|
|
1031
|
+
mutedSegments: store.mutedSegments,
|
|
1032
|
+
notes: unackedNotes
|
|
1033
|
+
});
|
|
1034
|
+
if (ok) {
|
|
1035
|
+
for (const n of store.notes) {
|
|
1036
|
+
if (!n.acked) n.acked = true;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
504
1039
|
store.indicatorState = ok ? "done" : "error";
|
|
505
1040
|
} else {
|
|
506
1041
|
store.indicatorState = "error";
|
|
507
1042
|
}
|
|
508
1043
|
renderIndicatorState(store);
|
|
509
1044
|
if (opts.showThanks && store.indicatorRoot && store.indicatorState === "done") {
|
|
510
|
-
showThanksScreen(store.indicatorRoot
|
|
1045
|
+
showThanksScreen(store.indicatorRoot, {
|
|
1046
|
+
onSubmitNote: async (text) => {
|
|
1047
|
+
if (!store.sessionId) return;
|
|
1048
|
+
store.endNote = text;
|
|
1049
|
+
const stillUnacked = store.notes.filter((n) => !n.acked).map((n) => ({ atMs: n.atMs, text: n.text }));
|
|
1050
|
+
const ok = await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds, {
|
|
1051
|
+
endNote: text,
|
|
1052
|
+
notes: stillUnacked
|
|
1053
|
+
});
|
|
1054
|
+
if (!ok) throw new Error("finalise failed");
|
|
1055
|
+
for (const n of store.notes) {
|
|
1056
|
+
if (!n.acked) n.acked = true;
|
|
1057
|
+
}
|
|
1058
|
+
},
|
|
1059
|
+
onSkip: () => {
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
511
1062
|
}
|
|
512
1063
|
}
|
|
513
1064
|
function userTest(options = {}) {
|
|
@@ -544,7 +1095,18 @@ function userTest(options = {}) {
|
|
|
544
1095
|
tasks: [],
|
|
545
1096
|
tasksPanelOpen: readTasksPanelOpen(),
|
|
546
1097
|
outsidePointerHandler: null,
|
|
547
|
-
keydownHandler: null
|
|
1098
|
+
keydownHandler: null,
|
|
1099
|
+
hasMicPermission: false,
|
|
1100
|
+
muted: false,
|
|
1101
|
+
mutedSinceMs: null,
|
|
1102
|
+
mutedSegments: [],
|
|
1103
|
+
muteToastShown: false,
|
|
1104
|
+
muteToastTimers: [],
|
|
1105
|
+
notes: [],
|
|
1106
|
+
notesPopoverOpen: false,
|
|
1107
|
+
notePopoverAtMs: null,
|
|
1108
|
+
endNote: "",
|
|
1109
|
+
finishFlowRan: false
|
|
548
1110
|
};
|
|
549
1111
|
ctx.setStore(store);
|
|
550
1112
|
const onFinish = () => {
|
|
@@ -557,23 +1119,67 @@ function userTest(options = {}) {
|
|
|
557
1119
|
renderTasksPanel(store);
|
|
558
1120
|
};
|
|
559
1121
|
const onToggleTasks = () => setPanelOpen(!store.tasksPanelOpen);
|
|
1122
|
+
const onToggleMute = () => {
|
|
1123
|
+
if (!store.hasMicPermission) return;
|
|
1124
|
+
const ok = toggleMute(store);
|
|
1125
|
+
if (!ok) return;
|
|
1126
|
+
if (store.muted) showMuteToast(store);
|
|
1127
|
+
renderIndicatorState(store);
|
|
1128
|
+
};
|
|
1129
|
+
const closeNote = () => closeNotePopover(store);
|
|
1130
|
+
const onOpenNote = () => {
|
|
1131
|
+
if (store.notesPopoverOpen) {
|
|
1132
|
+
closeNote();
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
openNotePopover(
|
|
1136
|
+
store,
|
|
1137
|
+
(text) => {
|
|
1138
|
+
const atMs = store.notePopoverAtMs ?? Math.max(0, Date.now() - store.startedAt);
|
|
1139
|
+
const note = { atMs, text, acked: false };
|
|
1140
|
+
store.notes.push(note);
|
|
1141
|
+
closeNote();
|
|
1142
|
+
renderNotesCount(store);
|
|
1143
|
+
if (store.sessionId) {
|
|
1144
|
+
const sessionId = store.sessionId;
|
|
1145
|
+
void (async () => {
|
|
1146
|
+
const result = await postNoteWithRetry(store.options.apiUrl, sessionId, atMs, text, ctx.logger);
|
|
1147
|
+
if (result.ok) {
|
|
1148
|
+
note.acked = true;
|
|
1149
|
+
if (result.id) note.serverId = result.id;
|
|
1150
|
+
}
|
|
1151
|
+
})();
|
|
1152
|
+
}
|
|
1153
|
+
},
|
|
1154
|
+
() => closeNote()
|
|
1155
|
+
);
|
|
1156
|
+
};
|
|
560
1157
|
if (!merged.hideIndicator) {
|
|
561
1158
|
const host = document.createElement("div");
|
|
562
1159
|
host.setAttribute("data-usero-user-test", "true");
|
|
563
1160
|
document.body.appendChild(host);
|
|
564
1161
|
store.indicator = host;
|
|
565
|
-
store.indicatorRoot = buildIndicator(host, store,
|
|
1162
|
+
store.indicatorRoot = buildIndicator(host, store, {
|
|
1163
|
+
onFinish,
|
|
1164
|
+
onToggleTasks,
|
|
1165
|
+
onToggleMute,
|
|
1166
|
+
onOpenNote
|
|
1167
|
+
});
|
|
566
1168
|
renderIndicatorState(store);
|
|
1169
|
+
renderNotesCount(store);
|
|
567
1170
|
}
|
|
568
1171
|
const outsidePointer = (event) => {
|
|
569
|
-
if (!store.tasksPanelOpen) return;
|
|
570
1172
|
const host = store.indicator;
|
|
571
1173
|
if (!host) return;
|
|
572
1174
|
const path = event.composedPath();
|
|
573
|
-
if (
|
|
1175
|
+
if (path.includes(host)) return;
|
|
1176
|
+
if (store.tasksPanelOpen) setPanelOpen(false);
|
|
1177
|
+
if (store.notesPopoverOpen) closeNote();
|
|
574
1178
|
};
|
|
575
1179
|
const onKeydown = (event) => {
|
|
576
|
-
if (event.key
|
|
1180
|
+
if (event.key !== "Escape") return;
|
|
1181
|
+
if (store.tasksPanelOpen) setPanelOpen(false);
|
|
1182
|
+
if (store.notesPopoverOpen) closeNote();
|
|
577
1183
|
};
|
|
578
1184
|
store.outsidePointerHandler = outsidePointer;
|
|
579
1185
|
store.keydownHandler = onKeydown;
|
|
@@ -625,6 +1231,13 @@ function userTest(options = {}) {
|
|
|
625
1231
|
document.removeEventListener("keydown", store.keydownHandler);
|
|
626
1232
|
store.keydownHandler = null;
|
|
627
1233
|
}
|
|
1234
|
+
for (const id of store.muteToastTimers) {
|
|
1235
|
+
try {
|
|
1236
|
+
window.clearTimeout(id);
|
|
1237
|
+
} catch {
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
store.muteToastTimers = [];
|
|
628
1241
|
if (store.indicator && store.indicator.parentNode) {
|
|
629
1242
|
store.indicator.parentNode.removeChild(store.indicator);
|
|
630
1243
|
}
|