@burdenoff/microfe-technospam 2026.1030.4 → 2026.1030.6
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/wspace-types.d.ts +1501 -11
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/Brush.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/ErrorBar.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/ReferenceArea.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/ReferenceDot.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/ReferenceLine.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/XAxis.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/YAxis.js.map +1 -1
- package/dist/node_modules/recharts/es6/component/ResponsiveContainer.js.map +1 -1
- package/dist/node_modules/sonner/dist/index.js.map +1 -1
- package/dist/stories/fixtures/technospamFixtures.d.ts +1 -0
- package/dist/stories/fixtures/technospamFixtures.d.ts.map +1 -1
- package/dist/stories/journeys/Courses.stories.d.ts +5 -2
- package/dist/stories/journeys/Courses.stories.d.ts.map +1 -1
- package/dist/stories/journeys/Dashboard.stories.d.ts +2 -2
- package/dist/stories/journeys/Dashboard.stories.d.ts.map +1 -1
- package/dist/technospam/TechnoSpamRoutes.d.ts.map +1 -1
- package/dist/technospam/TechnoSpamRoutes.js +31 -27
- package/dist/technospam/TechnoSpamRoutes.js.map +1 -1
- package/dist/technospam/api/technospamApi.d.ts +101 -5
- package/dist/technospam/api/technospamApi.d.ts.map +1 -1
- package/dist/technospam/api/technospamApi.js +278 -146
- package/dist/technospam/api/technospamApi.js.map +1 -1
- package/dist/technospam/components/lesson/InteractiveWorkspace.d.ts +15 -0
- package/dist/technospam/components/lesson/InteractiveWorkspace.d.ts.map +1 -0
- package/dist/technospam/components/lesson/InteractiveWorkspace.js +198 -0
- package/dist/technospam/components/lesson/InteractiveWorkspace.js.map +1 -0
- package/dist/technospam/components/lesson/LessonKindFields.d.ts +15 -0
- package/dist/technospam/components/lesson/LessonKindFields.d.ts.map +1 -0
- package/dist/technospam/components/lesson/LessonKindFields.js +150 -0
- package/dist/technospam/components/lesson/LessonKindFields.js.map +1 -0
- package/dist/technospam/components/lesson/QuizLesson.d.ts +14 -0
- package/dist/technospam/components/lesson/QuizLesson.d.ts.map +1 -0
- package/dist/technospam/components/lesson/QuizLesson.js +105 -0
- package/dist/technospam/components/lesson/QuizLesson.js.map +1 -0
- package/dist/technospam/components/lesson/lessonAuthoring.d.ts +47 -0
- package/dist/technospam/components/lesson/lessonAuthoring.d.ts.map +1 -0
- package/dist/technospam/components/lesson/lessonAuthoring.js +80 -0
- package/dist/technospam/components/lesson/lessonAuthoring.js.map +1 -0
- package/dist/technospam/hooks/useTechnoSpamAnalytics.d.ts +1 -1
- package/dist/technospam/hooks/useTechnoSpamAnalytics.d.ts.map +1 -1
- package/dist/technospam/hooks/useTechnoSpamAnalytics.js.map +1 -1
- package/dist/technospam/pages/AdminAdminDashboardPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/AdminAdminDashboardPagePage.js +230 -207
- package/dist/technospam/pages/AdminAdminDashboardPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +130 -132
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.d.ts +9 -0
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.d.ts.map +1 -0
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.js +291 -0
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.js.map +1 -0
- package/dist/technospam/pages/CoursesMyLearningPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesMyLearningPagePage.js +80 -78
- package/dist/technospam/pages/CoursesMyLearningPagePage.js.map +1 -1
- package/dist/technospam/pages/DashboardDashboardPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/DashboardDashboardPagePage.js +136 -133
- package/dist/technospam/pages/DashboardDashboardPagePage.js.map +1 -1
- package/dist/technospam/utils/lessonPath.d.ts +15 -0
- package/dist/technospam/utils/lessonPath.d.ts.map +1 -0
- package/dist/technospam/utils/lessonPath.js +15 -0
- package/dist/technospam/utils/lessonPath.js.map +1 -0
- package/dist/technospam/utils/pageContext.d.ts.map +1 -1
- package/dist/technospam/utils/pageContext.js +10 -0
- package/dist/technospam/utils/pageContext.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { humanizeEnum as e } from "../../utils/format.js";
|
|
2
|
+
import { LESSON_LANGUAGES as t, emptyQuestion as n, splitOptions as r } from "./lessonAuthoring.js";
|
|
3
|
+
import { Plus as i, Trash2 as a } from "lucide-react";
|
|
4
|
+
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
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
|
+
//#region src/technospam/components/lesson/LessonKindFields.tsx
|
|
7
|
+
function g({ kind: g, draft: _, onChange: v }) {
|
|
8
|
+
if (g === "INTERACTIVE") return /* @__PURE__ */ s("div", {
|
|
9
|
+
className: "mt-2 space-y-2 rounded-lg border border-border-seam p-3",
|
|
10
|
+
children: [
|
|
11
|
+
/* @__PURE__ */ o("p", {
|
|
12
|
+
className: "text-xs font-semibold uppercase tracking-wide text-text-muted",
|
|
13
|
+
children: "Runnable workspace"
|
|
14
|
+
}),
|
|
15
|
+
/* @__PURE__ */ s(u, {
|
|
16
|
+
value: _.language,
|
|
17
|
+
onValueChange: (e) => v({
|
|
18
|
+
..._,
|
|
19
|
+
language: e
|
|
20
|
+
}),
|
|
21
|
+
children: [/* @__PURE__ */ o(p, {
|
|
22
|
+
"aria-label": "Workspace language",
|
|
23
|
+
children: /* @__PURE__ */ o(m, {})
|
|
24
|
+
}), /* @__PURE__ */ o(d, { children: t.map((t) => /* @__PURE__ */ o(f, {
|
|
25
|
+
value: t,
|
|
26
|
+
children: e(t)
|
|
27
|
+
}, t)) })]
|
|
28
|
+
}),
|
|
29
|
+
/* @__PURE__ */ o(h, {
|
|
30
|
+
"aria-label": "Starter code",
|
|
31
|
+
placeholder: "Starter code shown in the editor",
|
|
32
|
+
className: "font-mono text-xs",
|
|
33
|
+
value: _.starterCode,
|
|
34
|
+
onChange: (e) => v({
|
|
35
|
+
..._,
|
|
36
|
+
starterCode: e.target.value
|
|
37
|
+
}),
|
|
38
|
+
spellCheck: !1
|
|
39
|
+
}),
|
|
40
|
+
/* @__PURE__ */ s("div", {
|
|
41
|
+
className: "grid grid-cols-1 gap-2 sm:grid-cols-2",
|
|
42
|
+
children: [/* @__PURE__ */ o(h, {
|
|
43
|
+
"aria-label": "Sample input",
|
|
44
|
+
placeholder: "Sample input (stdin, optional)",
|
|
45
|
+
className: "font-mono text-xs",
|
|
46
|
+
value: _.sampleInput,
|
|
47
|
+
onChange: (e) => v({
|
|
48
|
+
..._,
|
|
49
|
+
sampleInput: e.target.value
|
|
50
|
+
}),
|
|
51
|
+
spellCheck: !1
|
|
52
|
+
}), /* @__PURE__ */ o(h, {
|
|
53
|
+
"aria-label": "Expected output",
|
|
54
|
+
placeholder: "Expected output (optional; a matching run completes the lesson)",
|
|
55
|
+
className: "font-mono text-xs",
|
|
56
|
+
value: _.expectedOutput,
|
|
57
|
+
onChange: (e) => v({
|
|
58
|
+
..._,
|
|
59
|
+
expectedOutput: e.target.value
|
|
60
|
+
}),
|
|
61
|
+
spellCheck: !1
|
|
62
|
+
})]
|
|
63
|
+
})
|
|
64
|
+
]
|
|
65
|
+
});
|
|
66
|
+
if (g === "QUIZ") {
|
|
67
|
+
let e = (e, t) => v({
|
|
68
|
+
..._,
|
|
69
|
+
quiz: _.quiz.map((n, r) => r === e ? {
|
|
70
|
+
...n,
|
|
71
|
+
...t
|
|
72
|
+
} : n)
|
|
73
|
+
});
|
|
74
|
+
return /* @__PURE__ */ s("div", {
|
|
75
|
+
className: "mt-2 space-y-3 rounded-lg border border-border-seam p-3",
|
|
76
|
+
children: [
|
|
77
|
+
/* @__PURE__ */ o("p", {
|
|
78
|
+
className: "text-xs font-semibold uppercase tracking-wide text-text-muted",
|
|
79
|
+
children: "Quiz questions (learners need 70% to pass)"
|
|
80
|
+
}),
|
|
81
|
+
_.quiz.map((t, n) => {
|
|
82
|
+
let i = r(t.options);
|
|
83
|
+
return /* @__PURE__ */ s("div", {
|
|
84
|
+
className: "space-y-2 rounded-lg bg-bg-sunken p-3",
|
|
85
|
+
children: [
|
|
86
|
+
/* @__PURE__ */ s("div", {
|
|
87
|
+
className: "flex items-center gap-2",
|
|
88
|
+
children: [/* @__PURE__ */ o(l, {
|
|
89
|
+
"aria-label": `Question ${n + 1} prompt`,
|
|
90
|
+
placeholder: `Question ${n + 1}`,
|
|
91
|
+
value: t.prompt,
|
|
92
|
+
onChange: (t) => e(n, { prompt: t.target.value })
|
|
93
|
+
}), /* @__PURE__ */ o(c, {
|
|
94
|
+
size: "sm",
|
|
95
|
+
variant: "ghost",
|
|
96
|
+
"aria-label": `Remove question ${n + 1}`,
|
|
97
|
+
onClick: () => v({
|
|
98
|
+
..._,
|
|
99
|
+
quiz: _.quiz.filter((e, t) => t !== n)
|
|
100
|
+
}),
|
|
101
|
+
children: /* @__PURE__ */ o(a, { size: 14 })
|
|
102
|
+
})]
|
|
103
|
+
}),
|
|
104
|
+
/* @__PURE__ */ o(h, {
|
|
105
|
+
"aria-label": `Question ${n + 1} options`,
|
|
106
|
+
placeholder: "Answer options, one per line",
|
|
107
|
+
value: t.options,
|
|
108
|
+
onChange: (t) => e(n, { options: t.target.value })
|
|
109
|
+
}),
|
|
110
|
+
/* @__PURE__ */ s("div", {
|
|
111
|
+
className: "grid grid-cols-1 gap-2 sm:grid-cols-2",
|
|
112
|
+
children: [/* @__PURE__ */ s(u, {
|
|
113
|
+
value: String(t.correctOption),
|
|
114
|
+
onValueChange: (t) => e(n, { correctOption: Number(t) }),
|
|
115
|
+
disabled: i.length === 0,
|
|
116
|
+
children: [/* @__PURE__ */ o(p, {
|
|
117
|
+
"aria-label": `Question ${n + 1} correct option`,
|
|
118
|
+
children: /* @__PURE__ */ o(m, { placeholder: "Correct option" })
|
|
119
|
+
}), /* @__PURE__ */ o(d, { children: i.map((e, t) => /* @__PURE__ */ s(f, {
|
|
120
|
+
value: String(t),
|
|
121
|
+
children: ["Correct: ", e]
|
|
122
|
+
}, t)) })]
|
|
123
|
+
}), /* @__PURE__ */ o(l, {
|
|
124
|
+
"aria-label": `Question ${n + 1} explanation`,
|
|
125
|
+
placeholder: "Explanation shown after grading (optional)",
|
|
126
|
+
value: t.explanation,
|
|
127
|
+
onChange: (t) => e(n, { explanation: t.target.value })
|
|
128
|
+
})]
|
|
129
|
+
})
|
|
130
|
+
]
|
|
131
|
+
}, n);
|
|
132
|
+
}),
|
|
133
|
+
/* @__PURE__ */ s(c, {
|
|
134
|
+
size: "sm",
|
|
135
|
+
variant: "outline",
|
|
136
|
+
onClick: () => v({
|
|
137
|
+
..._,
|
|
138
|
+
quiz: [..._.quiz, n()]
|
|
139
|
+
}),
|
|
140
|
+
children: [/* @__PURE__ */ o(i, { size: 14 }), " Add question"]
|
|
141
|
+
})
|
|
142
|
+
]
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
export { g as LessonKindFields };
|
|
149
|
+
|
|
150
|
+
//# sourceMappingURL=LessonKindFields.js.map
|
|
@@ -0,0 +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":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { TecLesson } from '../../api/technospamApi';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multiple-choice surface for a QUIZ lesson (BOFF-4107). Mount it with
|
|
5
|
+
* `key={lesson.id}` so another lesson starts with a blank attempt. The lesson only
|
|
6
|
+
* carries questions and options; the answer key never reaches the browser.
|
|
7
|
+
* `tecSubmitLessonQuiz` grades the attempt server-side, returns per-question
|
|
8
|
+
* feedback, and completes the lesson for an enrolled learner who passes.
|
|
9
|
+
*/
|
|
10
|
+
export declare function QuizLesson({ lesson, onPassed }: {
|
|
11
|
+
lesson: TecLesson;
|
|
12
|
+
onPassed: () => void;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
14
|
+
//# sourceMappingURL=QuizLesson.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QuizLesson.d.ts","sourceRoot":"","sources":["../../../../src/technospam/components/lesson/QuizLesson.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAuB,KAAK,SAAS,EAAsB,MAAM,yBAAyB,CAAC;AAElG;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;CAAE,+BA8H3F"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { cn as e } from "../../utils/cn.js";
|
|
2
|
+
import { useSubmitLessonQuiz as t } from "../../api/technospamApi.js";
|
|
3
|
+
import { useState as n } from "react";
|
|
4
|
+
import { CheckCircle2 as r, RotateCcw as i, Send as a, XCircle as o } from "lucide-react";
|
|
5
|
+
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
6
|
+
import { Button as l } from "@burdenoff/fe-libs/ui";
|
|
7
|
+
//#region src/technospam/components/lesson/QuizLesson.tsx
|
|
8
|
+
function u({ lesson: u, onPassed: d }) {
|
|
9
|
+
let f = u.quizQuestions ?? [], [p, m] = n(() => f.map(() => null)), [h, g] = n(null), [_, { loading: v }] = t();
|
|
10
|
+
if (f.length === 0) return /* @__PURE__ */ s("p", {
|
|
11
|
+
className: "rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted",
|
|
12
|
+
children: "This quiz has no questions yet."
|
|
13
|
+
});
|
|
14
|
+
let y = p.every((e) => e !== null), b = async () => {
|
|
15
|
+
let e = (await _({ variables: {
|
|
16
|
+
lessonId: u.id,
|
|
17
|
+
answers: p.map((e) => e ?? -1)
|
|
18
|
+
} })).data?.tecSubmitLessonQuiz;
|
|
19
|
+
e && (g(e), e.passed && d());
|
|
20
|
+
};
|
|
21
|
+
return /* @__PURE__ */ c("div", {
|
|
22
|
+
className: "space-y-5",
|
|
23
|
+
children: [
|
|
24
|
+
f.map((t, n) => {
|
|
25
|
+
let i = h?.results[n];
|
|
26
|
+
return /* @__PURE__ */ c("fieldset", {
|
|
27
|
+
className: "rounded-xl border border-border-seam p-4",
|
|
28
|
+
disabled: v,
|
|
29
|
+
children: [
|
|
30
|
+
/* @__PURE__ */ c("legend", {
|
|
31
|
+
className: "px-1 text-sm font-semibold text-text-primary",
|
|
32
|
+
children: [
|
|
33
|
+
n + 1,
|
|
34
|
+
". ",
|
|
35
|
+
t.prompt
|
|
36
|
+
]
|
|
37
|
+
}),
|
|
38
|
+
/* @__PURE__ */ s("div", {
|
|
39
|
+
className: "mt-2 space-y-2",
|
|
40
|
+
children: t.options.map((t, r) => {
|
|
41
|
+
let a = p[n] === r, o = i ? i.correctOption === r : !1;
|
|
42
|
+
return /* @__PURE__ */ c("label", {
|
|
43
|
+
className: e("flex cursor-pointer items-center gap-3 rounded-lg border px-3 py-2 text-sm transition-colors", a ? "border-accent-primary bg-accent-primary/10" : "border-border-seam", i && o && "border-status-success-border bg-status-success-bg-subtle", i && a && !o && "border-status-error-border bg-status-error-bg-subtle"),
|
|
44
|
+
children: [/* @__PURE__ */ s("input", {
|
|
45
|
+
type: "radio",
|
|
46
|
+
name: `tec-quiz-${u.id}-${n}`,
|
|
47
|
+
checked: a,
|
|
48
|
+
onChange: () => {
|
|
49
|
+
m((e) => e.map((e, t) => t === n ? r : e)), g(null);
|
|
50
|
+
}
|
|
51
|
+
}), /* @__PURE__ */ s("span", {
|
|
52
|
+
className: "text-text-primary",
|
|
53
|
+
children: t
|
|
54
|
+
})]
|
|
55
|
+
}, r);
|
|
56
|
+
})
|
|
57
|
+
}),
|
|
58
|
+
i ? /* @__PURE__ */ c("p", {
|
|
59
|
+
className: e("mt-3 flex items-start gap-1.5 text-sm", i.correct ? "text-status-success-text" : "text-status-error-text"),
|
|
60
|
+
children: [i.correct ? /* @__PURE__ */ s(r, { size: 15 }) : /* @__PURE__ */ s(o, { size: 15 }), /* @__PURE__ */ c("span", { children: [i.correct ? "Correct." : "Not quite.", i.explanation ? ` ${i.explanation}` : ""] })]
|
|
61
|
+
}) : null
|
|
62
|
+
]
|
|
63
|
+
}, `${u.id}-${n}`);
|
|
64
|
+
}),
|
|
65
|
+
h ? /* @__PURE__ */ c("div", {
|
|
66
|
+
role: "status",
|
|
67
|
+
"data-testid": "lesson-quiz-result",
|
|
68
|
+
className: e("rounded-xl border px-4 py-3 text-sm", h.passed ? "border-status-success-border bg-status-success-bg-subtle text-status-success-text" : "border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text"),
|
|
69
|
+
children: [
|
|
70
|
+
"You scored ",
|
|
71
|
+
h.score,
|
|
72
|
+
"% (",
|
|
73
|
+
h.correctCount,
|
|
74
|
+
" of ",
|
|
75
|
+
h.totalCount,
|
|
76
|
+
" correct).",
|
|
77
|
+
" ",
|
|
78
|
+
h.passed ? "Quiz passed." : `You need ${h.passingScore}% to pass. Review the feedback and try again.`
|
|
79
|
+
]
|
|
80
|
+
}) : null,
|
|
81
|
+
/* @__PURE__ */ c("div", {
|
|
82
|
+
className: "flex flex-wrap gap-2",
|
|
83
|
+
children: [/* @__PURE__ */ c(l, {
|
|
84
|
+
onClick: () => void b(),
|
|
85
|
+
disabled: v || !y,
|
|
86
|
+
children: [
|
|
87
|
+
/* @__PURE__ */ s(a, { size: 15 }),
|
|
88
|
+
" ",
|
|
89
|
+
v ? "Checking…" : "Submit answers"
|
|
90
|
+
]
|
|
91
|
+
}), h && !h.passed ? /* @__PURE__ */ c(l, {
|
|
92
|
+
variant: "outline",
|
|
93
|
+
onClick: () => {
|
|
94
|
+
m(f.map(() => null)), g(null);
|
|
95
|
+
},
|
|
96
|
+
children: [/* @__PURE__ */ s(i, { size: 15 }), " Try again"]
|
|
97
|
+
}) : null]
|
|
98
|
+
})
|
|
99
|
+
]
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
export { u as QuizLesson };
|
|
104
|
+
|
|
105
|
+
//# sourceMappingURL=QuizLesson.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QuizLesson.js","names":[],"sources":["../../../../src/technospam/components/lesson/QuizLesson.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { CheckCircle2, RotateCcw, Send, XCircle } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../../utils/cn';\nimport { useSubmitLessonQuiz, type TecLesson, type TecQuizResult } from '../../api/technospamApi';\n\n/**\n * Multiple-choice surface for a QUIZ lesson (BOFF-4107). Mount it with\n * `key={lesson.id}` so another lesson starts with a blank attempt. The lesson only\n * carries questions and options; the answer key never reaches the browser.\n * `tecSubmitLessonQuiz` grades the attempt server-side, returns per-question\n * feedback, and completes the lesson for an enrolled learner who passes.\n */\nexport function QuizLesson({ lesson, onPassed }: { lesson: TecLesson; onPassed: () => void }) {\n const questions = lesson.quizQuestions ?? [];\n const [answers, setAnswers] = useState<(number | null)[]>(() => questions.map(() => null));\n const [result, setResult] = useState<TecQuizResult | null>(null);\n const [submitQuiz, { loading }] = useSubmitLessonQuiz();\n\n if (questions.length === 0) {\n return (\n <p className=\"rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted\">\n This quiz has no questions yet.\n </p>\n );\n }\n\n const allAnswered = answers.every((a) => a !== null);\n\n const submit = async (): Promise<void> => {\n const res = await submitQuiz({\n variables: { lessonId: lesson.id, answers: answers.map((a) => a ?? -1) },\n });\n const graded = res.data?.tecSubmitLessonQuiz;\n if (!graded) return;\n setResult(graded);\n if (graded.passed) onPassed();\n };\n\n return (\n <div className=\"space-y-5\">\n {questions.map((question, qi) => {\n const feedback = result?.results[qi];\n return (\n <fieldset\n key={`${lesson.id}-${qi}`}\n className=\"rounded-xl border border-border-seam p-4\"\n disabled={loading}\n >\n <legend className=\"px-1 text-sm font-semibold text-text-primary\">\n {qi + 1}. {question.prompt}\n </legend>\n <div className=\"mt-2 space-y-2\">\n {question.options.map((option, oi) => {\n const chosen = answers[qi] === oi;\n const isCorrect = feedback ? feedback.correctOption === oi : false;\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 feedback &&\n isCorrect &&\n 'border-status-success-border bg-status-success-bg-subtle',\n feedback &&\n chosen &&\n !isCorrect &&\n 'border-status-error-border bg-status-error-bg-subtle'\n )}\n >\n <input\n type=\"radio\"\n name={`tec-quiz-${lesson.id}-${qi}`}\n checked={chosen}\n onChange={() => {\n setAnswers((prev) => prev.map((a, i) => (i === qi ? oi : a)));\n setResult(null);\n }}\n />\n <span className=\"text-text-primary\">{option}</span>\n </label>\n );\n })}\n </div>\n {feedback ? (\n <p\n className={cn(\n 'mt-3 flex items-start gap-1.5 text-sm',\n feedback.correct ? 'text-status-success-text' : 'text-status-error-text'\n )}\n >\n {feedback.correct ? <CheckCircle2 size={15} /> : <XCircle size={15} />}\n <span>\n {feedback.correct ? 'Correct.' : 'Not quite.'}\n {feedback.explanation ? ` ${feedback.explanation}` : ''}\n </span>\n </p>\n ) : null}\n </fieldset>\n );\n })}\n\n {result ? (\n <div\n role=\"status\"\n data-testid=\"lesson-quiz-result\"\n className={cn(\n 'rounded-xl border px-4 py-3 text-sm',\n result.passed\n ? 'border-status-success-border bg-status-success-bg-subtle text-status-success-text'\n : 'border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text'\n )}\n >\n You scored {result.score}% ({result.correctCount} of {result.totalCount} correct).{' '}\n {result.passed\n ? 'Quiz passed.'\n : `You need ${result.passingScore}% to pass. Review the feedback and try again.`}\n </div>\n ) : null}\n\n <div className=\"flex flex-wrap gap-2\">\n <Button onClick={() => void submit()} disabled={loading || !allAnswered}>\n <Send size={15} /> {loading ? 'Checking…' : 'Submit answers'}\n </Button>\n {result && !result.passed ? (\n <Button\n variant=\"outline\"\n onClick={() => {\n setAnswers(questions.map(() => null));\n setResult(null);\n }}\n >\n <RotateCcw size={15} /> Try again\n </Button>\n ) : null}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;AAaA,SAAgB,EAAW,EAAE,WAAQ,eAAyD;CAC5F,IAAM,IAAY,EAAO,iBAAiB,EAAE,EACtC,CAAC,GAAS,KAAc,QAAkC,EAAU,UAAU,KAAK,CAAC,EACpF,CAAC,GAAQ,KAAa,EAA+B,KAAK,EAC1D,CAAC,GAAY,EAAE,gBAAa,GAAqB;AAEvD,KAAI,EAAU,WAAW,EACvB,QACE,kBAAC,KAAD;EAAG,WAAU;YAAuF;EAEhG,CAAA;CAIR,IAAM,IAAc,EAAQ,OAAO,MAAM,MAAM,KAAK,EAE9C,IAAS,YAA2B;EAIxC,IAAM,KAHM,MAAM,EAAW,EAC3B,WAAW;GAAE,UAAU,EAAO;GAAI,SAAS,EAAQ,KAAK,MAAM,KAAK,GAAG;GAAE,EACzE,CAAC,EACiB,MAAM;AACpB,QACL,EAAU,EAAO,EACb,EAAO,UAAQ,GAAU;;AAG/B,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACG,EAAU,KAAK,GAAU,MAAO;IAC/B,IAAM,IAAW,GAAQ,QAAQ;AACjC,WACE,kBAAC,YAAD;KAEE,WAAU;KACV,UAAU;eAHZ;MAKE,kBAAC,UAAD;OAAQ,WAAU;iBAAlB;QACG,IAAK;QAAE;QAAG,EAAS;QACb;;MACT,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAS,QAAQ,KAAK,GAAQ,MAAO;QACpC,IAAM,IAAS,EAAQ,OAAQ,GACzB,IAAY,IAAW,EAAS,kBAAkB,IAAK;AAC7D,eACE,kBAAC,SAAD;SAEE,WAAW,EACT,gGACA,IAAS,+CAA+C,sBACxD,KACE,KACA,4DACF,KACE,KACA,CAAC,KACD,uDACH;mBAZH,CAcE,kBAAC,SAAD;UACE,MAAK;UACL,MAAM,YAAY,EAAO,GAAG,GAAG;UAC/B,SAAS;UACT,gBAAgB;AAEd,WADA,GAAY,MAAS,EAAK,KAAK,GAAG,MAAO,MAAM,IAAK,IAAK,EAAG,CAAC,EAC7D,EAAU,KAAK;;UAEjB,CAAA,EACF,kBAAC,QAAD;UAAM,WAAU;oBAAqB;UAAc,CAAA,CAC7C;WAvBD,EAuBC;SAEV;OACE,CAAA;MACL,IACC,kBAAC,KAAD;OACE,WAAW,EACT,yCACA,EAAS,UAAU,6BAA6B,yBACjD;iBAJH,CAMG,EAAS,UAAU,kBAAC,GAAD,EAAc,MAAM,IAAM,CAAA,GAAG,kBAAC,GAAD,EAAS,MAAM,IAAM,CAAA,EACtE,kBAAC,QAAD,EAAA,UAAA,CACG,EAAS,UAAU,aAAa,cAChC,EAAS,cAAc,IAAI,EAAS,gBAAgB,GAChD,EAAA,CAAA,CACL;WACF;MACK;OAtDJ,GAAG,EAAO,GAAG,GAAG,IAsDZ;KAEb;GAED,IACC,kBAAC,OAAD;IACE,MAAK;IACL,eAAY;IACZ,WAAW,EACT,uCACA,EAAO,SACH,sFACA,oFACL;cARH;KASC;KACa,EAAO;KAAM;KAAI,EAAO;KAAa;KAAK,EAAO;KAAW;KAAW;KAClF,EAAO,SACJ,iBACA,YAAY,EAAO,aAAa;KAChC;QACJ;GAEJ,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAQ,eAAe,KAAK,GAAQ;KAAE,UAAU,KAAW,CAAC;eAA5D;MACE,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA;;MAAE,IAAU,cAAc;MACrC;QACR,KAAU,CAAC,EAAO,SACjB,kBAAC,GAAD;KACE,SAAQ;KACR,eAAe;AAEb,MADA,EAAW,EAAU,UAAU,KAAK,CAAC,EACrC,EAAU,KAAK;;eAJnB,CAOE,kBAAC,GAAD,EAAW,MAAM,IAAM,CAAA,EAAA,aAChB;SACP,KACA;;GACF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { TecLesson, TecLessonKind, TecQuizQuestionKey } from '../../api/technospamApi';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lesson-author drafts for the kind-specific lesson fields (BOFF-4107):
|
|
5
|
+
* the runnable workspace of an INTERACTIVE lesson and the question set of a
|
|
6
|
+
* QUIZ lesson. Pure helpers so the admin editor and its tests share them.
|
|
7
|
+
*/
|
|
8
|
+
/** Languages the platform code sandbox can run (wspace-technospam-svc SUPPORTED_LANGUAGES). */
|
|
9
|
+
export declare const LESSON_LANGUAGES: readonly ["python", "javascript", "typescript", "go", "ruby", "bash"];
|
|
10
|
+
export interface QuizQuestionDraft {
|
|
11
|
+
prompt: string;
|
|
12
|
+
/** One answer option per line. */
|
|
13
|
+
options: string;
|
|
14
|
+
/** Zero-based index of the correct option. */
|
|
15
|
+
correctOption: number;
|
|
16
|
+
explanation: string;
|
|
17
|
+
}
|
|
18
|
+
export interface LessonKindDraft {
|
|
19
|
+
language: string;
|
|
20
|
+
starterCode: string;
|
|
21
|
+
sampleInput: string;
|
|
22
|
+
expectedOutput: string;
|
|
23
|
+
quiz: QuizQuestionDraft[];
|
|
24
|
+
}
|
|
25
|
+
export declare const emptyQuestion: () => QuizQuestionDraft;
|
|
26
|
+
export declare function draftFromLesson(lesson?: Pick<TecLesson, 'language' | 'starterCode' | 'sampleInput' | 'expectedOutput'> | null, quizKey?: TecQuizQuestionKey[] | null): LessonKindDraft;
|
|
27
|
+
export declare function splitOptions(options: string): string[];
|
|
28
|
+
export interface QuizQuestionInputShape {
|
|
29
|
+
prompt: string;
|
|
30
|
+
options: string[];
|
|
31
|
+
correctOption: number;
|
|
32
|
+
explanation: string | null;
|
|
33
|
+
}
|
|
34
|
+
export type LessonKindInput = {
|
|
35
|
+
ok: true;
|
|
36
|
+
input: Record<string, unknown>;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
error: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The create/update input fields for a lesson of `kind`. Blank questions are
|
|
43
|
+
* dropped; a half-written question is reported instead of being sent, so the
|
|
44
|
+
* author sees the problem before the API rejects it.
|
|
45
|
+
*/
|
|
46
|
+
export declare function draftToInput(kind: TecLessonKind, draft: LessonKindDraft): LessonKindInput;
|
|
47
|
+
//# sourceMappingURL=lessonAuthoring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lessonAuthoring.d.ts","sourceRoot":"","sources":["../../../../src/technospam/components/lesson/lessonAuthoring.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE5F;;;;GAIG;AAEH,+FAA+F;AAC/F,eAAO,MAAM,gBAAgB,uEAOnB,CAAC;AAEX,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,iBAAiB,EAAE,CAAC;CAC3B;AAED,eAAO,MAAM,aAAa,QAAO,iBAK/B,CAAC;AAEH,wBAAgB,eAAe,CAC7B,MAAM,CAAC,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,GAAG,aAAa,GAAG,aAAa,GAAG,gBAAgB,CAAC,GAAG,IAAI,EAC9F,OAAO,CAAC,EAAE,kBAAkB,EAAE,GAAG,IAAI,GACpC,eAAe,CAajB;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAKtD;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,MAAM,eAAe,GACzB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9E;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,eAAe,GAAG,eAAe,CAmCzF"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//#region src/technospam/components/lesson/lessonAuthoring.ts
|
|
2
|
+
var e = [
|
|
3
|
+
"python",
|
|
4
|
+
"javascript",
|
|
5
|
+
"typescript",
|
|
6
|
+
"go",
|
|
7
|
+
"ruby",
|
|
8
|
+
"bash"
|
|
9
|
+
], t = () => ({
|
|
10
|
+
prompt: "",
|
|
11
|
+
options: "",
|
|
12
|
+
correctOption: 0,
|
|
13
|
+
explanation: ""
|
|
14
|
+
});
|
|
15
|
+
function n(t, n) {
|
|
16
|
+
return {
|
|
17
|
+
language: t?.language ?? e[0],
|
|
18
|
+
starterCode: t?.starterCode ?? "",
|
|
19
|
+
sampleInput: t?.sampleInput ?? "",
|
|
20
|
+
expectedOutput: t?.expectedOutput ?? "",
|
|
21
|
+
quiz: (n ?? []).map((e) => ({
|
|
22
|
+
prompt: e.prompt,
|
|
23
|
+
options: e.options.join("\n"),
|
|
24
|
+
correctOption: e.correctOption,
|
|
25
|
+
explanation: e.explanation ?? ""
|
|
26
|
+
}))
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function r(e) {
|
|
30
|
+
return e.split("\n").map((e) => e.trim()).filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
function i(e, t) {
|
|
33
|
+
if (e === "INTERACTIVE") return {
|
|
34
|
+
ok: !0,
|
|
35
|
+
input: {
|
|
36
|
+
language: t.language || null,
|
|
37
|
+
starterCode: t.starterCode.trim() ? t.starterCode : null,
|
|
38
|
+
sampleInput: t.sampleInput ? t.sampleInput : null,
|
|
39
|
+
expectedOutput: t.expectedOutput.trim() ? t.expectedOutput : null
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
if (e === "QUIZ") {
|
|
43
|
+
let e = [];
|
|
44
|
+
for (let [n, i] of t.quiz.entries()) {
|
|
45
|
+
let t = i.prompt.trim(), a = r(i.options);
|
|
46
|
+
if (!(!t && a.length === 0)) {
|
|
47
|
+
if (!t) return {
|
|
48
|
+
ok: !1,
|
|
49
|
+
error: `Question ${n + 1} needs a prompt.`
|
|
50
|
+
};
|
|
51
|
+
if (a.length < 2 || a.length > 8) return {
|
|
52
|
+
ok: !1,
|
|
53
|
+
error: `Question ${n + 1} needs two to eight options, one per line.`
|
|
54
|
+
};
|
|
55
|
+
if (i.correctOption < 0 || i.correctOption >= a.length) return {
|
|
56
|
+
ok: !1,
|
|
57
|
+
error: `Pick the correct option for question ${n + 1}.`
|
|
58
|
+
};
|
|
59
|
+
e.push({
|
|
60
|
+
prompt: t,
|
|
61
|
+
options: a,
|
|
62
|
+
correctOption: i.correctOption,
|
|
63
|
+
explanation: i.explanation.trim() || null
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
ok: !0,
|
|
69
|
+
input: { quizQuestions: e }
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
ok: !0,
|
|
74
|
+
input: {}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export { e as LESSON_LANGUAGES, n as draftFromLesson, i as draftToInput, t as emptyQuestion, r as splitOptions };
|
|
79
|
+
|
|
80
|
+
//# sourceMappingURL=lessonAuthoring.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lessonAuthoring.js","names":[],"sources":["../../../../src/technospam/components/lesson/lessonAuthoring.ts"],"sourcesContent":["import type { TecLesson, TecLessonKind, TecQuizQuestionKey } from '../../api/technospamApi';\n\n/**\n * Lesson-author drafts for the kind-specific lesson fields (BOFF-4107):\n * the runnable workspace of an INTERACTIVE lesson and the question set of a\n * QUIZ lesson. Pure helpers so the admin editor and its tests share them.\n */\n\n/** Languages the platform code sandbox can run (wspace-technospam-svc SUPPORTED_LANGUAGES). */\nexport const LESSON_LANGUAGES = [\n 'python',\n 'javascript',\n 'typescript',\n 'go',\n 'ruby',\n 'bash',\n] as const;\n\nexport interface QuizQuestionDraft {\n prompt: string;\n /** One answer option per line. */\n options: string;\n /** Zero-based index of the correct option. */\n correctOption: number;\n explanation: string;\n}\n\nexport interface LessonKindDraft {\n language: string;\n starterCode: string;\n sampleInput: string;\n expectedOutput: string;\n quiz: QuizQuestionDraft[];\n}\n\nexport const emptyQuestion = (): QuizQuestionDraft => ({\n prompt: '',\n options: '',\n correctOption: 0,\n explanation: '',\n});\n\nexport function draftFromLesson(\n lesson?: Pick<TecLesson, 'language' | 'starterCode' | 'sampleInput' | 'expectedOutput'> | null,\n quizKey?: TecQuizQuestionKey[] | null\n): LessonKindDraft {\n return {\n language: lesson?.language ?? LESSON_LANGUAGES[0],\n starterCode: lesson?.starterCode ?? '',\n sampleInput: lesson?.sampleInput ?? '',\n expectedOutput: lesson?.expectedOutput ?? '',\n quiz: (quizKey ?? []).map((q) => ({\n prompt: q.prompt,\n options: q.options.join('\\n'),\n correctOption: q.correctOption,\n explanation: q.explanation ?? '',\n })),\n };\n}\n\nexport function splitOptions(options: string): string[] {\n return options\n .split('\\n')\n .map((o) => o.trim())\n .filter(Boolean);\n}\n\nexport interface QuizQuestionInputShape {\n prompt: string;\n options: string[];\n correctOption: number;\n explanation: string | null;\n}\n\nexport type LessonKindInput =\n { ok: true; input: Record<string, unknown> } | { ok: false; error: string };\n\n/**\n * The create/update input fields for a lesson of `kind`. Blank questions are\n * dropped; a half-written question is reported instead of being sent, so the\n * author sees the problem before the API rejects it.\n */\nexport function draftToInput(kind: TecLessonKind, draft: LessonKindDraft): LessonKindInput {\n if (kind === 'INTERACTIVE') {\n return {\n ok: true,\n input: {\n language: draft.language || null,\n starterCode: draft.starterCode.trim() ? draft.starterCode : null,\n sampleInput: draft.sampleInput ? draft.sampleInput : null,\n expectedOutput: draft.expectedOutput.trim() ? draft.expectedOutput : null,\n },\n };\n }\n if (kind === 'QUIZ') {\n const questions: QuizQuestionInputShape[] = [];\n for (const [i, q] of draft.quiz.entries()) {\n const prompt = q.prompt.trim();\n const options = splitOptions(q.options);\n if (!prompt && options.length === 0) continue;\n if (!prompt) return { ok: false, error: `Question ${i + 1} needs a prompt.` };\n if (options.length < 2 || options.length > 8) {\n return { ok: false, error: `Question ${i + 1} needs two to eight options, one per line.` };\n }\n if (q.correctOption < 0 || q.correctOption >= options.length) {\n return { ok: false, error: `Pick the correct option for question ${i + 1}.` };\n }\n questions.push({\n prompt,\n options,\n correctOption: q.correctOption,\n explanation: q.explanation.trim() || null,\n });\n }\n return { ok: true, input: { quizQuestions: questions } };\n }\n return { ok: true, input: {} };\n}\n"],"mappings":";AASA,IAAa,IAAmB;CAC9B;CACA;CACA;CACA;CACA;CACA;CACD,EAmBY,WAA0C;CACrD,QAAQ;CACR,SAAS;CACT,eAAe;CACf,aAAa;CACd;AAED,SAAgB,EACd,GACA,GACiB;AACjB,QAAO;EACL,UAAU,GAAQ,YAAY,EAAiB;EAC/C,aAAa,GAAQ,eAAe;EACpC,aAAa,GAAQ,eAAe;EACpC,gBAAgB,GAAQ,kBAAkB;EAC1C,OAAO,KAAW,EAAE,EAAE,KAAK,OAAO;GAChC,QAAQ,EAAE;GACV,SAAS,EAAE,QAAQ,KAAK,KAAK;GAC7B,eAAe,EAAE;GACjB,aAAa,EAAE,eAAe;GAC/B,EAAE;EACJ;;AAGH,SAAgB,EAAa,GAA2B;AACtD,QAAO,EACJ,MAAM,KAAK,CACX,KAAK,MAAM,EAAE,MAAM,CAAC,CACpB,OAAO,QAAQ;;AAkBpB,SAAgB,EAAa,GAAqB,GAAyC;AACzF,KAAI,MAAS,cACX,QAAO;EACL,IAAI;EACJ,OAAO;GACL,UAAU,EAAM,YAAY;GAC5B,aAAa,EAAM,YAAY,MAAM,GAAG,EAAM,cAAc;GAC5D,aAAa,EAAM,cAAc,EAAM,cAAc;GACrD,gBAAgB,EAAM,eAAe,MAAM,GAAG,EAAM,iBAAiB;GACtE;EACF;AAEH,KAAI,MAAS,QAAQ;EACnB,IAAM,IAAsC,EAAE;AAC9C,OAAK,IAAM,CAAC,GAAG,MAAM,EAAM,KAAK,SAAS,EAAE;GACzC,IAAM,IAAS,EAAE,OAAO,MAAM,EACxB,IAAU,EAAa,EAAE,QAAQ;AACnC,UAAC,KAAU,EAAQ,WAAW,IAClC;QAAI,CAAC,EAAQ,QAAO;KAAE,IAAI;KAAO,OAAO,YAAY,IAAI,EAAE;KAAmB;AAC7E,QAAI,EAAQ,SAAS,KAAK,EAAQ,SAAS,EACzC,QAAO;KAAE,IAAI;KAAO,OAAO,YAAY,IAAI,EAAE;KAA6C;AAE5F,QAAI,EAAE,gBAAgB,KAAK,EAAE,iBAAiB,EAAQ,OACpD,QAAO;KAAE,IAAI;KAAO,OAAO,wCAAwC,IAAI,EAAE;KAAI;AAE/E,MAAU,KAAK;KACb;KACA;KACA,eAAe,EAAE;KACjB,aAAa,EAAE,YAAY,MAAM,IAAI;KACtC,CAAC;;;AAEJ,SAAO;GAAE,IAAI;GAAM,OAAO,EAAE,eAAe,GAAW;GAAE;;AAE1D,QAAO;EAAE,IAAI;EAAM,OAAO,EAAE;EAAE"}
|
|
@@ -13,7 +13,7 @@ export interface EntityEventPayload {
|
|
|
13
13
|
entityId?: string;
|
|
14
14
|
entityType?: string;
|
|
15
15
|
}
|
|
16
|
-
export type TechnoSpamEventName = 'technospam.page.viewed' | 'technospam.course.viewed' | 'technospam.course.enroll_clicked' | 'technospam.course.enrolled' | 'technospam.course.progress_updated' | 'technospam.course.certificate_claimed' | 'technospam.problem.viewed' | 'technospam.problem.submit_clicked' | 'technospam.problem.submitted' | 'technospam.mentor.viewed' | 'technospam.mentor.book_clicked' | 'technospam.mentor.booked' | 'technospam.mentor.become_mentor_clicked' | 'technospam.mentor.became_mentor' | 'technospam.session.cancel_clicked' | 'technospam.session.cancelled' | 'technospam.session.complete_clicked' | 'technospam.session.completed' | 'technospam.hackathon.viewed' | 'technospam.hackathon.register_team_clicked' | 'technospam.hackathon.team_registered' | 'technospam.hackathon.submit_project_clicked' | 'technospam.hackathon.project_submitted' | 'technospam.search.performed';
|
|
16
|
+
export type TechnoSpamEventName = 'technospam.page.viewed' | 'technospam.course.viewed' | 'technospam.course.enroll_clicked' | 'technospam.course.enrolled' | 'technospam.course.progress_updated' | 'technospam.course.certificate_claimed' | 'technospam.lesson.viewed' | 'technospam.lesson.completed' | 'technospam.problem.viewed' | 'technospam.problem.submit_clicked' | 'technospam.problem.submitted' | 'technospam.mentor.viewed' | 'technospam.mentor.book_clicked' | 'technospam.mentor.booked' | 'technospam.mentor.become_mentor_clicked' | 'technospam.mentor.became_mentor' | 'technospam.session.cancel_clicked' | 'technospam.session.cancelled' | 'technospam.session.complete_clicked' | 'technospam.session.completed' | 'technospam.hackathon.viewed' | 'technospam.hackathon.register_team_clicked' | 'technospam.hackathon.team_registered' | 'technospam.hackathon.submit_project_clicked' | 'technospam.hackathon.project_submitted' | 'technospam.search.performed';
|
|
17
17
|
export declare function useTechnoSpamAnalytics(): {
|
|
18
18
|
emit: (name: TechnoSpamEventName, payload?: EntityEventPayload & Record<string, unknown>) => void;
|
|
19
19
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useTechnoSpamAnalytics.d.ts","sourceRoot":"","sources":["../../../src/technospam/hooks/useTechnoSpamAnalytics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,MAAM,WAAW,kBAAkB;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,mBAAmB,GAC3B,wBAAwB,GACxB,0BAA0B,GAC1B,kCAAkC,GAClC,4BAA4B,GAC5B,oCAAoC,GACpC,uCAAuC,GACvC,2BAA2B,GAC3B,mCAAmC,GACnC,8BAA8B,GAC9B,0BAA0B,GAC1B,gCAAgC,GAChC,0BAA0B,GAC1B,yCAAyC,GACzC,iCAAiC,GACjC,mCAAmC,GACnC,8BAA8B,GAC9B,qCAAqC,GACrC,8BAA8B,GAC9B,6BAA6B,GAC7B,4CAA4C,GAC5C,sCAAsC,GACtC,6CAA6C,GAC7C,wCAAwC,GACxC,6BAA6B,CAAC;AAElC,wBAAgB,sBAAsB;iBAI3B,mBAAmB,YAAW,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;EAOpF"}
|
|
1
|
+
{"version":3,"file":"useTechnoSpamAnalytics.d.ts","sourceRoot":"","sources":["../../../src/technospam/hooks/useTechnoSpamAnalytics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,MAAM,WAAW,kBAAkB;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,mBAAmB,GAC3B,wBAAwB,GACxB,0BAA0B,GAC1B,kCAAkC,GAClC,4BAA4B,GAC5B,oCAAoC,GACpC,uCAAuC,GACvC,0BAA0B,GAC1B,6BAA6B,GAC7B,2BAA2B,GAC3B,mCAAmC,GACnC,8BAA8B,GAC9B,0BAA0B,GAC1B,gCAAgC,GAChC,0BAA0B,GAC1B,yCAAyC,GACzC,iCAAiC,GACjC,mCAAmC,GACnC,8BAA8B,GAC9B,qCAAqC,GACrC,8BAA8B,GAC9B,6BAA6B,GAC7B,4CAA4C,GAC5C,sCAAsC,GACtC,6CAA6C,GAC7C,wCAAwC,GACxC,6BAA6B,CAAC;AAElC,wBAAgB,sBAAsB;iBAI3B,mBAAmB,YAAW,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;EAOpF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useTechnoSpamAnalytics.js","names":[],"sources":["../../../src/technospam/hooks/useTechnoSpamAnalytics.ts"],"sourcesContent":["/**\n * TechnoSpam product analytics helper.\n *\n * Emits events on the shared event bus. The shell's RybbitSink forwards\n * allow-listed fact events to Rybbit Cloud; until TechnoSpam has a site ID,\n * events still flow to the bus and to backend audit / telemetry sinks.\n *\n * Pattern: emit an intent event before a mutation, then a fact event when the\n * server confirms the change.\n */\n\nimport { useCallback } from 'react';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\n\nexport interface EntityEventPayload {\n workspaceId?: string;\n entityId?: string;\n entityType?: string;\n}\n\nexport type TechnoSpamEventName =\n | 'technospam.page.viewed'\n | 'technospam.course.viewed'\n | 'technospam.course.enroll_clicked'\n | 'technospam.course.enrolled'\n | 'technospam.course.progress_updated'\n | 'technospam.course.certificate_claimed'\n | 'technospam.problem.viewed'\n | 'technospam.problem.submit_clicked'\n | 'technospam.problem.submitted'\n | 'technospam.mentor.viewed'\n | 'technospam.mentor.book_clicked'\n | 'technospam.mentor.booked'\n | 'technospam.mentor.become_mentor_clicked'\n | 'technospam.mentor.became_mentor'\n | 'technospam.session.cancel_clicked'\n | 'technospam.session.cancelled'\n | 'technospam.session.complete_clicked'\n | 'technospam.session.completed'\n | 'technospam.hackathon.viewed'\n | 'technospam.hackathon.register_team_clicked'\n | 'technospam.hackathon.team_registered'\n | 'technospam.hackathon.submit_project_clicked'\n | 'technospam.hackathon.project_submitted'\n | 'technospam.search.performed';\n\nexport function useTechnoSpamAnalytics() {\n const bus = useEventBus();\n\n const emit = useCallback(\n (name: TechnoSpamEventName, payload: EntityEventPayload & Record<string, unknown> = {}) => {\n bus.emitCustom(name, payload, { component: 'microfe-technospam' });\n },\n [bus]\n );\n\n return { emit };\n}\n"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"useTechnoSpamAnalytics.js","names":[],"sources":["../../../src/technospam/hooks/useTechnoSpamAnalytics.ts"],"sourcesContent":["/**\n * TechnoSpam product analytics helper.\n *\n * Emits events on the shared event bus. The shell's RybbitSink forwards\n * allow-listed fact events to Rybbit Cloud; until TechnoSpam has a site ID,\n * events still flow to the bus and to backend audit / telemetry sinks.\n *\n * Pattern: emit an intent event before a mutation, then a fact event when the\n * server confirms the change.\n */\n\nimport { useCallback } from 'react';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\n\nexport interface EntityEventPayload {\n workspaceId?: string;\n entityId?: string;\n entityType?: string;\n}\n\nexport type TechnoSpamEventName =\n | 'technospam.page.viewed'\n | 'technospam.course.viewed'\n | 'technospam.course.enroll_clicked'\n | 'technospam.course.enrolled'\n | 'technospam.course.progress_updated'\n | 'technospam.course.certificate_claimed'\n | 'technospam.lesson.viewed'\n | 'technospam.lesson.completed'\n | 'technospam.problem.viewed'\n | 'technospam.problem.submit_clicked'\n | 'technospam.problem.submitted'\n | 'technospam.mentor.viewed'\n | 'technospam.mentor.book_clicked'\n | 'technospam.mentor.booked'\n | 'technospam.mentor.become_mentor_clicked'\n | 'technospam.mentor.became_mentor'\n | 'technospam.session.cancel_clicked'\n | 'technospam.session.cancelled'\n | 'technospam.session.complete_clicked'\n | 'technospam.session.completed'\n | 'technospam.hackathon.viewed'\n | 'technospam.hackathon.register_team_clicked'\n | 'technospam.hackathon.team_registered'\n | 'technospam.hackathon.submit_project_clicked'\n | 'technospam.hackathon.project_submitted'\n | 'technospam.search.performed';\n\nexport function useTechnoSpamAnalytics() {\n const bus = useEventBus();\n\n const emit = useCallback(\n (name: TechnoSpamEventName, payload: EntityEventPayload & Record<string, unknown> = {}) => {\n bus.emitCustom(name, payload, { component: 'microfe-technospam' });\n },\n [bus]\n );\n\n return { emit };\n}\n"],"mappings":";;;AAgDA,SAAgB,IAAyB;CACvC,IAAM,IAAM,GAAa;AASzB,QAAO,EAAE,MAPI,GACV,GAA2B,IAAwD,EAAE,KAAK;AACzF,IAAI,WAAW,GAAM,GAAS,EAAE,WAAW,sBAAsB,CAAC;IAEpE,CAAC,EAAI,CACN,EAEc"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminAdminDashboardPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/AdminAdminDashboardPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AdminAdminDashboardPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/AdminAdminDashboardPagePage.tsx"],"names":[],"mappings":"AAsnDA,wBAAgB,2BAA2B,gCAqD1C"}
|