@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.
- package/dist/generated/global-types.d.ts +307 -41
- package/dist/generated/global-types.d.ts.map +1 -1
- package/dist/generated/wspace-types.d.ts +1730 -8
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/stories/fixtures/technospamFixtures.d.ts.map +1 -1
- package/dist/technospam/TechnoSpamRoutes.d.ts.map +1 -1
- package/dist/technospam/TechnoSpamRoutes.js +32 -28
- package/dist/technospam/TechnoSpamRoutes.js.map +1 -1
- package/dist/technospam/api/assessmentApi.d.ts +183 -0
- package/dist/technospam/api/assessmentApi.d.ts.map +1 -0
- package/dist/technospam/api/assessmentApi.js +278 -0
- package/dist/technospam/api/assessmentApi.js.map +1 -0
- package/dist/technospam/components/CourseAssessments.d.ts +9 -0
- package/dist/technospam/components/CourseAssessments.d.ts.map +1 -0
- package/dist/technospam/components/CourseAssessments.js +66 -0
- package/dist/technospam/components/CourseAssessments.js.map +1 -0
- package/dist/technospam/components/StatusBadge.d.ts.map +1 -1
- package/dist/technospam/components/StatusBadge.js +8 -1
- package/dist/technospam/components/StatusBadge.js.map +1 -1
- package/dist/technospam/components/admin/AssessmentsAdminTab.d.ts +7 -0
- package/dist/technospam/components/admin/AssessmentsAdminTab.d.ts.map +1 -0
- package/dist/technospam/components/admin/AssessmentsAdminTab.js +421 -0
- package/dist/technospam/components/admin/AssessmentsAdminTab.js.map +1 -0
- package/dist/technospam/components/lesson/LessonKindFields.d.ts +3 -1
- package/dist/technospam/components/lesson/LessonKindFields.d.ts.map +1 -1
- package/dist/technospam/components/lesson/LessonKindFields.js +2 -2
- package/dist/technospam/components/lesson/LessonKindFields.js.map +1 -1
- package/dist/technospam/hooks/useAssessmentIntegrity.d.ts +29 -0
- package/dist/technospam/hooks/useAssessmentIntegrity.d.ts.map +1 -0
- package/dist/technospam/hooks/useAssessmentIntegrity.js +57 -0
- package/dist/technospam/hooks/useAssessmentIntegrity.js.map +1 -0
- package/dist/technospam/pages/AdminAdminDashboardPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/AdminAdminDashboardPagePage.js +364 -359
- package/dist/technospam/pages/AdminAdminDashboardPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesAssessmentPagePage.d.ts +9 -0
- package/dist/technospam/pages/CoursesAssessmentPagePage.d.ts.map +1 -0
- package/dist/technospam/pages/CoursesAssessmentPagePage.js +464 -0
- package/dist/technospam/pages/CoursesAssessmentPagePage.js.map +1 -0
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +210 -208
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
- package/dist/technospam/utils/assessment.d.ts +64 -0
- package/dist/technospam/utils/assessment.d.ts.map +1 -0
- package/dist/technospam/utils/assessment.js +122 -0
- package/dist/technospam/utils/assessment.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { formatDateTime as e, humanizeEnum as t } from "../../utils/format.js";
|
|
2
|
+
import { StatusBadge as n } from "../StatusBadge.js";
|
|
3
|
+
import { EmptyView as r, ErrorView as i, LoadingRows as a } from "../StateViews.js";
|
|
4
|
+
import { notify as o } from "../../lib/toast.js";
|
|
5
|
+
import { useCoursesQuery as s } from "../../api/technospamApi.js";
|
|
6
|
+
import { draftFromLesson as c, draftToInput as l } from "../lesson/lessonAuthoring.js";
|
|
7
|
+
import { LessonKindFields as u } from "../lesson/LessonKindFields.js";
|
|
8
|
+
import { useArchiveAssessment as d, useAssessmentAttemptEventsQuery as f, useAssessmentAttemptsQuery as p, useAssessmentQuestionKeyQuery as m, useAssessmentsQuery as h, useCreateAssessment as g, usePublishAssessment as _, useReviewAssessmentAttempt as v, useUpdateAssessment as y } from "../../api/assessmentApi.js";
|
|
9
|
+
import { assessmentSettingsToInput as b, draftFromAssessment as x, emptyAssessmentDraft as S } from "../../utils/assessment.js";
|
|
10
|
+
import { useState as C } from "react";
|
|
11
|
+
import { Archive as w, Check as T, ListChecks as E, Pencil as D, Plus as O, Rocket as k, ShieldCheck as A, X as j } from "lucide-react";
|
|
12
|
+
import { Fragment as M, jsx as N, jsxs as P } from "react/jsx-runtime";
|
|
13
|
+
import { Button as F, GlassCard as I, Input as L, Select as R, SelectContent as z, SelectItem as B, SelectTrigger as V, SelectValue as H, Textarea as U } from "@burdenoff/fe-libs/ui";
|
|
14
|
+
//#region src/technospam/components/admin/AssessmentsAdminTab.tsx
|
|
15
|
+
var W = [
|
|
16
|
+
"INTEGRITY_MONITORING",
|
|
17
|
+
"REVIEW_REQUIRED",
|
|
18
|
+
"NONE"
|
|
19
|
+
], G = [
|
|
20
|
+
"FLAGGED",
|
|
21
|
+
"PENDING_REVIEW",
|
|
22
|
+
"ALL"
|
|
23
|
+
];
|
|
24
|
+
function K({ label: e, children: t }) {
|
|
25
|
+
return /* @__PURE__ */ P("label", {
|
|
26
|
+
className: "flex flex-col gap-1.5",
|
|
27
|
+
children: [/* @__PURE__ */ N("span", {
|
|
28
|
+
className: "text-xs font-medium text-text-secondary",
|
|
29
|
+
children: e
|
|
30
|
+
}), t]
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function q({ label: e, checked: t, onChange: n }) {
|
|
34
|
+
return /* @__PURE__ */ P("label", {
|
|
35
|
+
className: "flex items-center gap-2 text-sm text-text-primary",
|
|
36
|
+
children: [/* @__PURE__ */ N("input", {
|
|
37
|
+
type: "checkbox",
|
|
38
|
+
checked: t,
|
|
39
|
+
onChange: (e) => n(e.target.checked)
|
|
40
|
+
}), e]
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function J({ courseId: e, assessment: n, onDone: r }) {
|
|
44
|
+
let [i, s] = C(() => n ? x(n) : S()), { data: d, loading: f } = m(n?.id ?? null), [p, h] = C(n ? null : c()), [_, { loading: v }] = g(), [w, { loading: E }] = y(), D = p ?? (d ? c(null, d.tecAssessmentQuestionKey) : null), O = (e, t) => s((n) => ({
|
|
45
|
+
...n,
|
|
46
|
+
[e]: t
|
|
47
|
+
})), k = async () => {
|
|
48
|
+
let t = b(i);
|
|
49
|
+
if (!t.ok) return o.error(t.error);
|
|
50
|
+
let a = {};
|
|
51
|
+
if (D) {
|
|
52
|
+
let e = l("QUIZ", D);
|
|
53
|
+
if (!e.ok) return o.error(e.error);
|
|
54
|
+
a = { questions: e.input.quizQuestions };
|
|
55
|
+
}
|
|
56
|
+
if (n) {
|
|
57
|
+
(await w({ variables: {
|
|
58
|
+
id: n.id,
|
|
59
|
+
input: {
|
|
60
|
+
...t.input,
|
|
61
|
+
...a
|
|
62
|
+
}
|
|
63
|
+
} })).data?.tecUpdateAssessment && (o.success("Assessment saved"), r());
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
(await _({ variables: { input: {
|
|
67
|
+
courseId: e,
|
|
68
|
+
...t.input,
|
|
69
|
+
...a
|
|
70
|
+
} } })).data?.tecCreateAssessment && (o.success("Assessment created", "Publish it when the questions are ready."), r());
|
|
71
|
+
};
|
|
72
|
+
return /* @__PURE__ */ P(I, {
|
|
73
|
+
className: "space-y-4 p-4 sm:p-5",
|
|
74
|
+
"data-testid": "assessment-form",
|
|
75
|
+
children: [
|
|
76
|
+
/* @__PURE__ */ P("div", {
|
|
77
|
+
className: "grid grid-cols-1 gap-3 sm:grid-cols-2",
|
|
78
|
+
children: [
|
|
79
|
+
/* @__PURE__ */ N(K, {
|
|
80
|
+
label: "Title",
|
|
81
|
+
children: /* @__PURE__ */ N(L, {
|
|
82
|
+
value: i.title,
|
|
83
|
+
onChange: (e) => O("title", e.target.value)
|
|
84
|
+
})
|
|
85
|
+
}),
|
|
86
|
+
/* @__PURE__ */ N(K, {
|
|
87
|
+
label: "Integrity mode",
|
|
88
|
+
children: /* @__PURE__ */ P(R, {
|
|
89
|
+
value: i.proctoringMode,
|
|
90
|
+
onValueChange: (e) => O("proctoringMode", e),
|
|
91
|
+
children: [/* @__PURE__ */ N(V, {
|
|
92
|
+
"aria-label": "Integrity mode",
|
|
93
|
+
children: /* @__PURE__ */ N(H, {})
|
|
94
|
+
}), /* @__PURE__ */ N(z, { children: W.map((e) => /* @__PURE__ */ N(B, {
|
|
95
|
+
value: e,
|
|
96
|
+
children: t(e)
|
|
97
|
+
}, e)) })]
|
|
98
|
+
})
|
|
99
|
+
}),
|
|
100
|
+
/* @__PURE__ */ N(K, {
|
|
101
|
+
label: "Time limit (minutes)",
|
|
102
|
+
children: /* @__PURE__ */ N(L, {
|
|
103
|
+
inputMode: "numeric",
|
|
104
|
+
value: i.timeLimitMinutes,
|
|
105
|
+
onChange: (e) => O("timeLimitMinutes", e.target.value)
|
|
106
|
+
})
|
|
107
|
+
}),
|
|
108
|
+
/* @__PURE__ */ N(K, {
|
|
109
|
+
label: "Pass mark (%)",
|
|
110
|
+
children: /* @__PURE__ */ N(L, {
|
|
111
|
+
inputMode: "numeric",
|
|
112
|
+
value: i.passingScore,
|
|
113
|
+
onChange: (e) => O("passingScore", e.target.value)
|
|
114
|
+
})
|
|
115
|
+
}),
|
|
116
|
+
/* @__PURE__ */ N(K, {
|
|
117
|
+
label: "Attempts per learner",
|
|
118
|
+
children: /* @__PURE__ */ N(L, {
|
|
119
|
+
inputMode: "numeric",
|
|
120
|
+
value: i.maxAttempts,
|
|
121
|
+
onChange: (e) => O("maxAttempts", e.target.value)
|
|
122
|
+
})
|
|
123
|
+
}),
|
|
124
|
+
/* @__PURE__ */ N(K, {
|
|
125
|
+
label: "Allowed integrity events",
|
|
126
|
+
children: /* @__PURE__ */ N(L, {
|
|
127
|
+
inputMode: "numeric",
|
|
128
|
+
value: i.maxIntegrityViolations,
|
|
129
|
+
onChange: (e) => O("maxIntegrityViolations", e.target.value)
|
|
130
|
+
})
|
|
131
|
+
}),
|
|
132
|
+
/* @__PURE__ */ N("div", {
|
|
133
|
+
className: "sm:col-span-2",
|
|
134
|
+
children: /* @__PURE__ */ N(K, {
|
|
135
|
+
label: "Description",
|
|
136
|
+
children: /* @__PURE__ */ N(U, {
|
|
137
|
+
value: i.description,
|
|
138
|
+
onChange: (e) => O("description", e.target.value)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
}),
|
|
142
|
+
/* @__PURE__ */ P("div", {
|
|
143
|
+
className: "flex flex-wrap gap-x-5 gap-y-2 sm:col-span-2",
|
|
144
|
+
children: [
|
|
145
|
+
/* @__PURE__ */ N(q, {
|
|
146
|
+
label: "Require fullscreen",
|
|
147
|
+
checked: i.requireFullscreen,
|
|
148
|
+
onChange: (e) => O("requireFullscreen", e)
|
|
149
|
+
}),
|
|
150
|
+
/* @__PURE__ */ N(q, {
|
|
151
|
+
label: "Block copy and paste",
|
|
152
|
+
checked: i.blockClipboard,
|
|
153
|
+
onChange: (e) => O("blockClipboard", e)
|
|
154
|
+
}),
|
|
155
|
+
/* @__PURE__ */ N(q, {
|
|
156
|
+
label: "Shuffle questions",
|
|
157
|
+
checked: i.shuffleQuestions,
|
|
158
|
+
onChange: (e) => O("shuffleQuestions", e)
|
|
159
|
+
}),
|
|
160
|
+
/* @__PURE__ */ N(q, {
|
|
161
|
+
label: "Shuffle options",
|
|
162
|
+
checked: i.shuffleOptions,
|
|
163
|
+
onChange: (e) => O("shuffleOptions", e)
|
|
164
|
+
}),
|
|
165
|
+
/* @__PURE__ */ N(q, {
|
|
166
|
+
label: "Issue course certificate",
|
|
167
|
+
checked: i.issuesCertificate,
|
|
168
|
+
onChange: (e) => O("issuesCertificate", e)
|
|
169
|
+
})
|
|
170
|
+
]
|
|
171
|
+
})
|
|
172
|
+
]
|
|
173
|
+
}),
|
|
174
|
+
D ? /* @__PURE__ */ N(u, {
|
|
175
|
+
kind: "QUIZ",
|
|
176
|
+
draft: D,
|
|
177
|
+
onChange: h,
|
|
178
|
+
heading: `Questions (learners need ${i.passingScore || "?"}% to pass)`
|
|
179
|
+
}) : f ? /* @__PURE__ */ N(a, { count: 2 }) : null,
|
|
180
|
+
/* @__PURE__ */ P("div", {
|
|
181
|
+
className: "flex justify-end gap-2 border-t border-border-seam pt-3",
|
|
182
|
+
children: [/* @__PURE__ */ P(F, {
|
|
183
|
+
variant: "outline",
|
|
184
|
+
onClick: r,
|
|
185
|
+
children: [/* @__PURE__ */ N(j, { size: 15 }), " Cancel"]
|
|
186
|
+
}), /* @__PURE__ */ P(F, {
|
|
187
|
+
onClick: () => void k(),
|
|
188
|
+
disabled: v || E,
|
|
189
|
+
"data-testid": "assessment-save",
|
|
190
|
+
children: [
|
|
191
|
+
/* @__PURE__ */ N(T, { size: 15 }),
|
|
192
|
+
" ",
|
|
193
|
+
v || E ? "Saving…" : "Save assessment"
|
|
194
|
+
]
|
|
195
|
+
})]
|
|
196
|
+
})
|
|
197
|
+
]
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function Y({ attempt: r, title: i }) {
|
|
201
|
+
let [a, s] = C(!1), [c, l] = C(""), { data: u } = f(a ? r.id : null), [d, { loading: p }] = v(), m = r.proctoringStatus === "FLAGGED" || r.proctoringStatus === "PENDING_REVIEW", h = async (e) => {
|
|
202
|
+
let t = (await d({ variables: {
|
|
203
|
+
id: r.id,
|
|
204
|
+
decision: e,
|
|
205
|
+
notes: c.trim() || null
|
|
206
|
+
} })).data?.tecReviewAssessmentAttempt;
|
|
207
|
+
t && o.success(e === "VERIFIED" ? "Attempt verified" : "Attempt rejected", t.certificateId ? "The certificate was issued." : void 0);
|
|
208
|
+
};
|
|
209
|
+
return /* @__PURE__ */ P(I, {
|
|
210
|
+
className: "space-y-3 p-4",
|
|
211
|
+
"data-testid": "assessment-review-row",
|
|
212
|
+
children: [/* @__PURE__ */ P("div", {
|
|
213
|
+
className: "flex flex-wrap items-center justify-between gap-3",
|
|
214
|
+
children: [/* @__PURE__ */ P("div", {
|
|
215
|
+
className: "min-w-0",
|
|
216
|
+
children: [/* @__PURE__ */ N("div", {
|
|
217
|
+
className: "truncate text-sm font-semibold text-text-primary",
|
|
218
|
+
children: i
|
|
219
|
+
}), /* @__PURE__ */ P("div", {
|
|
220
|
+
className: "text-xs text-text-muted",
|
|
221
|
+
children: [
|
|
222
|
+
"Learner ",
|
|
223
|
+
r.learnerId,
|
|
224
|
+
" · attempt ",
|
|
225
|
+
r.attemptNumber,
|
|
226
|
+
" · ",
|
|
227
|
+
r.score ?? 0,
|
|
228
|
+
"% (",
|
|
229
|
+
r.passed ? "passed" : "not passed",
|
|
230
|
+
") · ",
|
|
231
|
+
r.integrityViolations,
|
|
232
|
+
" integrity events · ",
|
|
233
|
+
e(r.submittedAt ?? r.startedAt)
|
|
234
|
+
]
|
|
235
|
+
})]
|
|
236
|
+
}), /* @__PURE__ */ P("div", {
|
|
237
|
+
className: "flex items-center gap-2",
|
|
238
|
+
children: [/* @__PURE__ */ N(n, { status: r.proctoringStatus }), /* @__PURE__ */ N(F, {
|
|
239
|
+
size: "sm",
|
|
240
|
+
variant: "outline",
|
|
241
|
+
onClick: () => s((e) => !e),
|
|
242
|
+
children: a ? "Hide" : "Review"
|
|
243
|
+
})]
|
|
244
|
+
})]
|
|
245
|
+
}), a ? /* @__PURE__ */ P("div", {
|
|
246
|
+
className: "space-y-3",
|
|
247
|
+
children: [/* @__PURE__ */ N("ol", {
|
|
248
|
+
className: "max-h-56 space-y-1 overflow-auto rounded-lg bg-bg-sunken p-3 text-xs",
|
|
249
|
+
"data-testid": "assessment-events",
|
|
250
|
+
children: (u?.tecAssessmentAttemptEvents ?? []).map((n) => /* @__PURE__ */ P("li", {
|
|
251
|
+
className: n.violation ? "text-status-warning-text" : "text-text-secondary",
|
|
252
|
+
children: [
|
|
253
|
+
e(n.occurredAt),
|
|
254
|
+
" · ",
|
|
255
|
+
t(n.type),
|
|
256
|
+
n.violation ? " (violation)" : ""
|
|
257
|
+
]
|
|
258
|
+
}, n.id))
|
|
259
|
+
}), m ? /* @__PURE__ */ P(M, { children: [/* @__PURE__ */ N(U, {
|
|
260
|
+
"aria-label": "Review notes",
|
|
261
|
+
placeholder: "Notes for the learner (optional)",
|
|
262
|
+
value: c,
|
|
263
|
+
onChange: (e) => l(e.target.value)
|
|
264
|
+
}), /* @__PURE__ */ P("div", {
|
|
265
|
+
className: "flex gap-2",
|
|
266
|
+
children: [/* @__PURE__ */ P(F, {
|
|
267
|
+
size: "sm",
|
|
268
|
+
disabled: p,
|
|
269
|
+
onClick: () => void h("VERIFIED"),
|
|
270
|
+
children: [/* @__PURE__ */ N(T, { size: 14 }), " Verify"]
|
|
271
|
+
}), /* @__PURE__ */ P(F, {
|
|
272
|
+
size: "sm",
|
|
273
|
+
variant: "outline",
|
|
274
|
+
disabled: p,
|
|
275
|
+
onClick: () => void h("REJECTED"),
|
|
276
|
+
children: [/* @__PURE__ */ N(j, { size: 14 }), " Reject"]
|
|
277
|
+
})]
|
|
278
|
+
})] }) : r.reviewNotes ? /* @__PURE__ */ P("p", {
|
|
279
|
+
className: "text-sm text-text-secondary",
|
|
280
|
+
children: ["Reviewer note: ", r.reviewNotes]
|
|
281
|
+
}) : null]
|
|
282
|
+
}) : null]
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
function X() {
|
|
286
|
+
let { data: e } = s({}), c = e?.tecListCourses?.items ?? [], [l, u] = C(""), f = l || c[0]?.id || "", { data: m, loading: g, error: v } = h({ courseId: f }, !f), [y, b] = C(null), [x] = _(), [S] = d(), [T, j] = C("FLAGGED"), M = p({ proctoringStatus: T === "ALL" ? null : T }), L = m?.tecListAssessments ?? [], U = new Map(L.map((e) => [e.id, e.title]));
|
|
287
|
+
return /* @__PURE__ */ P("div", {
|
|
288
|
+
className: "space-y-6",
|
|
289
|
+
children: [/* @__PURE__ */ P("section", {
|
|
290
|
+
className: "space-y-3",
|
|
291
|
+
children: [
|
|
292
|
+
/* @__PURE__ */ P("div", {
|
|
293
|
+
className: "flex flex-wrap items-end justify-between gap-3",
|
|
294
|
+
children: [/* @__PURE__ */ N(K, {
|
|
295
|
+
label: "Course",
|
|
296
|
+
children: /* @__PURE__ */ P(R, {
|
|
297
|
+
value: f,
|
|
298
|
+
onValueChange: (e) => {
|
|
299
|
+
u(e), b(null);
|
|
300
|
+
},
|
|
301
|
+
children: [/* @__PURE__ */ N(V, {
|
|
302
|
+
"aria-label": "Course",
|
|
303
|
+
className: "min-w-64",
|
|
304
|
+
children: /* @__PURE__ */ N(H, { placeholder: "Choose a course" })
|
|
305
|
+
}), /* @__PURE__ */ N(z, { children: c.map((e) => /* @__PURE__ */ N(B, {
|
|
306
|
+
value: e.id,
|
|
307
|
+
children: e.title
|
|
308
|
+
}, e.id)) })]
|
|
309
|
+
})
|
|
310
|
+
}), f && y === null ? /* @__PURE__ */ P(F, {
|
|
311
|
+
size: "sm",
|
|
312
|
+
onClick: () => b("new"),
|
|
313
|
+
"data-testid": "assessment-new",
|
|
314
|
+
children: [/* @__PURE__ */ N(O, { size: 15 }), " New assessment"]
|
|
315
|
+
}) : null]
|
|
316
|
+
}),
|
|
317
|
+
y !== null && f ? /* @__PURE__ */ N(J, {
|
|
318
|
+
courseId: f,
|
|
319
|
+
assessment: y === "new" ? null : y,
|
|
320
|
+
onDone: () => b(null)
|
|
321
|
+
}, y === "new" ? "new" : y.id) : null,
|
|
322
|
+
f ? v ? /* @__PURE__ */ N(i, {
|
|
323
|
+
title: "Could not load assessments",
|
|
324
|
+
error: v
|
|
325
|
+
}) : g && L.length === 0 ? /* @__PURE__ */ N(a, { count: 2 }) : L.length === 0 ? /* @__PURE__ */ N(r, {
|
|
326
|
+
icon: /* @__PURE__ */ N(A, { size: 26 }),
|
|
327
|
+
title: "No assessments for this course",
|
|
328
|
+
description: "Add a timed, integrity-monitored exam that issues the course certificate."
|
|
329
|
+
}) : /* @__PURE__ */ N("div", {
|
|
330
|
+
className: "space-y-2",
|
|
331
|
+
children: L.map((e) => /* @__PURE__ */ P(I, {
|
|
332
|
+
className: "flex flex-wrap items-center justify-between gap-3 p-4",
|
|
333
|
+
children: [/* @__PURE__ */ P("div", {
|
|
334
|
+
className: "min-w-0",
|
|
335
|
+
children: [/* @__PURE__ */ N("div", {
|
|
336
|
+
className: "truncate text-sm font-semibold text-text-primary",
|
|
337
|
+
children: e.title
|
|
338
|
+
}), /* @__PURE__ */ P("div", {
|
|
339
|
+
className: "text-xs text-text-muted",
|
|
340
|
+
children: [
|
|
341
|
+
e.questionCount,
|
|
342
|
+
" questions · ",
|
|
343
|
+
e.timeLimitMinutes,
|
|
344
|
+
" min · pass",
|
|
345
|
+
" ",
|
|
346
|
+
e.passingScore,
|
|
347
|
+
"% · ",
|
|
348
|
+
t(e.proctoringMode)
|
|
349
|
+
]
|
|
350
|
+
})]
|
|
351
|
+
}), /* @__PURE__ */ P("div", {
|
|
352
|
+
className: "flex flex-wrap items-center gap-2",
|
|
353
|
+
children: [
|
|
354
|
+
/* @__PURE__ */ N(n, { status: e.status }),
|
|
355
|
+
/* @__PURE__ */ P(F, {
|
|
356
|
+
size: "sm",
|
|
357
|
+
variant: "outline",
|
|
358
|
+
onClick: () => b(e),
|
|
359
|
+
children: [/* @__PURE__ */ N(D, { size: 14 }), " Edit"]
|
|
360
|
+
}),
|
|
361
|
+
e.status === "PUBLISHED" ? /* @__PURE__ */ P(F, {
|
|
362
|
+
size: "sm",
|
|
363
|
+
variant: "outline",
|
|
364
|
+
onClick: () => void S({ variables: { id: e.id } }).then((e) => {
|
|
365
|
+
e.data?.tecArchiveAssessment && o.success("Assessment archived");
|
|
366
|
+
}),
|
|
367
|
+
children: [/* @__PURE__ */ N(w, { size: 14 }), " Archive"]
|
|
368
|
+
}) : /* @__PURE__ */ P(F, {
|
|
369
|
+
size: "sm",
|
|
370
|
+
onClick: () => void x({ variables: { id: e.id } }).then((e) => {
|
|
371
|
+
e.data?.tecPublishAssessment && o.success("Assessment published");
|
|
372
|
+
}),
|
|
373
|
+
children: [/* @__PURE__ */ N(k, { size: 14 }), " Publish"]
|
|
374
|
+
})
|
|
375
|
+
]
|
|
376
|
+
})]
|
|
377
|
+
}, e.id))
|
|
378
|
+
}) : /* @__PURE__ */ N(r, {
|
|
379
|
+
title: "No courses yet",
|
|
380
|
+
description: "Create a course first, then add its assessments."
|
|
381
|
+
})
|
|
382
|
+
]
|
|
383
|
+
}), /* @__PURE__ */ P("section", {
|
|
384
|
+
className: "space-y-3",
|
|
385
|
+
children: [/* @__PURE__ */ P("div", {
|
|
386
|
+
className: "flex flex-wrap items-end justify-between gap-3",
|
|
387
|
+
children: [/* @__PURE__ */ P("h3", {
|
|
388
|
+
className: "flex items-center gap-2 text-sm font-semibold text-text-primary",
|
|
389
|
+
children: [/* @__PURE__ */ N(E, { size: 16 }), " Review queue"]
|
|
390
|
+
}), /* @__PURE__ */ P(R, {
|
|
391
|
+
value: T,
|
|
392
|
+
onValueChange: (e) => j(e),
|
|
393
|
+
children: [/* @__PURE__ */ N(V, {
|
|
394
|
+
"aria-label": "Review filter",
|
|
395
|
+
className: "min-w-48",
|
|
396
|
+
children: /* @__PURE__ */ N(H, {})
|
|
397
|
+
}), /* @__PURE__ */ N(z, { children: G.map((e) => /* @__PURE__ */ N(B, {
|
|
398
|
+
value: e,
|
|
399
|
+
children: e === "ALL" ? "All attempts" : t(e)
|
|
400
|
+
}, e)) })]
|
|
401
|
+
})]
|
|
402
|
+
}), M.error ? /* @__PURE__ */ N(i, {
|
|
403
|
+
title: "Could not load attempts",
|
|
404
|
+
error: M.error
|
|
405
|
+
}) : (M.data?.tecListAssessmentAttempts ?? []).length === 0 ? /* @__PURE__ */ N(r, {
|
|
406
|
+
title: "Nothing to review",
|
|
407
|
+
description: "Flagged and review-required attempts appear here."
|
|
408
|
+
}) : /* @__PURE__ */ N("div", {
|
|
409
|
+
className: "space-y-2",
|
|
410
|
+
children: (M.data?.tecListAssessmentAttempts ?? []).map((e) => /* @__PURE__ */ N(Y, {
|
|
411
|
+
attempt: e,
|
|
412
|
+
title: U.get(e.assessmentId) ?? "Assessment attempt"
|
|
413
|
+
}, e.id))
|
|
414
|
+
})]
|
|
415
|
+
})]
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
//#endregion
|
|
419
|
+
export { X as AssessmentsAdminTab };
|
|
420
|
+
|
|
421
|
+
//# sourceMappingURL=AssessmentsAdminTab.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AssessmentsAdminTab.js","names":[],"sources":["../../../../src/technospam/components/admin/AssessmentsAdminTab.tsx"],"sourcesContent":["import { useState, type ReactNode } from 'react';\nimport { Archive, Check, ListChecks, Pencil, Plus, Rocket, ShieldCheck, X } from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useCoursesQuery } from '../../api/technospamApi';\nimport {\n useArchiveAssessment,\n useAssessmentAttemptEventsQuery,\n useAssessmentAttemptsQuery,\n useAssessmentQuestionKeyQuery,\n useAssessmentsQuery,\n useCreateAssessment,\n usePublishAssessment,\n useReviewAssessmentAttempt,\n useUpdateAssessment,\n type TecAssessment,\n type TecAssessmentAttempt,\n type TecProctoringMode,\n type TecProctoringStatus,\n} from '../../api/assessmentApi';\nimport { LessonKindFields } from '../lesson/LessonKindFields';\nimport { draftFromLesson, draftToInput, type LessonKindDraft } from '../lesson/lessonAuthoring';\nimport { EmptyView, ErrorView, LoadingRows } from '../StateViews';\nimport { StatusBadge } from '../StatusBadge';\nimport { notify } from '../../lib/toast';\nimport { formatDateTime, humanizeEnum } from '../../utils/format';\nimport {\n assessmentSettingsToInput,\n draftFromAssessment,\n emptyAssessmentDraft,\n type AssessmentSettingsDraft,\n} from '../../utils/assessment';\n\nconst MODES: TecProctoringMode[] = ['INTEGRITY_MONITORING', 'REVIEW_REQUIRED', 'NONE'];\nconst REVIEW_FILTERS: (TecProctoringStatus | 'ALL')[] = ['FLAGGED', 'PENDING_REVIEW', 'ALL'];\n\nfunction Field({ label, children }: { label: string; children: ReactNode }) {\n return (\n <label className=\"flex flex-col gap-1.5\">\n <span className=\"text-xs font-medium text-text-secondary\">{label}</span>\n {children}\n </label>\n );\n}\n\nfunction Toggle({\n label,\n checked,\n onChange,\n}: {\n label: string;\n checked: boolean;\n onChange: (value: boolean) => void;\n}) {\n return (\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input type=\"checkbox\" checked={checked} onChange={(e) => onChange(e.target.checked)} />\n {label}\n </label>\n );\n}\n\n/**\n * Create / edit form for one assessment: settings plus the question set with\n * its answer key (reusing the quiz-lesson editor).\n */\nfunction AssessmentForm({\n courseId,\n assessment,\n onDone,\n}: {\n courseId: string;\n assessment: TecAssessment | null;\n onDone: () => void;\n}) {\n const [settings, setSettings] = useState<AssessmentSettingsDraft>(() =>\n assessment ? draftFromAssessment(assessment) : emptyAssessmentDraft()\n );\n const { data: keyData, loading: keyLoading } = useAssessmentQuestionKeyQuery(\n assessment?.id ?? null\n );\n const [questions, setQuestions] = useState<LessonKindDraft | null>(\n assessment ? null : draftFromLesson()\n );\n const [create, { loading: creating }] = useCreateAssessment();\n const [update, { loading: updating }] = useUpdateAssessment();\n const quiz =\n questions ?? (keyData ? draftFromLesson(null, keyData.tecAssessmentQuestionKey) : null);\n const set = <K extends keyof AssessmentSettingsDraft>(\n key: K,\n value: AssessmentSettingsDraft[K]\n ): void => setSettings((prev) => ({ ...prev, [key]: value }));\n\n const save = async (): Promise<void> => {\n const parsed = assessmentSettingsToInput(settings);\n if (!parsed.ok) return notify.error(parsed.error);\n let questionInput: Record<string, unknown> = {};\n if (quiz) {\n const q = draftToInput('QUIZ', quiz);\n if (!q.ok) return notify.error(q.error);\n questionInput = { questions: q.input.quizQuestions };\n }\n if (assessment) {\n const res = await update({\n variables: { id: assessment.id, input: { ...parsed.input, ...questionInput } },\n });\n if (res.data?.tecUpdateAssessment) {\n notify.success('Assessment saved');\n onDone();\n }\n return;\n }\n const res = await create({\n variables: { input: { courseId, ...parsed.input, ...questionInput } },\n });\n if (res.data?.tecCreateAssessment) {\n notify.success('Assessment created', 'Publish it when the questions are ready.');\n onDone();\n }\n };\n\n return (\n <GlassCard className=\"space-y-4 p-4 sm:p-5\" data-testid=\"assessment-form\">\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n <Field label=\"Title\">\n <Input value={settings.title} onChange={(e) => set('title', e.target.value)} />\n </Field>\n <Field label=\"Integrity mode\">\n <Select\n value={settings.proctoringMode}\n onValueChange={(v) => set('proctoringMode', v as TecProctoringMode)}\n >\n <SelectTrigger aria-label=\"Integrity mode\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {MODES.map((mode) => (\n <SelectItem key={mode} value={mode}>\n {humanizeEnum(mode)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </Field>\n <Field label=\"Time limit (minutes)\">\n <Input\n inputMode=\"numeric\"\n value={settings.timeLimitMinutes}\n onChange={(e) => set('timeLimitMinutes', e.target.value)}\n />\n </Field>\n <Field label=\"Pass mark (%)\">\n <Input\n inputMode=\"numeric\"\n value={settings.passingScore}\n onChange={(e) => set('passingScore', e.target.value)}\n />\n </Field>\n <Field label=\"Attempts per learner\">\n <Input\n inputMode=\"numeric\"\n value={settings.maxAttempts}\n onChange={(e) => set('maxAttempts', e.target.value)}\n />\n </Field>\n <Field label=\"Allowed integrity events\">\n <Input\n inputMode=\"numeric\"\n value={settings.maxIntegrityViolations}\n onChange={(e) => set('maxIntegrityViolations', e.target.value)}\n />\n </Field>\n <div className=\"sm:col-span-2\">\n <Field label=\"Description\">\n <Textarea\n value={settings.description}\n onChange={(e) => set('description', e.target.value)}\n />\n </Field>\n </div>\n <div className=\"flex flex-wrap gap-x-5 gap-y-2 sm:col-span-2\">\n <Toggle\n label=\"Require fullscreen\"\n checked={settings.requireFullscreen}\n onChange={(v) => set('requireFullscreen', v)}\n />\n <Toggle\n label=\"Block copy and paste\"\n checked={settings.blockClipboard}\n onChange={(v) => set('blockClipboard', v)}\n />\n <Toggle\n label=\"Shuffle questions\"\n checked={settings.shuffleQuestions}\n onChange={(v) => set('shuffleQuestions', v)}\n />\n <Toggle\n label=\"Shuffle options\"\n checked={settings.shuffleOptions}\n onChange={(v) => set('shuffleOptions', v)}\n />\n <Toggle\n label=\"Issue course certificate\"\n checked={settings.issuesCertificate}\n onChange={(v) => set('issuesCertificate', v)}\n />\n </div>\n </div>\n {quiz ? (\n <LessonKindFields\n kind=\"QUIZ\"\n draft={quiz}\n onChange={setQuestions}\n heading={`Questions (learners need ${settings.passingScore || '?'}% to pass)`}\n />\n ) : keyLoading ? (\n <LoadingRows count={2} />\n ) : null}\n <div className=\"flex justify-end gap-2 border-t border-border-seam pt-3\">\n <Button variant=\"outline\" onClick={onDone}>\n <X size={15} /> Cancel\n </Button>\n <Button\n onClick={() => void save()}\n disabled={creating || updating}\n data-testid=\"assessment-save\"\n >\n <Check size={15} /> {creating || updating ? 'Saving…' : 'Save assessment'}\n </Button>\n </div>\n </GlassCard>\n );\n}\n\nfunction AttemptReviewRow({ attempt, title }: { attempt: TecAssessmentAttempt; title: string }) {\n const [open, setOpen] = useState(false);\n const [notes, setNotes] = useState('');\n const { data: eventsData } = useAssessmentAttemptEventsQuery(open ? attempt.id : null);\n const [review, { loading }] = useReviewAssessmentAttempt();\n const reviewable =\n attempt.proctoringStatus === 'FLAGGED' || attempt.proctoringStatus === 'PENDING_REVIEW';\n\n const decide = async (decision: 'VERIFIED' | 'REJECTED'): Promise<void> => {\n const res = await review({\n variables: { id: attempt.id, decision, notes: notes.trim() || null },\n });\n const done = res.data?.tecReviewAssessmentAttempt;\n if (done) {\n notify.success(\n decision === 'VERIFIED' ? 'Attempt verified' : 'Attempt rejected',\n done.certificateId ? 'The certificate was issued.' : undefined\n );\n }\n };\n\n return (\n <GlassCard className=\"space-y-3 p-4\" data-testid=\"assessment-review-row\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm font-semibold text-text-primary\">{title}</div>\n <div className=\"text-xs text-text-muted\">\n Learner {attempt.learnerId} · attempt {attempt.attemptNumber} · {attempt.score ?? 0}% (\n {attempt.passed ? 'passed' : 'not passed'}) · {attempt.integrityViolations} integrity\n events · {formatDateTime(attempt.submittedAt ?? attempt.startedAt)}\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <StatusBadge status={attempt.proctoringStatus} />\n <Button size=\"sm\" variant=\"outline\" onClick={() => setOpen((v) => !v)}>\n {open ? 'Hide' : 'Review'}\n </Button>\n </div>\n </div>\n {open ? (\n <div className=\"space-y-3\">\n <ol\n className=\"max-h-56 space-y-1 overflow-auto rounded-lg bg-bg-sunken p-3 text-xs\"\n data-testid=\"assessment-events\"\n >\n {(eventsData?.tecAssessmentAttemptEvents ?? []).map((event) => (\n <li\n key={event.id}\n className={event.violation ? 'text-status-warning-text' : 'text-text-secondary'}\n >\n {formatDateTime(event.occurredAt)} · {humanizeEnum(event.type)}\n {event.violation ? ' (violation)' : ''}\n </li>\n ))}\n </ol>\n {reviewable ? (\n <>\n <Textarea\n aria-label=\"Review notes\"\n placeholder=\"Notes for the learner (optional)\"\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n />\n <div className=\"flex gap-2\">\n <Button size=\"sm\" disabled={loading} onClick={() => void decide('VERIFIED')}>\n <Check size={14} /> Verify\n </Button>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={loading}\n onClick={() => void decide('REJECTED')}\n >\n <X size={14} /> Reject\n </Button>\n </div>\n </>\n ) : attempt.reviewNotes ? (\n <p className=\"text-sm text-text-secondary\">Reviewer note: {attempt.reviewNotes}</p>\n ) : null}\n </div>\n ) : null}\n </GlassCard>\n );\n}\n\n/**\n * Authoring console tab for certification assessments (BOFF-4427): pick a\n * course, create / edit / publish / archive its assessments, and work the\n * review queue of flagged and review-required attempts.\n */\nexport function AssessmentsAdminTab() {\n const { data: courseData } = useCoursesQuery({});\n const courses = courseData?.tecListCourses?.items ?? [];\n const [courseId, setCourseId] = useState<string>('');\n const activeCourseId = courseId || courses[0]?.id || '';\n const { data, loading, error } = useAssessmentsQuery(\n { courseId: activeCourseId },\n !activeCourseId\n );\n const [editing, setEditing] = useState<TecAssessment | 'new' | null>(null);\n const [publish] = usePublishAssessment();\n const [archive] = useArchiveAssessment();\n const [filter, setFilter] = useState<TecProctoringStatus | 'ALL'>('FLAGGED');\n const attemptsQuery = useAssessmentAttemptsQuery({\n proctoringStatus: filter === 'ALL' ? null : filter,\n });\n const assessments = data?.tecListAssessments ?? [];\n const titles = new Map(assessments.map((a) => [a.id, a.title]));\n\n return (\n <div className=\"space-y-6\">\n <section className=\"space-y-3\">\n <div className=\"flex flex-wrap items-end justify-between gap-3\">\n <Field label=\"Course\">\n <Select\n value={activeCourseId}\n onValueChange={(v) => {\n setCourseId(v);\n setEditing(null);\n }}\n >\n <SelectTrigger aria-label=\"Course\" className=\"min-w-64\">\n <SelectValue placeholder=\"Choose a course\" />\n </SelectTrigger>\n <SelectContent>\n {courses.map((course) => (\n <SelectItem key={course.id} value={course.id}>\n {course.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </Field>\n {activeCourseId && editing === null ? (\n <Button size=\"sm\" onClick={() => setEditing('new')} data-testid=\"assessment-new\">\n <Plus size={15} /> New assessment\n </Button>\n ) : null}\n </div>\n\n {editing !== null && activeCourseId ? (\n <AssessmentForm\n key={editing === 'new' ? 'new' : editing.id}\n courseId={activeCourseId}\n assessment={editing === 'new' ? null : editing}\n onDone={() => setEditing(null)}\n />\n ) : null}\n\n {!activeCourseId ? (\n <EmptyView\n title=\"No courses yet\"\n description=\"Create a course first, then add its assessments.\"\n />\n ) : error ? (\n <ErrorView title=\"Could not load assessments\" error={error} />\n ) : loading && assessments.length === 0 ? (\n <LoadingRows count={2} />\n ) : assessments.length === 0 ? (\n <EmptyView\n icon={<ShieldCheck size={26} />}\n title=\"No assessments for this course\"\n description=\"Add a timed, integrity-monitored exam that issues the course certificate.\"\n />\n ) : (\n <div className=\"space-y-2\">\n {assessments.map((assessment) => (\n <GlassCard\n key={assessment.id}\n className=\"flex flex-wrap items-center justify-between gap-3 p-4\"\n >\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm font-semibold text-text-primary\">\n {assessment.title}\n </div>\n <div className=\"text-xs text-text-muted\">\n {assessment.questionCount} questions · {assessment.timeLimitMinutes} min · pass{' '}\n {assessment.passingScore}% · {humanizeEnum(assessment.proctoringMode)}\n </div>\n </div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={assessment.status} />\n <Button size=\"sm\" variant=\"outline\" onClick={() => setEditing(assessment)}>\n <Pencil size={14} /> Edit\n </Button>\n {assessment.status !== 'PUBLISHED' ? (\n <Button\n size=\"sm\"\n onClick={() =>\n void publish({ variables: { id: assessment.id } }).then((res) => {\n if (res.data?.tecPublishAssessment)\n notify.success('Assessment published');\n })\n }\n >\n <Rocket size={14} /> Publish\n </Button>\n ) : (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() =>\n void archive({ variables: { id: assessment.id } }).then((res) => {\n if (res.data?.tecArchiveAssessment) notify.success('Assessment archived');\n })\n }\n >\n <Archive size={14} /> Archive\n </Button>\n )}\n </div>\n </GlassCard>\n ))}\n </div>\n )}\n </section>\n\n <section className=\"space-y-3\">\n <div className=\"flex flex-wrap items-end justify-between gap-3\">\n <h3 className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <ListChecks size={16} /> Review queue\n </h3>\n <Select value={filter} onValueChange={(v) => setFilter(v as TecProctoringStatus | 'ALL')}>\n <SelectTrigger aria-label=\"Review filter\" className=\"min-w-48\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {REVIEW_FILTERS.map((f) => (\n <SelectItem key={f} value={f}>\n {f === 'ALL' ? 'All attempts' : humanizeEnum(f)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n {attemptsQuery.error ? (\n <ErrorView title=\"Could not load attempts\" error={attemptsQuery.error} />\n ) : (attemptsQuery.data?.tecListAssessmentAttempts ?? []).length === 0 ? (\n <EmptyView\n title=\"Nothing to review\"\n description=\"Flagged and review-required attempts appear here.\"\n />\n ) : (\n <div className=\"space-y-2\">\n {(attemptsQuery.data?.tecListAssessmentAttempts ?? []).map((attempt) => (\n <AttemptReviewRow\n key={attempt.id}\n attempt={attempt}\n title={titles.get(attempt.assessmentId) ?? 'Assessment attempt'}\n />\n ))}\n </div>\n )}\n </section>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -7,9 +7,11 @@ import { LessonKindDraft } from './lessonAuthoring';
|
|
|
7
7
|
* expected output) and the multiple-choice questions of a QUIZ lesson with
|
|
8
8
|
* their answer key. Renders nothing for other kinds.
|
|
9
9
|
*/
|
|
10
|
-
export declare function LessonKindFields({ kind, draft, onChange, }: {
|
|
10
|
+
export declare function LessonKindFields({ kind, draft, onChange, heading, }: {
|
|
11
11
|
kind: TecLessonKind;
|
|
12
12
|
draft: LessonKindDraft;
|
|
13
13
|
onChange: (next: LessonKindDraft) => void;
|
|
14
|
+
/** Overrides the QUIZ section heading (assessments carry their own pass mark; BOFF-4427). */
|
|
15
|
+
heading?: string;
|
|
14
16
|
}): import("react").JSX.Element | null;
|
|
15
17
|
//# sourceMappingURL=LessonKindFields.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LessonKindFields.d.ts","sourceRoot":"","sources":["../../../../src/technospam/components/lesson/LessonKindFields.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,mBAAmB,CAAC;AAE3B;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,KAAK,EACL,QAAQ,
|
|
1
|
+
{"version":3,"file":"LessonKindFields.d.ts","sourceRoot":"","sources":["../../../../src/technospam/components/lesson/LessonKindFields.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,mBAAmB,CAAC;AAE3B;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,OAAO,GACR,EAAE;IACD,IAAI,EAAE,aAAa,CAAC;IACpB,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;IAC1C,6FAA6F;IAC7F,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,sCA6HA"}
|
|
@@ -4,7 +4,7 @@ import { Plus as i, Trash2 as a } from "lucide-react";
|
|
|
4
4
|
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
5
5
|
import { Button as c, Input as l, Select as u, SelectContent as d, SelectItem as f, SelectTrigger as p, SelectValue as m, Textarea as h } from "@burdenoff/fe-libs/ui";
|
|
6
6
|
//#region src/technospam/components/lesson/LessonKindFields.tsx
|
|
7
|
-
function g({ kind: g, draft: _, onChange: v }) {
|
|
7
|
+
function g({ kind: g, draft: _, onChange: v, heading: y }) {
|
|
8
8
|
if (g === "INTERACTIVE") return /* @__PURE__ */ s("div", {
|
|
9
9
|
className: "mt-2 space-y-2 rounded-lg border border-border-seam p-3",
|
|
10
10
|
children: [
|
|
@@ -76,7 +76,7 @@ function g({ kind: g, draft: _, onChange: v }) {
|
|
|
76
76
|
children: [
|
|
77
77
|
/* @__PURE__ */ o("p", {
|
|
78
78
|
className: "text-xs font-semibold uppercase tracking-wide text-text-muted",
|
|
79
|
-
children: "Quiz questions (learners need 70% to pass)"
|
|
79
|
+
children: y ?? "Quiz questions (learners need 70% to pass)"
|
|
80
80
|
}),
|
|
81
81
|
_.quiz.map((t, n) => {
|
|
82
82
|
let i = r(t.options);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LessonKindFields.js","names":[],"sources":["../../../../src/technospam/components/lesson/LessonKindFields.tsx"],"sourcesContent":["import { Plus, Trash2 } from 'lucide-react';\nimport {\n Button,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport type { TecLessonKind } from '../../api/technospamApi';\nimport { humanizeEnum } from '../../utils/format';\nimport {\n LESSON_LANGUAGES,\n emptyQuestion,\n splitOptions,\n type LessonKindDraft,\n type QuizQuestionDraft,\n} from './lessonAuthoring';\n\n/**\n * Kind-specific authoring fields for a lesson (BOFF-4107): the runnable\n * workspace of an INTERACTIVE lesson (language, starter code, sample input,\n * expected output) and the multiple-choice questions of a QUIZ lesson with\n * their answer key. Renders nothing for other kinds.\n */\nexport function LessonKindFields({\n kind,\n draft,\n onChange,\n}: {\n kind: TecLessonKind;\n draft: LessonKindDraft;\n onChange: (next: LessonKindDraft) => void;\n}) {\n if (kind === 'INTERACTIVE') {\n return (\n <div className=\"mt-2 space-y-2 rounded-lg border border-border-seam p-3\">\n <p className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Runnable workspace\n </p>\n <Select value={draft.language} onValueChange={(v) => onChange({ ...draft, language: v })}>\n <SelectTrigger aria-label=\"Workspace language\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {LESSON_LANGUAGES.map((l) => (\n <SelectItem key={l} value={l}>\n {humanizeEnum(l)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Textarea\n aria-label=\"Starter code\"\n placeholder=\"Starter code shown in the editor\"\n className=\"font-mono text-xs\"\n value={draft.starterCode}\n onChange={(e) => onChange({ ...draft, starterCode: e.target.value })}\n spellCheck={false}\n />\n <div className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n <Textarea\n aria-label=\"Sample input\"\n placeholder=\"Sample input (stdin, optional)\"\n className=\"font-mono text-xs\"\n value={draft.sampleInput}\n onChange={(e) => onChange({ ...draft, sampleInput: e.target.value })}\n spellCheck={false}\n />\n <Textarea\n aria-label=\"Expected output\"\n placeholder=\"Expected output (optional; a matching run completes the lesson)\"\n className=\"font-mono text-xs\"\n value={draft.expectedOutput}\n onChange={(e) => onChange({ ...draft, expectedOutput: e.target.value })}\n spellCheck={false}\n />\n </div>\n </div>\n );\n }\n\n if (kind === 'QUIZ') {\n const update = (index: number, patch: Partial<QuizQuestionDraft>) =>\n onChange({\n ...draft,\n quiz: draft.quiz.map((q, i) => (i === index ? { ...q, ...patch } : q)),\n });\n return (\n <div className=\"mt-2 space-y-3 rounded-lg border border-border-seam p-3\">\n <p className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Quiz questions (learners need 70% to pass)\n </p>\n {draft.quiz.map((q, i) => {\n const options = splitOptions(q.options);\n return (\n <div key={i} className=\"space-y-2 rounded-lg bg-bg-sunken p-3\">\n <div className=\"flex items-center gap-2\">\n <Input\n aria-label={`Question ${i + 1} prompt`}\n placeholder={`Question ${i + 1}`}\n value={q.prompt}\n onChange={(e) => update(i, { prompt: e.target.value })}\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove question ${i + 1}`}\n onClick={() => onChange({ ...draft, quiz: draft.quiz.filter((_, j) => j !== i) })}\n >\n <Trash2 size={14} />\n </Button>\n </div>\n <Textarea\n aria-label={`Question ${i + 1} options`}\n placeholder=\"Answer options, one per line\"\n value={q.options}\n onChange={(e) => update(i, { options: e.target.value })}\n />\n <div className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n <Select\n value={String(q.correctOption)}\n onValueChange={(v) => update(i, { correctOption: Number(v) })}\n disabled={options.length === 0}\n >\n <SelectTrigger aria-label={`Question ${i + 1} correct option`}>\n <SelectValue placeholder=\"Correct option\" />\n </SelectTrigger>\n <SelectContent>\n {options.map((o, oi) => (\n <SelectItem key={oi} value={String(oi)}>\n Correct: {o}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Input\n aria-label={`Question ${i + 1} explanation`}\n placeholder=\"Explanation shown after grading (optional)\"\n value={q.explanation}\n onChange={(e) => update(i, { explanation: e.target.value })}\n />\n </div>\n </div>\n );\n })}\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => onChange({ ...draft, quiz: [...draft.quiz, emptyQuestion()] })}\n >\n <Plus size={14} /> Add question\n </Button>\n </div>\n );\n }\n\n return null;\n}\n"],"mappings":";;;;;;AA2BA,SAAgB,EAAiB,EAC/B,SACA,UACA,
|
|
1
|
+
{"version":3,"file":"LessonKindFields.js","names":[],"sources":["../../../../src/technospam/components/lesson/LessonKindFields.tsx"],"sourcesContent":["import { Plus, Trash2 } from 'lucide-react';\nimport {\n Button,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport type { TecLessonKind } from '../../api/technospamApi';\nimport { humanizeEnum } from '../../utils/format';\nimport {\n LESSON_LANGUAGES,\n emptyQuestion,\n splitOptions,\n type LessonKindDraft,\n type QuizQuestionDraft,\n} from './lessonAuthoring';\n\n/**\n * Kind-specific authoring fields for a lesson (BOFF-4107): the runnable\n * workspace of an INTERACTIVE lesson (language, starter code, sample input,\n * expected output) and the multiple-choice questions of a QUIZ lesson with\n * their answer key. Renders nothing for other kinds.\n */\nexport function LessonKindFields({\n kind,\n draft,\n onChange,\n heading,\n}: {\n kind: TecLessonKind;\n draft: LessonKindDraft;\n onChange: (next: LessonKindDraft) => void;\n /** Overrides the QUIZ section heading (assessments carry their own pass mark; BOFF-4427). */\n heading?: string;\n}) {\n if (kind === 'INTERACTIVE') {\n return (\n <div className=\"mt-2 space-y-2 rounded-lg border border-border-seam p-3\">\n <p className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Runnable workspace\n </p>\n <Select value={draft.language} onValueChange={(v) => onChange({ ...draft, language: v })}>\n <SelectTrigger aria-label=\"Workspace language\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {LESSON_LANGUAGES.map((l) => (\n <SelectItem key={l} value={l}>\n {humanizeEnum(l)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Textarea\n aria-label=\"Starter code\"\n placeholder=\"Starter code shown in the editor\"\n className=\"font-mono text-xs\"\n value={draft.starterCode}\n onChange={(e) => onChange({ ...draft, starterCode: e.target.value })}\n spellCheck={false}\n />\n <div className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n <Textarea\n aria-label=\"Sample input\"\n placeholder=\"Sample input (stdin, optional)\"\n className=\"font-mono text-xs\"\n value={draft.sampleInput}\n onChange={(e) => onChange({ ...draft, sampleInput: e.target.value })}\n spellCheck={false}\n />\n <Textarea\n aria-label=\"Expected output\"\n placeholder=\"Expected output (optional; a matching run completes the lesson)\"\n className=\"font-mono text-xs\"\n value={draft.expectedOutput}\n onChange={(e) => onChange({ ...draft, expectedOutput: e.target.value })}\n spellCheck={false}\n />\n </div>\n </div>\n );\n }\n\n if (kind === 'QUIZ') {\n const update = (index: number, patch: Partial<QuizQuestionDraft>) =>\n onChange({\n ...draft,\n quiz: draft.quiz.map((q, i) => (i === index ? { ...q, ...patch } : q)),\n });\n return (\n <div className=\"mt-2 space-y-3 rounded-lg border border-border-seam p-3\">\n <p className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n {heading ?? 'Quiz questions (learners need 70% to pass)'}\n </p>\n {draft.quiz.map((q, i) => {\n const options = splitOptions(q.options);\n return (\n <div key={i} className=\"space-y-2 rounded-lg bg-bg-sunken p-3\">\n <div className=\"flex items-center gap-2\">\n <Input\n aria-label={`Question ${i + 1} prompt`}\n placeholder={`Question ${i + 1}`}\n value={q.prompt}\n onChange={(e) => update(i, { prompt: e.target.value })}\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n aria-label={`Remove question ${i + 1}`}\n onClick={() => onChange({ ...draft, quiz: draft.quiz.filter((_, j) => j !== i) })}\n >\n <Trash2 size={14} />\n </Button>\n </div>\n <Textarea\n aria-label={`Question ${i + 1} options`}\n placeholder=\"Answer options, one per line\"\n value={q.options}\n onChange={(e) => update(i, { options: e.target.value })}\n />\n <div className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n <Select\n value={String(q.correctOption)}\n onValueChange={(v) => update(i, { correctOption: Number(v) })}\n disabled={options.length === 0}\n >\n <SelectTrigger aria-label={`Question ${i + 1} correct option`}>\n <SelectValue placeholder=\"Correct option\" />\n </SelectTrigger>\n <SelectContent>\n {options.map((o, oi) => (\n <SelectItem key={oi} value={String(oi)}>\n Correct: {o}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Input\n aria-label={`Question ${i + 1} explanation`}\n placeholder=\"Explanation shown after grading (optional)\"\n value={q.explanation}\n onChange={(e) => update(i, { explanation: e.target.value })}\n />\n </div>\n </div>\n );\n })}\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => onChange({ ...draft, quiz: [...draft.quiz, emptyQuestion()] })}\n >\n <Plus size={14} /> Add question\n </Button>\n </div>\n );\n }\n\n return null;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { TecAssessmentSignal } from '../api/assessmentApi';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Browser-side integrity hooks for a running assessment attempt (BOFF-4427).
|
|
5
|
+
*
|
|
6
|
+
* While `active`, it:
|
|
7
|
+
* - reports leaving the tab (`visibilitychange`) and the window (`blur`);
|
|
8
|
+
* a blur that the tab switch already explains is not reported twice;
|
|
9
|
+
* - tracks fullscreen and reports leaving it when the attempt requires it;
|
|
10
|
+
* - blocks copy / cut / paste and the context menu when the attempt says so,
|
|
11
|
+
* and reports each try.
|
|
12
|
+
*
|
|
13
|
+
* These are deterrents and evidence, not a lockdown: the server decides what
|
|
14
|
+
* counts as a violation and what happens to the attempt.
|
|
15
|
+
*/
|
|
16
|
+
export interface IntegrityOptions {
|
|
17
|
+
active: boolean;
|
|
18
|
+
requireFullscreen: boolean;
|
|
19
|
+
blockClipboard: boolean;
|
|
20
|
+
onSignal: (signal: TecAssessmentSignal, detail?: Record<string, string | number | boolean>) => void;
|
|
21
|
+
}
|
|
22
|
+
export interface IntegrityState {
|
|
23
|
+
/** Fullscreen is supported by this browser (and not blocked by the embedding page). */
|
|
24
|
+
fullscreenSupported: boolean;
|
|
25
|
+
isFullscreen: boolean;
|
|
26
|
+
enterFullscreen: () => Promise<boolean>;
|
|
27
|
+
}
|
|
28
|
+
export declare function useAssessmentIntegrity({ active, requireFullscreen, blockClipboard, onSignal, }: IntegrityOptions): IntegrityState;
|
|
29
|
+
//# sourceMappingURL=useAssessmentIntegrity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAssessmentIntegrity.d.ts","sourceRoot":"","sources":["../../../src/technospam/hooks/useAssessmentIntegrity.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,CAAC;IAChB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,CACR,MAAM,EAAE,mBAAmB,EAC3B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,KAC/C,IAAI,CAAC;CACX;AAED,MAAM,WAAW,cAAc;IAC7B,uFAAuF;IACvF,mBAAmB,EAAE,OAAO,CAAC;IAC7B,YAAY,EAAE,OAAO,CAAC;IACtB,eAAe,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACzC;AASD,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,iBAAiB,EACjB,cAAc,EACd,QAAQ,GACT,EAAE,gBAAgB,GAAG,cAAc,CA8GnC"}
|