@usero/sdk 1.1.0 → 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.
|
@@ -625,12 +625,15 @@ function showMuteToast(store) {
|
|
|
625
625
|
toast.setAttribute("role", "status");
|
|
626
626
|
toast.innerHTML = `<strong>Mic off.</strong> Screen is still recording. Tap to unmute.`;
|
|
627
627
|
slot.appendChild(toast);
|
|
628
|
-
window.setTimeout(() => {
|
|
628
|
+
const outer = window.setTimeout(() => {
|
|
629
|
+
if (!toast.isConnected) return;
|
|
629
630
|
toast.setAttribute("data-leaving", "true");
|
|
630
|
-
window.setTimeout(() => {
|
|
631
|
-
toast.remove();
|
|
631
|
+
const inner = window.setTimeout(() => {
|
|
632
|
+
if (toast.isConnected) toast.remove();
|
|
632
633
|
}, 260);
|
|
634
|
+
store.muteToastTimers.push(inner);
|
|
633
635
|
}, 3e3);
|
|
636
|
+
store.muteToastTimers.push(outer);
|
|
634
637
|
}
|
|
635
638
|
function openNotePopover(store, onSave, onCancel) {
|
|
636
639
|
const root = store.indicatorRoot;
|
|
@@ -758,6 +761,17 @@ function showThanksScreen(root, opts) {
|
|
|
758
761
|
sent.textContent = message;
|
|
759
762
|
card.appendChild(sent);
|
|
760
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
|
+
};
|
|
761
775
|
const submit = async () => {
|
|
762
776
|
const text = ta.value.trim();
|
|
763
777
|
ta.disabled = true;
|
|
@@ -765,10 +779,21 @@ function showThanksScreen(root, opts) {
|
|
|
765
779
|
const submitBtn = form.querySelector("button.primary");
|
|
766
780
|
if (submitBtn) submitBtn.disabled = true;
|
|
767
781
|
if (text) {
|
|
768
|
-
|
|
769
|
-
|
|
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
|
+
}
|
|
770
796
|
} else {
|
|
771
|
-
opts.onSkip();
|
|
772
797
|
swapToSent("All good. You can close this tab.");
|
|
773
798
|
}
|
|
774
799
|
};
|
|
@@ -825,6 +850,12 @@ async function finaliseSession(apiUrl, sessionId, durationSeconds, extras = {})
|
|
|
825
850
|
}
|
|
826
851
|
const trimmedEndNote = extras.endNote?.trim();
|
|
827
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
|
+
}
|
|
828
859
|
const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/user-test-sessions/${encodeURIComponent(sessionId)}/finalise`, {
|
|
829
860
|
method: "POST",
|
|
830
861
|
headers: { "Content-Type": "application/json" },
|
|
@@ -836,7 +867,7 @@ async function finaliseSession(apiUrl, sessionId, durationSeconds, extras = {})
|
|
|
836
867
|
return false;
|
|
837
868
|
}
|
|
838
869
|
}
|
|
839
|
-
async function
|
|
870
|
+
async function postNoteOnce(apiUrl, sessionId, atMs, text, logger) {
|
|
840
871
|
try {
|
|
841
872
|
const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/user-test-sessions/${encodeURIComponent(sessionId)}/notes`, {
|
|
842
873
|
method: "POST",
|
|
@@ -846,14 +877,26 @@ async function postNote(apiUrl, sessionId, atMs, text, logger) {
|
|
|
846
877
|
});
|
|
847
878
|
if (!res.ok) {
|
|
848
879
|
logger.warn(`note POST rejected with ${res.status}`);
|
|
849
|
-
return false;
|
|
880
|
+
return { ok: false, transient: res.status >= 500 || res.status === 408 || res.status === 429 };
|
|
850
881
|
}
|
|
851
|
-
|
|
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 };
|
|
852
889
|
} catch (err) {
|
|
853
890
|
logger.warn("note POST failed", err);
|
|
854
|
-
return false;
|
|
891
|
+
return { ok: false, transient: true };
|
|
855
892
|
}
|
|
856
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
|
+
}
|
|
857
900
|
async function flushPendingFromIdb(store, ctx) {
|
|
858
901
|
if (!store.sessionId) return;
|
|
859
902
|
const pending = await idbListChunks(store.sessionId);
|
|
@@ -974,7 +1017,9 @@ function stopRecording(store) {
|
|
|
974
1017
|
}
|
|
975
1018
|
async function finishFlow(store, ctx, opts) {
|
|
976
1019
|
if (store.cancelled) return;
|
|
1020
|
+
if (store.finishFlowRan) return;
|
|
977
1021
|
if (store.indicatorState === "finishing" || store.indicatorState === "done") return;
|
|
1022
|
+
store.finishFlowRan = true;
|
|
978
1023
|
store.indicatorState = "finishing";
|
|
979
1024
|
flushMuteIfActive(store);
|
|
980
1025
|
renderIndicatorState(store);
|
|
@@ -983,9 +1028,16 @@ async function finishFlow(store, ctx, opts) {
|
|
|
983
1028
|
await flushPendingFromIdb(store, ctx);
|
|
984
1029
|
const durationSeconds = (Date.now() - store.startedAt) / 1e3;
|
|
985
1030
|
if (store.sessionId) {
|
|
1031
|
+
const unackedNotes = store.notes.filter((n) => !n.acked).map((n) => ({ atMs: n.atMs, text: n.text }));
|
|
986
1032
|
const ok = await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds, {
|
|
987
|
-
mutedSegments: store.mutedSegments
|
|
1033
|
+
mutedSegments: store.mutedSegments,
|
|
1034
|
+
notes: unackedNotes
|
|
988
1035
|
});
|
|
1036
|
+
if (ok) {
|
|
1037
|
+
for (const n of store.notes) {
|
|
1038
|
+
if (!n.acked) n.acked = true;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
989
1041
|
store.indicatorState = ok ? "done" : "error";
|
|
990
1042
|
} else {
|
|
991
1043
|
store.indicatorState = "error";
|
|
@@ -996,10 +1048,15 @@ async function finishFlow(store, ctx, opts) {
|
|
|
996
1048
|
onSubmitNote: async (text) => {
|
|
997
1049
|
if (!store.sessionId) return;
|
|
998
1050
|
store.endNote = text;
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
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
|
|
1002
1055
|
});
|
|
1056
|
+
if (!ok) throw new Error("finalise failed");
|
|
1057
|
+
for (const n of store.notes) {
|
|
1058
|
+
if (!n.acked) n.acked = true;
|
|
1059
|
+
}
|
|
1003
1060
|
},
|
|
1004
1061
|
onSkip: () => {
|
|
1005
1062
|
}
|
|
@@ -1046,10 +1103,12 @@ function userTest(options = {}) {
|
|
|
1046
1103
|
mutedSinceMs: null,
|
|
1047
1104
|
mutedSegments: [],
|
|
1048
1105
|
muteToastShown: false,
|
|
1106
|
+
muteToastTimers: [],
|
|
1049
1107
|
notes: [],
|
|
1050
1108
|
notesPopoverOpen: false,
|
|
1051
1109
|
notePopoverAtMs: null,
|
|
1052
|
-
endNote: ""
|
|
1110
|
+
endNote: "",
|
|
1111
|
+
finishFlowRan: false
|
|
1053
1112
|
};
|
|
1054
1113
|
ctx.setStore(store);
|
|
1055
1114
|
const onFinish = () => {
|
|
@@ -1079,11 +1138,19 @@ function userTest(options = {}) {
|
|
|
1079
1138
|
store,
|
|
1080
1139
|
(text) => {
|
|
1081
1140
|
const atMs = store.notePopoverAtMs ?? Math.max(0, Date.now() - store.startedAt);
|
|
1082
|
-
|
|
1141
|
+
const note = { atMs, text, acked: false };
|
|
1142
|
+
store.notes.push(note);
|
|
1083
1143
|
closeNote();
|
|
1084
1144
|
renderNotesCount(store);
|
|
1085
1145
|
if (store.sessionId) {
|
|
1086
|
-
|
|
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
|
+
})();
|
|
1087
1154
|
}
|
|
1088
1155
|
},
|
|
1089
1156
|
() => closeNote()
|
|
@@ -1166,6 +1233,13 @@ function userTest(options = {}) {
|
|
|
1166
1233
|
document.removeEventListener("keydown", store.keydownHandler);
|
|
1167
1234
|
store.keydownHandler = null;
|
|
1168
1235
|
}
|
|
1236
|
+
for (const id of store.muteToastTimers) {
|
|
1237
|
+
try {
|
|
1238
|
+
window.clearTimeout(id);
|
|
1239
|
+
} catch {
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
store.muteToastTimers = [];
|
|
1169
1243
|
if (store.indicator && store.indicator.parentNode) {
|
|
1170
1244
|
store.indicator.parentNode.removeChild(store.indicator);
|
|
1171
1245
|
}
|