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