@burdenoff/microfe-technospam 2026.1102.3 → 2026.1102.4

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.
Files changed (46) hide show
  1. package/dist/generated/global-types.d.ts +307 -41
  2. package/dist/generated/global-types.d.ts.map +1 -1
  3. package/dist/generated/wspace-types.d.ts +1730 -8
  4. package/dist/generated/wspace-types.d.ts.map +1 -1
  5. package/dist/stories/fixtures/technospamFixtures.d.ts.map +1 -1
  6. package/dist/technospam/TechnoSpamRoutes.d.ts.map +1 -1
  7. package/dist/technospam/TechnoSpamRoutes.js +32 -28
  8. package/dist/technospam/TechnoSpamRoutes.js.map +1 -1
  9. package/dist/technospam/api/assessmentApi.d.ts +183 -0
  10. package/dist/technospam/api/assessmentApi.d.ts.map +1 -0
  11. package/dist/technospam/api/assessmentApi.js +278 -0
  12. package/dist/technospam/api/assessmentApi.js.map +1 -0
  13. package/dist/technospam/components/CourseAssessments.d.ts +9 -0
  14. package/dist/technospam/components/CourseAssessments.d.ts.map +1 -0
  15. package/dist/technospam/components/CourseAssessments.js +66 -0
  16. package/dist/technospam/components/CourseAssessments.js.map +1 -0
  17. package/dist/technospam/components/StatusBadge.d.ts.map +1 -1
  18. package/dist/technospam/components/StatusBadge.js +8 -1
  19. package/dist/technospam/components/StatusBadge.js.map +1 -1
  20. package/dist/technospam/components/admin/AssessmentsAdminTab.d.ts +7 -0
  21. package/dist/technospam/components/admin/AssessmentsAdminTab.d.ts.map +1 -0
  22. package/dist/technospam/components/admin/AssessmentsAdminTab.js +421 -0
  23. package/dist/technospam/components/admin/AssessmentsAdminTab.js.map +1 -0
  24. package/dist/technospam/components/lesson/LessonKindFields.d.ts +3 -1
  25. package/dist/technospam/components/lesson/LessonKindFields.d.ts.map +1 -1
  26. package/dist/technospam/components/lesson/LessonKindFields.js +2 -2
  27. package/dist/technospam/components/lesson/LessonKindFields.js.map +1 -1
  28. package/dist/technospam/hooks/useAssessmentIntegrity.d.ts +29 -0
  29. package/dist/technospam/hooks/useAssessmentIntegrity.d.ts.map +1 -0
  30. package/dist/technospam/hooks/useAssessmentIntegrity.js +57 -0
  31. package/dist/technospam/hooks/useAssessmentIntegrity.js.map +1 -0
  32. package/dist/technospam/pages/AdminAdminDashboardPagePage.d.ts.map +1 -1
  33. package/dist/technospam/pages/AdminAdminDashboardPagePage.js +364 -359
  34. package/dist/technospam/pages/AdminAdminDashboardPagePage.js.map +1 -1
  35. package/dist/technospam/pages/CoursesAssessmentPagePage.d.ts +9 -0
  36. package/dist/technospam/pages/CoursesAssessmentPagePage.d.ts.map +1 -0
  37. package/dist/technospam/pages/CoursesAssessmentPagePage.js +464 -0
  38. package/dist/technospam/pages/CoursesAssessmentPagePage.js.map +1 -0
  39. package/dist/technospam/pages/CoursesCourseDetailsPagePage.d.ts.map +1 -1
  40. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +210 -208
  41. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
  42. package/dist/technospam/utils/assessment.d.ts +64 -0
  43. package/dist/technospam/utils/assessment.d.ts.map +1 -0
  44. package/dist/technospam/utils/assessment.js +122 -0
  45. package/dist/technospam/utils/assessment.js.map +1 -0
  46. package/package.json +1 -1
@@ -0,0 +1,464 @@
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { PageLayout as t } from "../components/PageLayout.js";
3
+ import { formatDateTime as n } from "../utils/format.js";
4
+ import { StatusBadge as r } from "../components/StatusBadge.js";
5
+ import { EmptyView as i, ErrorView as a } from "../components/StateViews.js";
6
+ import { notify as o } from "../lib/toast.js";
7
+ import { userMessageFromError as s } from "../lib/errorMessage.js";
8
+ import "../components/index.js";
9
+ import { courseDetailsPath as c } from "../utils/lessonPath.js";
10
+ import { useAssessmentQuery as l, useClaimAssessmentCertificate as u, useMyAssessmentAttemptsQuery as d, useRecordAssessmentSignal as f, useSaveAssessmentAnswers as p, useStartAssessment as m, useSubmitAssessment as h } from "../api/assessmentApi.js";
11
+ import { answeredCount as g, attemptsLeft as _, certificatePending as v, formatClock as y, proctoringModeDescription as b, proctoringStatusDescription as x } from "../utils/assessment.js";
12
+ import { useAssessmentIntegrity as S } from "../hooks/useAssessmentIntegrity.js";
13
+ import { useCallback as C, useEffect as w, useRef as T, useState as E } from "react";
14
+ import { Award as D, CheckCircle2 as O, Clock as k, Maximize as A, RotateCcw as j, Send as M, ShieldCheck as N, Timer as P, XCircle as F } from "lucide-react";
15
+ import { useSearchParams as I } from "react-router";
16
+ import { jsx as L, jsxs as R } from "react/jsx-runtime";
17
+ import { Button as z, GlassCard as B, Skeleton as V } from "@burdenoff/fe-libs/ui";
18
+ //#region src/technospam/pages/CoursesAssessmentPagePage.tsx
19
+ var H = 1500, U = 5e3, W = 3e4;
20
+ function G({ icon: e, children: t }) {
21
+ return /* @__PURE__ */ R("li", {
22
+ className: "flex items-start gap-2 text-sm text-text-secondary",
23
+ children: [/* @__PURE__ */ L(e, {
24
+ size: 16,
25
+ className: "mt-0.5 shrink-0 text-text-muted"
26
+ }), /* @__PURE__ */ L("span", { children: t })]
27
+ });
28
+ }
29
+ function K({ assessment: e, attempts: t, onStarted: n }) {
30
+ let [r, i] = E(!1), [a, o] = E(null), [c, { loading: l }] = m(), u = e.proctoringMode !== "NONE", d = _(e, t), f = typeof document < "u" && document.fullscreenEnabled === !0, p = async () => {
31
+ o(null), e.requireFullscreen && u && f && await document.documentElement.requestFullscreen().catch(() => void 0);
32
+ try {
33
+ let t = (await c({ variables: {
34
+ assessmentId: e.id,
35
+ acceptMonitoring: u ? r : !1
36
+ } })).data?.tecStartAssessment;
37
+ t && n(t);
38
+ } catch (e) {
39
+ o(s(e)), document.fullscreenElement && document.exitFullscreen().catch(() => void 0);
40
+ }
41
+ };
42
+ return /* @__PURE__ */ R(B, {
43
+ className: "space-y-5 p-6",
44
+ "data-testid": "assessment-intro",
45
+ children: [
46
+ e.description ? /* @__PURE__ */ L("p", {
47
+ className: "text-sm text-text-secondary",
48
+ children: e.description
49
+ }) : null,
50
+ /* @__PURE__ */ R("ul", {
51
+ className: "space-y-2",
52
+ children: [
53
+ /* @__PURE__ */ R(G, {
54
+ icon: P,
55
+ children: [e.timeLimitMinutes, " minutes, timed by the server from the moment you start. Unsubmitted answers are saved as you go and graded when time runs out."]
56
+ }),
57
+ /* @__PURE__ */ R(G, {
58
+ icon: O,
59
+ children: [
60
+ e.questionCount,
61
+ " questions, presented in a random order. You need",
62
+ " ",
63
+ e.passingScore,
64
+ "% to pass."
65
+ ]
66
+ }),
67
+ /* @__PURE__ */ R(G, {
68
+ icon: j,
69
+ children: [
70
+ d,
71
+ " of ",
72
+ e.maxAttempts,
73
+ " attempt",
74
+ e.maxAttempts === 1 ? "" : "s",
75
+ " left."
76
+ ]
77
+ }),
78
+ /* @__PURE__ */ L(G, {
79
+ icon: N,
80
+ children: b(e.proctoringMode)
81
+ }),
82
+ u && e.requireFullscreen ? /* @__PURE__ */ L(G, {
83
+ icon: A,
84
+ children: "The exam runs in fullscreen. Leaving fullscreen is recorded."
85
+ }) : null,
86
+ u && e.blockClipboard ? /* @__PURE__ */ L(G, {
87
+ icon: F,
88
+ children: "Copy, cut, paste and the right-click menu are disabled and recorded."
89
+ }) : null,
90
+ e.issuesCertificate ? /* @__PURE__ */ L(G, {
91
+ icon: D,
92
+ children: "Passing earns the course certificate once the integrity check is cleared."
93
+ }) : null
94
+ ]
95
+ }),
96
+ u ? /* @__PURE__ */ R("label", {
97
+ className: "flex items-start gap-2 text-sm text-text-primary",
98
+ children: [/* @__PURE__ */ L("input", {
99
+ type: "checkbox",
100
+ className: "mt-0.5",
101
+ checked: r,
102
+ onChange: (e) => i(e.target.checked),
103
+ "data-testid": "assessment-consent"
104
+ }), /* @__PURE__ */ L("span", { children: "I agree that integrity events from this browser tab are recorded for this attempt. No camera, microphone or screen recording is used." })]
105
+ }) : null,
106
+ a ? /* @__PURE__ */ L("p", {
107
+ role: "alert",
108
+ className: "text-sm text-status-error-text",
109
+ children: a
110
+ }) : null,
111
+ /* @__PURE__ */ R(z, {
112
+ onClick: () => void p(),
113
+ disabled: l || d === 0 || u && !r,
114
+ "data-testid": "assessment-start",
115
+ children: [
116
+ /* @__PURE__ */ L(P, { size: 15 }),
117
+ " ",
118
+ l ? "Starting…" : "Start assessment"
119
+ ]
120
+ })
121
+ ]
122
+ });
123
+ }
124
+ function q({ attempt: t, onClosed: n }) {
125
+ let [r, i] = E(() => t.questions.map((e, n) => t.savedAnswers[n] ?? -1)), [a, o] = E(t.integrityViolations), [c] = E(() => Date.now() + t.remainingSeconds * 1e3), [l, u] = E(t.remainingSeconds), [d] = p(), [m] = f(), [_, { loading: v }] = h(), b = T(!1), x = T(0), D = T(r);
126
+ w(() => {
127
+ D.current = r;
128
+ }, [r]);
129
+ let O = t.proctoringMode !== "NONE", j = C((e, n) => {
130
+ b.current || m({ variables: {
131
+ attemptId: t.id,
132
+ signal: e,
133
+ detail: n ?? null
134
+ } }).then((e) => {
135
+ let t = e.data?.tecRecordAssessmentSignal;
136
+ t && o(t.integrityViolations);
137
+ }).catch(() => void 0);
138
+ }, [t.id, m]), N = S({
139
+ active: O,
140
+ requireFullscreen: t.requireFullscreen,
141
+ blockClipboard: t.blockClipboard,
142
+ onSignal: j
143
+ }), P = O && t.requireFullscreen && N.fullscreenSupported && !N.isFullscreen, F = C(() => {
144
+ b.current = !1, x.current = Date.now() + U;
145
+ }, []), I = C(async (e) => {
146
+ if (!b.current) {
147
+ b.current = !0;
148
+ try {
149
+ let r = (await _({ variables: {
150
+ attemptId: t.id,
151
+ answers: e
152
+ } })).data?.tecSubmitAssessment ?? null;
153
+ (!r || r.status === "IN_PROGRESS") && F(), n(r);
154
+ } catch (e) {
155
+ F(), n(null, s(e));
156
+ }
157
+ }
158
+ }, [
159
+ t.id,
160
+ n,
161
+ F,
162
+ _
163
+ ]);
164
+ w(() => {
165
+ let e = () => {
166
+ let e = Math.max(0, Math.round((c - Date.now()) / 1e3));
167
+ u(e), e === 0 && Date.now() >= x.current && I(D.current);
168
+ };
169
+ e();
170
+ let t = window.setInterval(e, 1e3);
171
+ return () => window.clearInterval(t);
172
+ }, [c, I]);
173
+ let V = T(!1), G = T(!1), K = T(void 0), q = T(0), J = T(!0), [Y, X] = E("saved"), Z = C(async () => {
174
+ if (!J.current) return;
175
+ if (V.current) {
176
+ G.current = !0;
177
+ return;
178
+ }
179
+ K.current !== void 0 && (window.clearTimeout(K.current), K.current = void 0), V.current = !0, X("saving");
180
+ let e = !1;
181
+ try {
182
+ do {
183
+ if (G.current = !1, b.current || !J.current) break;
184
+ try {
185
+ await d({ variables: {
186
+ attemptId: t.id,
187
+ answers: D.current
188
+ } }), q.current = 0;
189
+ } catch {
190
+ e = !0, G.current = !1;
191
+ break;
192
+ }
193
+ } while (G.current);
194
+ } finally {
195
+ V.current = !1;
196
+ }
197
+ if (!(b.current || !J.current)) if (e) {
198
+ X("unsaved"), q.current += 1;
199
+ let e = Math.min(W, H * 2 ** q.current);
200
+ K.current = window.setTimeout(() => void Z(), e);
201
+ } else X("saved");
202
+ }, [t.id, d]);
203
+ w(() => (J.current = !0, () => {
204
+ J.current = !1, K.current !== void 0 && window.clearTimeout(K.current);
205
+ }), []), w(() => {
206
+ if (b.current) return;
207
+ let e = window.setTimeout(() => void Z(), H);
208
+ return () => window.clearTimeout(e);
209
+ }, [r, Z]);
210
+ let Q = g(r);
211
+ return /* @__PURE__ */ R("div", {
212
+ className: "space-y-4",
213
+ "data-testid": "assessment-exam",
214
+ children: [/* @__PURE__ */ R(B, {
215
+ className: "sticky top-0 z-10 flex flex-wrap items-center justify-between gap-3 p-4",
216
+ children: [
217
+ /* @__PURE__ */ R("div", {
218
+ className: e("flex items-center gap-2 font-mono text-lg font-semibold", l <= 60 ? "text-status-error-text" : "text-text-primary"),
219
+ "aria-live": "polite",
220
+ "data-testid": "assessment-timer",
221
+ children: [
222
+ /* @__PURE__ */ L(k, { size: 18 }),
223
+ " ",
224
+ y(l)
225
+ ]
226
+ }),
227
+ /* @__PURE__ */ R("span", {
228
+ className: "text-sm text-text-secondary",
229
+ children: [
230
+ Q,
231
+ " of ",
232
+ t.questions.length,
233
+ " answered"
234
+ ]
235
+ }),
236
+ /* @__PURE__ */ L("span", {
237
+ className: e("text-xs", Y === "unsaved" ? "text-status-warning-text" : "text-text-muted"),
238
+ role: "status",
239
+ "data-testid": "assessment-save-state",
240
+ children: Y === "saving" ? "Saving…" : Y === "unsaved" ? "Answers not saved yet, retrying…" : "Answers saved"
241
+ }),
242
+ O ? /* @__PURE__ */ R("span", {
243
+ className: "text-sm text-text-secondary",
244
+ "data-testid": "assessment-violations",
245
+ children: [
246
+ "Integrity events: ",
247
+ a,
248
+ t.proctoringMode === "INTEGRITY_MONITORING" ? ` (allowed ${t.maxIntegrityViolations})` : ""
249
+ ]
250
+ }) : null,
251
+ /* @__PURE__ */ R(z, {
252
+ onClick: () => {
253
+ window.confirm("Submit your answers? You cannot change them afterwards.") && I(r);
254
+ },
255
+ disabled: v,
256
+ "data-testid": "assessment-submit",
257
+ children: [
258
+ /* @__PURE__ */ L(M, { size: 15 }),
259
+ " ",
260
+ v ? "Submitting…" : "Submit"
261
+ ]
262
+ })
263
+ ]
264
+ }), P ? /* @__PURE__ */ R(B, {
265
+ className: "space-y-3 p-6 text-center",
266
+ "data-testid": "assessment-fullscreen-gate",
267
+ children: [/* @__PURE__ */ L("p", {
268
+ className: "text-sm text-text-primary",
269
+ children: "This exam runs in fullscreen. Leaving fullscreen was recorded. Return to fullscreen to continue."
270
+ }), /* @__PURE__ */ R(z, {
271
+ onClick: () => void N.enterFullscreen(),
272
+ children: [/* @__PURE__ */ L(A, { size: 15 }), " Return to fullscreen"]
273
+ })]
274
+ }) : /* @__PURE__ */ L("div", {
275
+ className: e("space-y-4", t.blockClipboard && O && "select-none"),
276
+ children: t.questions.map((n) => /* @__PURE__ */ R("fieldset", {
277
+ className: "rounded-xl border border-border-seam bg-bg-surface p-4",
278
+ disabled: v,
279
+ "data-testid": "assessment-question",
280
+ children: [/* @__PURE__ */ R("legend", {
281
+ className: "px-1 text-sm font-semibold text-text-primary",
282
+ children: [
283
+ n.position + 1,
284
+ ". ",
285
+ n.prompt
286
+ ]
287
+ }), /* @__PURE__ */ L("div", {
288
+ className: "mt-2 space-y-2",
289
+ children: n.options.map((a, o) => {
290
+ let s = r[n.position] === o;
291
+ return /* @__PURE__ */ R("label", {
292
+ className: e("flex cursor-pointer items-center gap-3 rounded-lg border px-3 py-2 text-sm transition-colors", s ? "border-accent-primary bg-accent-primary/10" : "border-border-seam"),
293
+ children: [/* @__PURE__ */ L("input", {
294
+ type: "radio",
295
+ name: `tec-assessment-${t.id}-${n.position}`,
296
+ checked: s,
297
+ onChange: () => i((e) => e.map((e, t) => t === n.position ? o : e))
298
+ }), /* @__PURE__ */ L("span", {
299
+ className: "text-text-primary",
300
+ children: a
301
+ })]
302
+ }, o);
303
+ })
304
+ })]
305
+ }, n.position))
306
+ })]
307
+ });
308
+ }
309
+ function J({ attempt: e, assessment: t, canRetake: i, onRetake: a }) {
310
+ let [s, { loading: c }] = u(), l = e.passed === !0;
311
+ return /* @__PURE__ */ R(B, {
312
+ className: "space-y-4 p-6",
313
+ "data-testid": "assessment-result",
314
+ children: [
315
+ /* @__PURE__ */ R("div", {
316
+ className: "flex flex-wrap items-center gap-3",
317
+ children: [
318
+ l ? /* @__PURE__ */ L(O, {
319
+ size: 22,
320
+ className: "text-status-success-text"
321
+ }) : /* @__PURE__ */ L(F, {
322
+ size: 22,
323
+ className: "text-status-error-text"
324
+ }),
325
+ /* @__PURE__ */ R("h2", {
326
+ className: "text-lg font-semibold text-text-primary",
327
+ "data-testid": "assessment-score",
328
+ children: [
329
+ e.score ?? 0,
330
+ "% · ",
331
+ l ? "Passed" : "Not passed"
332
+ ]
333
+ }),
334
+ /* @__PURE__ */ L(r, { status: e.status }),
335
+ /* @__PURE__ */ L(r, { status: e.proctoringStatus })
336
+ ]
337
+ }),
338
+ /* @__PURE__ */ R("p", {
339
+ className: "text-sm text-text-secondary",
340
+ children: [
341
+ e.correctCount,
342
+ " of ",
343
+ e.totalCount,
344
+ " correct; ",
345
+ e.passingScore,
346
+ "% needed to pass. Attempt ",
347
+ e.attemptNumber,
348
+ e.submittedAt ? `, closed ${n(e.submittedAt)}` : "",
349
+ "."
350
+ ]
351
+ }),
352
+ /* @__PURE__ */ R("p", {
353
+ className: "text-sm text-text-secondary",
354
+ "data-testid": "assessment-proctoring",
355
+ children: [x(e.proctoringStatus), e.proctoringMode === "NONE" ? "" : ` Integrity events recorded: ${e.integrityViolations}.`]
356
+ }),
357
+ e.reviewNotes ? /* @__PURE__ */ R("p", {
358
+ className: "rounded-lg bg-bg-sunken px-3 py-2 text-sm text-text-secondary",
359
+ children: ["Reviewer note: ", e.reviewNotes]
360
+ }) : null,
361
+ e.certificate ? /* @__PURE__ */ R("p", {
362
+ className: "flex items-center gap-2 rounded-lg border border-status-success-border bg-status-success-bg-subtle px-3 py-2 text-sm text-status-success-text",
363
+ "data-testid": "assessment-certificate",
364
+ children: [
365
+ /* @__PURE__ */ L(D, { size: 16 }),
366
+ " Certificate issued · serial ",
367
+ e.certificate.serial
368
+ ]
369
+ }) : null,
370
+ /* @__PURE__ */ R("div", {
371
+ className: "flex flex-wrap gap-2",
372
+ children: [v(e, t) ? /* @__PURE__ */ R(z, {
373
+ variant: "outline",
374
+ disabled: c,
375
+ onClick: () => void s({ variables: { attemptId: e.id } }).then((e) => {
376
+ e.data?.tecClaimAssessmentCertificate.certificate && o.success("Certificate issued", "Your certificate is ready.");
377
+ }),
378
+ children: [
379
+ /* @__PURE__ */ L(D, { size: 15 }),
380
+ " ",
381
+ c ? "Issuing…" : "Issue my certificate"
382
+ ]
383
+ }) : null, i ? /* @__PURE__ */ R(z, {
384
+ variant: "outline",
385
+ onClick: a,
386
+ "data-testid": "assessment-retake",
387
+ children: [/* @__PURE__ */ L(j, { size: 15 }), " Take another attempt"]
388
+ }) : null]
389
+ })
390
+ ]
391
+ });
392
+ }
393
+ function Y() {
394
+ let [e] = I(), n = e.get("assessmentId") ?? "", { data: s, loading: u, error: f } = l(n), p = d(n || null), m = p.refetch, [h, g] = E(null), [v, y] = E(!1), [b, x] = E(null), S = s?.tecAssessment ?? null, w = p.data?.tecMyAssessmentAttempts ?? [], T = h ?? w.find((e) => e.status === "IN_PROGRESS") ?? null, D = w.find((e) => e.status !== "IN_PROGRESS") ?? null, O = S ? c(S.courseId) : "/courses/courses-page", k = C((e, t) => {
395
+ if (!e || e.status === "IN_PROGRESS") {
396
+ x(t ?? null);
397
+ return;
398
+ }
399
+ g(null), y(!1), x(t ?? null), o.success("Assessment submitted", `You scored ${e.score ?? 0}%.`), m();
400
+ }, [m]);
401
+ if (!n) return /* @__PURE__ */ L(t, {
402
+ title: "Assessment",
403
+ backPath: "/courses/courses-page",
404
+ children: /* @__PURE__ */ L(i, {
405
+ title: "No assessment selected",
406
+ description: "Open an assessment from a course page."
407
+ })
408
+ });
409
+ if (u && !S || p.loading && !p.data) return /* @__PURE__ */ R(t, {
410
+ title: "Assessment",
411
+ backPath: O,
412
+ children: [/* @__PURE__ */ L(V, { className: "mb-4 h-10 w-1/2 rounded-xl" }), /* @__PURE__ */ L(V, { className: "h-64 w-full rounded-xl" })]
413
+ });
414
+ if (f || p.error) return /* @__PURE__ */ L(t, {
415
+ title: "Assessment",
416
+ backPath: O,
417
+ children: /* @__PURE__ */ L(a, {
418
+ title: "Could not load this assessment",
419
+ error: f ?? p.error
420
+ })
421
+ });
422
+ if (!S) return /* @__PURE__ */ L(t, {
423
+ title: "Assessment",
424
+ backPath: "/courses/courses-page",
425
+ children: /* @__PURE__ */ L(i, {
426
+ title: "Assessment not found",
427
+ description: "It may have been removed from this workspace."
428
+ })
429
+ });
430
+ let A = _(S, w), j = S.status === "PUBLISHED", M;
431
+ return M = T ? /* @__PURE__ */ L(q, {
432
+ attempt: T,
433
+ onClosed: k
434
+ }, T.id) : D && !v ? /* @__PURE__ */ L(J, {
435
+ attempt: D,
436
+ assessment: S,
437
+ canRetake: j && A > 0,
438
+ onRetake: () => y(!0)
439
+ }) : j ? /* @__PURE__ */ L(K, {
440
+ assessment: S,
441
+ attempts: w,
442
+ onStarted: g
443
+ }) : /* @__PURE__ */ L(i, {
444
+ title: "This assessment is closed",
445
+ description: "It is not accepting new attempts."
446
+ }), /* @__PURE__ */ L(t, {
447
+ title: S.title,
448
+ description: S.course?.title ? `Certification assessment · ${S.course.title}` : void 0,
449
+ backPath: T ? void 0 : O,
450
+ actions: /* @__PURE__ */ L(r, { status: S.status }),
451
+ children: /* @__PURE__ */ R("div", {
452
+ className: "mx-auto w-full max-w-3xl space-y-4",
453
+ children: [b ? /* @__PURE__ */ L("p", {
454
+ role: "status",
455
+ className: "rounded-lg bg-bg-sunken px-3 py-2 text-sm text-text-secondary",
456
+ children: b
457
+ }) : null, M]
458
+ })
459
+ });
460
+ }
461
+ //#endregion
462
+ export { Y as CoursesAssessmentPagePage };
463
+
464
+ //# sourceMappingURL=CoursesAssessmentPagePage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CoursesAssessmentPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesAssessmentPagePage.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport { useSearchParams } from 'react-router';\nimport {\n Award,\n CheckCircle2,\n Clock,\n Maximize,\n RotateCcw,\n Send,\n ShieldCheck,\n Timer,\n XCircle,\n} from 'lucide-react';\nimport { Button, GlassCard, Skeleton } from '@burdenoff/fe-libs/ui';\nimport { EmptyView, ErrorView, PageLayout, StatusBadge } from '../components';\nimport {\n useAssessmentQuery,\n useClaimAssessmentCertificate,\n useMyAssessmentAttemptsQuery,\n useRecordAssessmentSignal,\n useSaveAssessmentAnswers,\n useStartAssessment,\n useSubmitAssessment,\n type TecAssessment,\n type TecAssessmentAttempt,\n type TecAssessmentSignal,\n} from '../api/assessmentApi';\nimport { useAssessmentIntegrity } from '../hooks/useAssessmentIntegrity';\nimport { userMessageFromError } from '../lib/errorMessage';\nimport { notify } from '../lib/toast';\nimport { cn } from '../utils/cn';\nimport { formatDateTime } from '../utils/format';\nimport { courseDetailsPath } from '../utils/lessonPath';\nimport {\n answeredCount,\n attemptsLeft,\n certificatePending,\n formatClock,\n proctoringModeDescription,\n proctoringStatusDescription,\n} from '../utils/assessment';\n\n/** Debounce for saving answers while the learner works. */\nconst AUTOSAVE_MS = 1500;\n/** Delay before the expired countdown retries a submit that failed. */\nconst SUBMIT_RETRY_MS = 5_000;\n/** Upper bound for the backoff between retries of a failed autosave. */\nconst AUTOSAVE_RETRY_MAX_MS = 30_000;\n\nfunction Rule({ icon: Icon, children }: { icon: typeof Clock; children: React.ReactNode }) {\n return (\n <li className=\"flex items-start gap-2 text-sm text-text-secondary\">\n <Icon size={16} className=\"mt-0.5 shrink-0 text-text-muted\" />\n <span>{children}</span>\n </li>\n );\n}\n\nfunction AssessmentIntro({\n assessment,\n attempts,\n onStarted,\n}: {\n assessment: TecAssessment;\n attempts: TecAssessmentAttempt[];\n onStarted: (attempt: TecAssessmentAttempt) => void;\n}) {\n const [consent, setConsent] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [start, { loading }] = useStartAssessment();\n const monitored = assessment.proctoringMode !== 'NONE';\n const left = attemptsLeft(assessment, attempts);\n const fullscreenSupported =\n typeof document !== 'undefined' && document.fullscreenEnabled === true;\n\n const begin = async (): Promise<void> => {\n setError(null);\n // Ask for fullscreen inside the click so the browser allows it.\n if (assessment.requireFullscreen && monitored && fullscreenSupported) {\n await document.documentElement.requestFullscreen().catch(() => undefined);\n }\n try {\n const res = await start({\n variables: { assessmentId: assessment.id, acceptMonitoring: monitored ? consent : false },\n });\n const attempt = res.data?.tecStartAssessment;\n if (attempt) onStarted(attempt);\n } catch (err) {\n setError(userMessageFromError(err as Error));\n if (document.fullscreenElement) void document.exitFullscreen().catch(() => undefined);\n }\n };\n\n return (\n <GlassCard className=\"space-y-5 p-6\" data-testid=\"assessment-intro\">\n {assessment.description ? (\n <p className=\"text-sm text-text-secondary\">{assessment.description}</p>\n ) : null}\n <ul className=\"space-y-2\">\n <Rule icon={Timer}>\n {assessment.timeLimitMinutes} minutes, timed by the server from the moment you start.\n Unsubmitted answers are saved as you go and graded when time runs out.\n </Rule>\n <Rule icon={CheckCircle2}>\n {assessment.questionCount} questions, presented in a random order. You need{' '}\n {assessment.passingScore}% to pass.\n </Rule>\n <Rule icon={RotateCcw}>\n {left} of {assessment.maxAttempts} attempt{assessment.maxAttempts === 1 ? '' : 's'} left.\n </Rule>\n <Rule icon={ShieldCheck}>{proctoringModeDescription(assessment.proctoringMode)}</Rule>\n {monitored && assessment.requireFullscreen ? (\n <Rule icon={Maximize}>The exam runs in fullscreen. Leaving fullscreen is recorded.</Rule>\n ) : null}\n {monitored && assessment.blockClipboard ? (\n <Rule icon={XCircle}>\n Copy, cut, paste and the right-click menu are disabled and recorded.\n </Rule>\n ) : null}\n {assessment.issuesCertificate ? (\n <Rule icon={Award}>\n Passing earns the course certificate once the integrity check is cleared.\n </Rule>\n ) : null}\n </ul>\n\n {monitored ? (\n <label className=\"flex items-start gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n className=\"mt-0.5\"\n checked={consent}\n onChange={(e) => setConsent(e.target.checked)}\n data-testid=\"assessment-consent\"\n />\n <span>\n I agree that integrity events from this browser tab are recorded for this attempt. No\n camera, microphone or screen recording is used.\n </span>\n </label>\n ) : null}\n\n {error ? (\n <p role=\"alert\" className=\"text-sm text-status-error-text\">\n {error}\n </p>\n ) : null}\n\n <Button\n onClick={() => void begin()}\n disabled={loading || left === 0 || (monitored && !consent)}\n data-testid=\"assessment-start\"\n >\n <Timer size={15} /> {loading ? 'Starting…' : 'Start assessment'}\n </Button>\n </GlassCard>\n );\n}\n\nfunction AssessmentExam({\n attempt,\n onClosed,\n}: {\n attempt: TecAssessmentAttempt;\n onClosed: (attempt: TecAssessmentAttempt | null, message?: string) => void;\n}) {\n const [answers, setAnswers] = useState<number[]>(() =>\n attempt.questions.map((_, i) => attempt.savedAnswers[i] ?? -1)\n );\n const [violations, setViolations] = useState(attempt.integrityViolations);\n // Deadline on the local clock, from the server's remaining seconds (immune to clock skew).\n // The component is keyed by attempt id, so this is computed once per attempt.\n const [deadline] = useState(() => Date.now() + attempt.remainingSeconds * 1000);\n const [remaining, setRemaining] = useState(attempt.remainingSeconds);\n const [save] = useSaveAssessmentAnswers();\n const [recordSignal] = useRecordAssessmentSignal();\n const [submit, { loading: submitting }] = useSubmitAssessment();\n const closedRef = useRef(false);\n // After a failed submit, the countdown waits this long before auto-retrying.\n const retryAfterRef = useRef(0);\n const answersRef = useRef(answers);\n useEffect(() => {\n answersRef.current = answers;\n }, [answers]);\n const monitored = attempt.proctoringMode !== 'NONE';\n\n const onSignal = useCallback(\n (signal: TecAssessmentSignal, detail?: Record<string, string | number | boolean>) => {\n if (closedRef.current) return;\n void recordSignal({ variables: { attemptId: attempt.id, signal, detail: detail ?? null } })\n .then((res) => {\n const updated = res.data?.tecRecordAssessmentSignal;\n if (updated) setViolations(updated.integrityViolations);\n })\n .catch(() => undefined);\n },\n [attempt.id, recordSignal]\n );\n\n const integrity = useAssessmentIntegrity({\n active: monitored,\n requireFullscreen: attempt.requireFullscreen,\n blockClipboard: attempt.blockClipboard,\n onSignal,\n });\n const fullscreenGate =\n monitored &&\n attempt.requireFullscreen &&\n integrity.fullscreenSupported &&\n !integrity.isFullscreen;\n\n const reopen = useCallback((): void => {\n closedRef.current = false;\n retryAfterRef.current = Date.now() + SUBMIT_RETRY_MS;\n }, []);\n\n const finish = useCallback(\n async (finalAnswers: number[]): Promise<void> => {\n if (closedRef.current) return;\n closedRef.current = true;\n try {\n const res = await submit({ variables: { attemptId: attempt.id, answers: finalAnswers } });\n const closed = res.data?.tecSubmitAssessment ?? null;\n if (!closed || closed.status === 'IN_PROGRESS') reopen();\n onClosed(closed);\n } catch (err) {\n // The request may never have reached the server (e.g. offline): keep\n // the exam open so the learner can submit again and autosave resumes.\n // The parent refetches the attempt; if the server did close it (a late\n // submit is graded on the saved answers), the result replaces the exam.\n reopen();\n onClosed(null, userMessageFromError(err as Error));\n }\n },\n [attempt.id, onClosed, reopen, submit]\n );\n\n // Countdown; auto-submit at zero.\n useEffect(() => {\n const tick = (): void => {\n const left = Math.max(0, Math.round((deadline - Date.now()) / 1000));\n setRemaining(left);\n if (left === 0 && Date.now() >= retryAfterRef.current) void finish(answersRef.current);\n };\n tick();\n const handle = window.setInterval(tick, 1000);\n return () => window.clearInterval(handle);\n }, [deadline, finish]);\n\n // Autosaves are serialized: while one is in flight, later changes are\n // coalesced and the newest answers are saved once it finishes, so an older\n // snapshot can never land after a newer one. A failed save keeps the\n // answers dirty and retries with backoff; the header shows the save state.\n const savingRef = useRef(false);\n const dirtyRef = useRef(false);\n const retryRef = useRef<number | undefined>(undefined);\n const failuresRef = useRef(0);\n // Cleared on unmount, so an abandoned exam never saves or retries again.\n const mountedRef = useRef(true);\n const [saveState, setSaveState] = useState<'saved' | 'saving' | 'unsaved'>('saved');\n const flushSave = useCallback(async (): Promise<void> => {\n if (!mountedRef.current) return;\n if (savingRef.current) {\n dirtyRef.current = true;\n return;\n }\n if (retryRef.current !== undefined) {\n window.clearTimeout(retryRef.current);\n retryRef.current = undefined;\n }\n savingRef.current = true;\n setSaveState('saving');\n let failed = false;\n try {\n do {\n dirtyRef.current = false;\n if (closedRef.current || !mountedRef.current) break;\n try {\n await save({ variables: { attemptId: attempt.id, answers: answersRef.current } });\n failuresRef.current = 0;\n } catch {\n failed = true;\n dirtyRef.current = false;\n break;\n }\n } while (dirtyRef.current);\n } finally {\n savingRef.current = false;\n }\n if (closedRef.current || !mountedRef.current) return;\n if (failed) {\n setSaveState('unsaved');\n failuresRef.current += 1;\n const delay = Math.min(AUTOSAVE_RETRY_MAX_MS, AUTOSAVE_MS * 2 ** failuresRef.current);\n retryRef.current = window.setTimeout(() => void flushSave(), delay);\n } else {\n setSaveState('saved');\n }\n }, [attempt.id, save]);\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n if (retryRef.current !== undefined) window.clearTimeout(retryRef.current);\n };\n }, []);\n\n // Autosave after the learner stops changing answers.\n useEffect(() => {\n if (closedRef.current) return undefined;\n const handle = window.setTimeout(() => void flushSave(), AUTOSAVE_MS);\n return () => window.clearTimeout(handle);\n }, [answers, flushSave]);\n\n const answered = answeredCount(answers);\n const lowTime = remaining <= 60;\n\n return (\n <div className=\"space-y-4\" data-testid=\"assessment-exam\">\n <GlassCard className=\"sticky top-0 z-10 flex flex-wrap items-center justify-between gap-3 p-4\">\n <div\n className={cn(\n 'flex items-center gap-2 font-mono text-lg font-semibold',\n lowTime ? 'text-status-error-text' : 'text-text-primary'\n )}\n aria-live=\"polite\"\n data-testid=\"assessment-timer\"\n >\n <Clock size={18} /> {formatClock(remaining)}\n </div>\n <span className=\"text-sm text-text-secondary\">\n {answered} of {attempt.questions.length} answered\n </span>\n <span\n className={cn(\n 'text-xs',\n saveState === 'unsaved' ? 'text-status-warning-text' : 'text-text-muted'\n )}\n role=\"status\"\n data-testid=\"assessment-save-state\"\n >\n {saveState === 'saving'\n ? 'Saving…'\n : saveState === 'unsaved'\n ? 'Answers not saved yet, retrying…'\n : 'Answers saved'}\n </span>\n {monitored ? (\n <span className=\"text-sm text-text-secondary\" data-testid=\"assessment-violations\">\n Integrity events: {violations}\n {attempt.proctoringMode === 'INTEGRITY_MONITORING'\n ? ` (allowed ${attempt.maxIntegrityViolations})`\n : ''}\n </span>\n ) : null}\n <Button\n onClick={() => {\n if (window.confirm('Submit your answers? You cannot change them afterwards.')) {\n void finish(answers);\n }\n }}\n disabled={submitting}\n data-testid=\"assessment-submit\"\n >\n <Send size={15} /> {submitting ? 'Submitting…' : 'Submit'}\n </Button>\n </GlassCard>\n\n {fullscreenGate ? (\n <GlassCard className=\"space-y-3 p-6 text-center\" data-testid=\"assessment-fullscreen-gate\">\n <p className=\"text-sm text-text-primary\">\n This exam runs in fullscreen. Leaving fullscreen was recorded. Return to fullscreen to\n continue.\n </p>\n <Button onClick={() => void integrity.enterFullscreen()}>\n <Maximize size={15} /> Return to fullscreen\n </Button>\n </GlassCard>\n ) : (\n <div className={cn('space-y-4', attempt.blockClipboard && monitored && 'select-none')}>\n {attempt.questions.map((question) => (\n <fieldset\n key={question.position}\n className=\"rounded-xl border border-border-seam bg-bg-surface p-4\"\n disabled={submitting}\n data-testid=\"assessment-question\"\n >\n <legend className=\"px-1 text-sm font-semibold text-text-primary\">\n {question.position + 1}. {question.prompt}\n </legend>\n <div className=\"mt-2 space-y-2\">\n {question.options.map((option, oi) => {\n const chosen = answers[question.position] === oi;\n return (\n <label\n key={oi}\n className={cn(\n 'flex cursor-pointer items-center gap-3 rounded-lg border px-3 py-2 text-sm transition-colors',\n chosen ? 'border-accent-primary bg-accent-primary/10' : 'border-border-seam'\n )}\n >\n <input\n type=\"radio\"\n name={`tec-assessment-${attempt.id}-${question.position}`}\n checked={chosen}\n onChange={() =>\n setAnswers((prev) =>\n prev.map((a, i) => (i === question.position ? oi : a))\n )\n }\n />\n <span className=\"text-text-primary\">{option}</span>\n </label>\n );\n })}\n </div>\n </fieldset>\n ))}\n </div>\n )}\n </div>\n );\n}\n\nfunction AttemptResult({\n attempt,\n assessment,\n canRetake,\n onRetake,\n}: {\n attempt: TecAssessmentAttempt;\n assessment: TecAssessment;\n canRetake: boolean;\n onRetake: () => void;\n}) {\n const [claim, { loading: claiming }] = useClaimAssessmentCertificate();\n const passed = attempt.passed === true;\n return (\n <GlassCard className=\"space-y-4 p-6\" data-testid=\"assessment-result\">\n <div className=\"flex flex-wrap items-center gap-3\">\n {passed ? (\n <CheckCircle2 size={22} className=\"text-status-success-text\" />\n ) : (\n <XCircle size={22} className=\"text-status-error-text\" />\n )}\n <h2 className=\"text-lg font-semibold text-text-primary\" data-testid=\"assessment-score\">\n {attempt.score ?? 0}% · {passed ? 'Passed' : 'Not passed'}\n </h2>\n <StatusBadge status={attempt.status} />\n <StatusBadge status={attempt.proctoringStatus} />\n </div>\n <p className=\"text-sm text-text-secondary\">\n {attempt.correctCount} of {attempt.totalCount} correct; {attempt.passingScore}% needed to\n pass. Attempt {attempt.attemptNumber}\n {attempt.submittedAt ? `, closed ${formatDateTime(attempt.submittedAt)}` : ''}.\n </p>\n <p className=\"text-sm text-text-secondary\" data-testid=\"assessment-proctoring\">\n {proctoringStatusDescription(attempt.proctoringStatus)}\n {attempt.proctoringMode !== 'NONE'\n ? ` Integrity events recorded: ${attempt.integrityViolations}.`\n : ''}\n </p>\n {attempt.reviewNotes ? (\n <p className=\"rounded-lg bg-bg-sunken px-3 py-2 text-sm text-text-secondary\">\n Reviewer note: {attempt.reviewNotes}\n </p>\n ) : null}\n {attempt.certificate ? (\n <p\n className=\"flex items-center gap-2 rounded-lg border border-status-success-border bg-status-success-bg-subtle px-3 py-2 text-sm text-status-success-text\"\n data-testid=\"assessment-certificate\"\n >\n <Award size={16} /> Certificate issued · serial {attempt.certificate.serial}\n </p>\n ) : null}\n <div className=\"flex flex-wrap gap-2\">\n {certificatePending(attempt, assessment) ? (\n <Button\n variant=\"outline\"\n disabled={claiming}\n onClick={() =>\n void claim({ variables: { attemptId: attempt.id } }).then((res) => {\n if (res.data?.tecClaimAssessmentCertificate.certificate) {\n notify.success('Certificate issued', 'Your certificate is ready.');\n }\n })\n }\n >\n <Award size={15} /> {claiming ? 'Issuing…' : 'Issue my certificate'}\n </Button>\n ) : null}\n {canRetake ? (\n <Button variant=\"outline\" onClick={onRetake} data-testid=\"assessment-retake\">\n <RotateCcw size={15} /> Take another attempt\n </Button>\n ) : null}\n </div>\n </GlassCard>\n );\n}\n\n/**\n * Certification assessment (BOFF-4427): a timed, integrity-monitored exam on\n * a course. Intro with the rules and the monitoring consent, the exam itself\n * (server deadline, shuffled questions, autosave, integrity signals,\n * fullscreen gate) and the graded result with the proctoring status and the\n * certificate. URL: `/courses/assessment-page?assessmentId=<id>`.\n */\nexport function CoursesAssessmentPagePage() {\n const [params] = useSearchParams();\n const assessmentId = params.get('assessmentId') ?? '';\n const { data, loading, error } = useAssessmentQuery(assessmentId);\n const attemptsQuery = useMyAssessmentAttemptsQuery(assessmentId || null);\n const refetchAttempts = attemptsQuery.refetch;\n const [active, setActive] = useState<TecAssessmentAttempt | null>(null);\n const [showIntro, setShowIntro] = useState(false);\n const [closedMessage, setClosedMessage] = useState<string | null>(null);\n\n const assessment = data?.tecAssessment ?? null;\n const attempts = attemptsQuery.data?.tecMyAssessmentAttempts ?? [];\n const inProgress = active ?? attempts.find((a) => a.status === 'IN_PROGRESS') ?? null;\n const latestClosed = attempts.find((a) => a.status !== 'IN_PROGRESS') ?? null;\n const backPath = assessment ? courseDetailsPath(assessment.courseId) : '/courses/courses-page';\n\n const handleClosed = useCallback(\n (closed: TecAssessmentAttempt | null, message?: string) => {\n if (!closed || closed.status === 'IN_PROGRESS') {\n // The submit did not close the attempt (e.g. a network failure): keep\n // the exam mounted so the learner can submit again; the countdown\n // retries on its own once time is up, and the server returns the\n // closed attempt if it already graded it.\n setClosedMessage(message ?? null);\n return;\n }\n setActive(null);\n setShowIntro(false);\n setClosedMessage(message ?? null);\n notify.success('Assessment submitted', `You scored ${closed.score ?? 0}%.`);\n void refetchAttempts();\n },\n [refetchAttempts]\n );\n\n if (!assessmentId) {\n return (\n <PageLayout title=\"Assessment\" backPath=\"/courses/courses-page\">\n <EmptyView\n title=\"No assessment selected\"\n description=\"Open an assessment from a course page.\"\n />\n </PageLayout>\n );\n }\n if ((loading && !assessment) || (attemptsQuery.loading && !attemptsQuery.data)) {\n return (\n <PageLayout title=\"Assessment\" backPath={backPath}>\n <Skeleton className=\"mb-4 h-10 w-1/2 rounded-xl\" />\n <Skeleton className=\"h-64 w-full rounded-xl\" />\n </PageLayout>\n );\n }\n if (error || attemptsQuery.error) {\n return (\n <PageLayout title=\"Assessment\" backPath={backPath}>\n <ErrorView title=\"Could not load this assessment\" error={error ?? attemptsQuery.error} />\n </PageLayout>\n );\n }\n if (!assessment) {\n return (\n <PageLayout title=\"Assessment\" backPath=\"/courses/courses-page\">\n <EmptyView\n title=\"Assessment not found\"\n description=\"It may have been removed from this workspace.\"\n />\n </PageLayout>\n );\n }\n\n const left = attemptsLeft(assessment, attempts);\n const open = assessment.status === 'PUBLISHED';\n\n let body: React.ReactNode;\n if (inProgress) {\n body = <AssessmentExam key={inProgress.id} attempt={inProgress} onClosed={handleClosed} />;\n } else if (latestClosed && !showIntro) {\n body = (\n <AttemptResult\n attempt={latestClosed}\n assessment={assessment}\n canRetake={open && left > 0}\n onRetake={() => setShowIntro(true)}\n />\n );\n } else if (!open) {\n body = (\n <EmptyView\n title=\"This assessment is closed\"\n description=\"It is not accepting new attempts.\"\n />\n );\n } else {\n body = <AssessmentIntro assessment={assessment} attempts={attempts} onStarted={setActive} />;\n }\n\n return (\n <PageLayout\n title={assessment.title}\n description={\n assessment.course?.title\n ? `Certification assessment · ${assessment.course.title}`\n : undefined\n }\n backPath={inProgress ? undefined : backPath}\n actions={<StatusBadge status={assessment.status} />}\n >\n <div className=\"mx-auto w-full max-w-3xl space-y-4\">\n {closedMessage ? (\n <p\n role=\"status\"\n className=\"rounded-lg bg-bg-sunken px-3 py-2 text-sm text-text-secondary\"\n >\n {closedMessage}\n </p>\n ) : null}\n {body}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CoursesCourseDetailsPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/CoursesCourseDetailsPagePage.tsx"],"names":[],"mappings":"AAoSA,wBAAgB,4BAA4B,gCA4V3C"}
1
+ {"version":3,"file":"CoursesCourseDetailsPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/CoursesCourseDetailsPagePage.tsx"],"names":[],"mappings":"AAqSA,wBAAgB,4BAA4B,gCA8V3C"}