@burdenoff/microfe-buildmyiq 2026.716.1 → 2026.720.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/buildmyiq/BuildmyiqAdminRoot.d.ts.map +1 -1
  2. package/dist/buildmyiq/BuildmyiqAdminRoot.js +149 -135
  3. package/dist/buildmyiq/BuildmyiqAdminRoot.js.map +1 -1
  4. package/dist/buildmyiq/BuildmyiqRoot.d.ts.map +1 -1
  5. package/dist/buildmyiq/BuildmyiqRoot.js +59 -54
  6. package/dist/buildmyiq/BuildmyiqRoot.js.map +1 -1
  7. package/dist/buildmyiq/BuildmyiqRoutes.js.map +1 -1
  8. package/dist/buildmyiq/components/Avatar.js.map +1 -1
  9. package/dist/buildmyiq/components/BuiDiscussion.js.map +1 -1
  10. package/dist/buildmyiq/components/BuiPermissionButton.d.ts +25 -0
  11. package/dist/buildmyiq/components/BuiPermissionButton.d.ts.map +1 -0
  12. package/dist/buildmyiq/components/BuiPermissionButton.js +15 -0
  13. package/dist/buildmyiq/components/BuiPermissionButton.js.map +1 -0
  14. package/dist/buildmyiq/components/BuiTags.js.map +1 -1
  15. package/dist/buildmyiq/components/DataTable.js.map +1 -1
  16. package/dist/buildmyiq/components/EmptyState.js.map +1 -1
  17. package/dist/buildmyiq/components/EvidenceUpload.js.map +1 -1
  18. package/dist/buildmyiq/components/FileLink.js.map +1 -1
  19. package/dist/buildmyiq/components/FilterTabs.js.map +1 -1
  20. package/dist/buildmyiq/components/MetricsCard.js.map +1 -1
  21. package/dist/buildmyiq/components/MiniBar.js.map +1 -1
  22. package/dist/buildmyiq/components/PageLayout.js.map +1 -1
  23. package/dist/buildmyiq/components/Pagination.js.map +1 -1
  24. package/dist/buildmyiq/components/StateViews.js.map +1 -1
  25. package/dist/buildmyiq/components/StatusBadge.js.map +1 -1
  26. package/dist/buildmyiq/components/Tabs.js.map +1 -1
  27. package/dist/buildmyiq/components/Viz.js.map +1 -1
  28. package/dist/buildmyiq/components/index.d.ts +1 -0
  29. package/dist/buildmyiq/components/index.d.ts.map +1 -1
  30. package/dist/buildmyiq/components/index.js +1 -0
  31. package/dist/buildmyiq/context/BuildmyiqContext.js.map +1 -1
  32. package/dist/buildmyiq/hooks/index.d.ts +1 -0
  33. package/dist/buildmyiq/hooks/index.d.ts.map +1 -1
  34. package/dist/buildmyiq/hooks/index.js +1 -0
  35. package/dist/buildmyiq/hooks/useBuiComments.js.map +1 -1
  36. package/dist/buildmyiq/hooks/useBuiFiles.js.map +1 -1
  37. package/dist/buildmyiq/hooks/useBuiPermissions.d.ts +54 -0
  38. package/dist/buildmyiq/hooks/useBuiPermissions.d.ts.map +1 -0
  39. package/dist/buildmyiq/hooks/useBuiPermissions.js +2 -0
  40. package/dist/buildmyiq/hooks/useBuiTags.js.map +1 -1
  41. package/dist/buildmyiq/hooks/useBuildmyiqApi.js.map +1 -1
  42. package/dist/buildmyiq/hooks/useCursorPager.js.map +1 -1
  43. package/dist/buildmyiq/pages/DashboardPage.js.map +1 -1
  44. package/dist/buildmyiq/pages/_shared/CourseCard.js.map +1 -1
  45. package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.js.map +1 -1
  46. package/dist/buildmyiq/pages/assessments/AssessmentsPage.js.map +1 -1
  47. package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.js.map +1 -1
  48. package/dist/buildmyiq/pages/authoring/AuthoringPage.d.ts.map +1 -1
  49. package/dist/buildmyiq/pages/authoring/AuthoringPage.js +97 -86
  50. package/dist/buildmyiq/pages/authoring/AuthoringPage.js.map +1 -1
  51. package/dist/buildmyiq/pages/authoring/CourseBuilderPage.d.ts.map +1 -1
  52. package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js +159 -118
  53. package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js.map +1 -1
  54. package/dist/buildmyiq/pages/authoring/_forms.js.map +1 -1
  55. package/dist/buildmyiq/pages/catalog/CatalogPage.js.map +1 -1
  56. package/dist/buildmyiq/pages/catalog/CourseDetailPage.d.ts.map +1 -1
  57. package/dist/buildmyiq/pages/catalog/CourseDetailPage.js +75 -69
  58. package/dist/buildmyiq/pages/catalog/CourseDetailPage.js.map +1 -1
  59. package/dist/buildmyiq/pages/certificates/CertificatesPage.d.ts.map +1 -1
  60. package/dist/buildmyiq/pages/certificates/CertificatesPage.js +186 -170
  61. package/dist/buildmyiq/pages/certificates/CertificatesPage.js.map +1 -1
  62. package/dist/buildmyiq/pages/cohorts/CohortsPage.d.ts.map +1 -1
  63. package/dist/buildmyiq/pages/cohorts/CohortsPage.js +177 -166
  64. package/dist/buildmyiq/pages/cohorts/CohortsPage.js.map +1 -1
  65. package/dist/buildmyiq/pages/insights/InsightsPage.js.map +1 -1
  66. package/dist/buildmyiq/pages/learning/LessonPlayerPage.js.map +1 -1
  67. package/dist/buildmyiq/pages/learning/MyLearningPage.js.map +1 -1
  68. package/dist/buildmyiq/pages/paths/PathsPage.js.map +1 -1
  69. package/dist/buildmyiq/pages/skills/SkillsPage.js.map +1 -1
  70. package/dist/buildmyiq/pages/submissions/SubmissionsPage.d.ts.map +1 -1
  71. package/dist/buildmyiq/pages/submissions/SubmissionsPage.js +160 -154
  72. package/dist/buildmyiq/pages/submissions/SubmissionsPage.js.map +1 -1
  73. package/dist/buildmyiq/shared/hooks/useTr.js.map +1 -1
  74. package/dist/buildmyiq/types/index.js.map +1 -1
  75. package/dist/buildmyiq/utils/cn.js.map +1 -1
  76. package/dist/buildmyiq/utils/formatters.js.map +1 -1
  77. package/dist/buildmyiq/utils/navigation.js.map +1 -1
  78. package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
  79. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1 -1
  80. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
  81. package/dist/services/createSandboxAssistantTransport.js.map +1 -1
  82. package/dist/utils/pageContext.d.ts.map +1 -1
  83. package/dist/utils/pageContext.js +7 -2
  84. package/dist/utils/pageContext.js.map +1 -1
  85. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"AssessmentDetailPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/AssessmentDetailPage.tsx"],"sourcesContent":["/**\n * AssessmentDetailPage — a single assessment + its question builder.\n * @module buildmyiq/pages/assessments\n *\n * Reads `buiAssessment` (which includes `questions[]`). Educators can publish a\n * DRAFT assessment (`useBuiPublishAssessment`) and add questions\n * (`useBuiCreateQuestion`). The question builder writes an `options` JSON array\n * of `{ label }` for choice questions and an optional input-only `answerKey`\n * (correct label(s)) — the latter is never read back (not exposed on the type).\n */\n\nimport { useMemo, useState, type FC, type ReactNode } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Plus,\n Send,\n ClipboardCheck,\n ClipboardEdit,\n Circle,\n Square,\n Clock,\n Trophy,\n Repeat,\n Trash2,\n Lightbulb,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Label,\n Textarea,\n Checkbox,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n Section,\n EmptyState,\n LoadingView,\n ErrorView,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiAssessment, useBuiPublishAssessment, useBuiCreateQuestion } from '../../hooks';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport { QUESTION_TYPES } from '../../types';\nimport type { BuiQuestion, BuiQuestionType } from '../../types';\n\n// ---------------------------------------------------------------------------\n// Safe narrowing for the untyped `options` JSON blob (array of { id?, label }).\n// ---------------------------------------------------------------------------\ninterface QuestionOption {\n id?: string;\n label: string;\n}\n\nconst parseOptions = (raw: unknown): QuestionOption[] => {\n if (!Array.isArray(raw)) return [];\n const out: QuestionOption[] = [];\n for (const item of raw) {\n if (item && typeof item === 'object' && 'label' in item) {\n const label = (item as { label: unknown }).label;\n if (typeof label === 'string') {\n const idVal = (item as { id?: unknown }).id;\n out.push({ label, id: typeof idVal === 'string' ? idVal : undefined });\n }\n }\n }\n return out;\n};\n\nconst PROMPT_MAX = 1000;\n\nconst CHOICE_TYPES = new Set<BuiQuestionType>(['SINGLE_CHOICE', 'MULTIPLE_CHOICE']);\nconst isExclusive = (type: BuiQuestionType): boolean =>\n type === 'SINGLE_CHOICE' || type === 'TRUE_FALSE';\n\ninterface OptionDraft {\n label: string;\n correct: boolean;\n}\n\ninterface QuestionFormState {\n prompt: string;\n type: BuiQuestionType;\n points: string;\n explanation: string;\n options: OptionDraft[];\n shortAnswer: string;\n}\n\nconst blankChoiceOptions = (): OptionDraft[] => [\n { label: '', correct: false },\n { label: '', correct: false },\n];\n\nconst trueFalseOptions = (): OptionDraft[] => [\n { label: 'True', correct: false },\n { label: 'False', correct: false },\n];\n\nconst makeEmptyForm = (): QuestionFormState => ({\n prompt: '',\n type: 'SINGLE_CHOICE',\n points: '1',\n explanation: '',\n options: blankChoiceOptions(),\n shortAnswer: '',\n});\n\nexport const AssessmentDetailPage: FC = () => {\n const { assessmentId } = useParams<{ assessmentId: string }>();\n const { navigateTo } = useBuildmyiq();\n\n const assessmentQuery = useBuiAssessment(assessmentId);\n const publishAssessment = useBuiPublishAssessment();\n const createQuestion = useBuiCreateQuestion();\n\n const [dialogOpen, setDialogOpen] = useState(false);\n const [form, setForm] = useState<QuestionFormState>(makeEmptyForm);\n const [formError, setFormError] = useState<string | null>(null);\n\n const assessment = assessmentQuery.data ?? null;\n const questions = useMemo(\n () => [...(assessment?.questions ?? [])].sort((a, b) => a.position - b.position),\n [assessment]\n );\n\n const nextPosition = useMemo(\n () => (questions.length ? Math.max(...questions.map((q) => q.position)) + 1 : 1),\n [questions]\n );\n\n const totalPoints = useMemo(() => questions.reduce((sum, q) => sum + q.points, 0), [questions]);\n\n const usesOptions = form.type === 'TRUE_FALSE' || CHOICE_TYPES.has(form.type);\n\n const openDialog = () => {\n setForm(makeEmptyForm());\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = () => {\n setDialogOpen(false);\n setForm(makeEmptyForm());\n setFormError(null);\n };\n\n const changeType = (type: BuiQuestionType) => {\n setForm((prev) => {\n let options = prev.options;\n if (type === 'TRUE_FALSE') options = trueFalseOptions();\n else if (CHOICE_TYPES.has(type)) {\n options = prev.options.some((o) => o.label === 'True' || o.label === 'False')\n ? blankChoiceOptions()\n : prev.options;\n }\n return { ...prev, type, options };\n });\n };\n\n const setOptionLabel = (index: number, label: string) => {\n setForm((prev) => ({\n ...prev,\n options: prev.options.map((o, i) => (i === index ? { ...o, label } : o)),\n }));\n };\n\n const toggleCorrect = (index: number, value: boolean) => {\n setForm((prev) => ({\n ...prev,\n options: prev.options.map((o, i) =>\n i === index\n ? { ...o, correct: value }\n : isExclusive(prev.type)\n ? { ...o, correct: false }\n : o\n ),\n }));\n };\n\n const addOption = () => {\n setForm((prev) => ({ ...prev, options: [...prev.options, { label: '', correct: false }] }));\n };\n\n const removeOption = (index: number) => {\n setForm((prev) => ({ ...prev, options: prev.options.filter((_, i) => i !== index) }));\n };\n\n const handlePublish = async () => {\n if (!assessment) return;\n try {\n await publishAssessment.mutateAsync(assessment.id);\n } catch {\n // The mutation surfaces its own error state; nothing else to do here.\n }\n };\n\n const handleAddQuestion = async () => {\n if (!assessmentId) return;\n if (!form.prompt.trim()) {\n setFormError('Question prompt is required.');\n return;\n }\n\n let options: { label: string }[] | undefined;\n let answerKey: string[] | string | undefined;\n\n if (usesOptions) {\n const cleaned = form.options\n .map((o) => ({ label: o.label.trim(), correct: o.correct }))\n .filter((o) => o.label.length > 0);\n if (CHOICE_TYPES.has(form.type) && cleaned.length < 2) {\n setFormError('Add at least two options.');\n return;\n }\n options = cleaned.map((o) => ({ label: o.label }));\n const correct = cleaned.filter((o) => o.correct).map((o) => o.label);\n if (correct.length) answerKey = correct;\n } else if (form.type === 'SHORT_ANSWER') {\n answerKey = form.shortAnswer.trim() || undefined;\n }\n\n try {\n await createQuestion.mutateAsync({\n assessmentId,\n prompt: form.prompt.trim(),\n type: form.type,\n points: form.points ? Number(form.points) : undefined,\n position: nextPosition,\n explanation: form.explanation.trim() || undefined,\n options,\n answerKey,\n });\n closeDialog();\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to add question.');\n }\n };\n\n // ----- Loading / error / not-found -----\n if (assessmentQuery.isLoading) {\n return (\n <PageLayout title=\"Assessment\">\n <LoadingView label=\"Loading assessment…\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isError) {\n return (\n <PageLayout title=\"Assessment\">\n <ErrorView\n message=\"Couldn't load this assessment.\"\n onRetry={() => assessmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!assessment) {\n return (\n <PageLayout title=\"Assessment\">\n <EmptyState\n illustration=\"empty-search\"\n title=\"Assessment not found\"\n description=\"This assessment may have been removed or you don't have access to it.\"\n action={<Button onClick={() => navigateTo('assessments')}>Back to assessments</Button>}\n />\n </PageLayout>\n );\n }\n\n const isDraft = assessment.status === 'DRAFT';\n\n return (\n <PageLayout\n title={\n <span className=\"flex min-w-0 items-center gap-3\">\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n <ClipboardEdit className=\"h-5 w-5\" />\n </span>\n <span className=\"truncate\">{assessment.title}</span>\n </span>\n }\n description={assessment.description ?? undefined}\n actions={\n <>\n <Button variant=\"ghost\" onClick={() => navigateTo('submissions')}>\n <ClipboardCheck className=\"mr-1.5 h-4 w-4\" /> Submissions\n </Button>\n {isDraft ? (\n <Button onClick={handlePublish} disabled={publishAssessment.isPending}>\n <Send className=\"mr-1.5 h-4 w-4\" />\n {publishAssessment.isPending ? 'Publishing…' : 'Publish'}\n </Button>\n ) : (\n <StatusBadge status={assessment.status} className=\"px-3 py-1.5\" />\n )}\n </>\n }\n >\n <button\n type=\"button\"\n onClick={() => navigateTo('assessments')}\n className=\"mb-4 inline-flex items-center gap-1.5 text-sm text-text-secondary transition-colors hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> All assessments\n </button>\n\n {/* Meta */}\n <GlassCard treatment=\"glass\" className=\"mb-6 p-5 sm:p-6\">\n <div className=\"mb-5 flex flex-wrap items-center gap-2\">\n <StatusBadge status={assessment.type} />\n <StatusBadge status={assessment.status} />\n {assessment.isGraded ? (\n <StatusBadge status=\"GRADED\" />\n ) : (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\">\n Not graded\n </span>\n )}\n </div>\n <div className=\"grid grid-cols-2 gap-y-6 sm:grid-cols-4\">\n <MetaTile\n icon={<Trophy className=\"h-5 w-5\" />}\n label=\"Passing score\"\n value={`${assessment.passingScore}%`}\n />\n <MetaTile\n icon={<Clock className=\"h-5 w-5\" />}\n label=\"Time limit\"\n value={formatDuration(assessment.timeLimitMin)}\n divider\n />\n <MetaTile\n icon={<Repeat className=\"h-5 w-5\" />}\n label=\"Max attempts\"\n value={assessment.maxAttempts}\n divider\n />\n <MetaTile\n icon={<ClipboardCheck className=\"h-5 w-5\" />}\n label=\"Graded\"\n value={assessment.isGraded ? 'Yes' : 'No'}\n divider\n />\n </div>\n </GlassCard>\n\n {/* Questions */}\n <Section\n title={`Questions (${questions.length})`}\n description={\n questions.length > 0\n ? `${totalPoints} point${totalPoints === 1 ? '' : 's'} across ${questions.length} question${\n questions.length === 1 ? '' : 's'\n }`\n : undefined\n }\n action={\n <Button variant=\"outline\" onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add question\n </Button>\n }\n >\n {questions.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No questions yet\"\n description=\"Build this assessment by adding its first question.\"\n action={\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add question\n </Button>\n }\n />\n ) : (\n <ol className=\"flex flex-col gap-3\">\n {questions.map((q, idx) => (\n <QuestionRow key={q.id} index={idx + 1} question={q} />\n ))}\n </ol>\n )}\n </Section>\n\n {/* Add-question dialog */}\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add question</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Question {nextPosition} · pick a type, write the prompt and mark the correct\n answer(s).\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"prompt\">Prompt *</Label>\n <Textarea\n id=\"prompt\"\n value={form.prompt}\n onChange={(e) => setForm({ ...form, prompt: e.target.value.slice(0, PROMPT_MAX) })}\n placeholder=\"Enter your question prompt…\"\n rows={3}\n maxLength={PROMPT_MAX}\n />\n <p className=\"text-right text-xs tabular-nums text-text-secondary\">\n {form.prompt.length} / {PROMPT_MAX}\n </p>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"questionType\">Type</Label>\n <Select value={form.type} onValueChange={(v) => changeType(v as BuiQuestionType)}>\n <SelectTrigger id=\"questionType\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {QUESTION_TYPES.map((t) => (\n <SelectItem key={t} value={t}>\n {formatEnumLabel(t)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"points\">Points</Label>\n <Input\n id=\"points\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={form.points}\n onChange={(e) => setForm({ ...form, points: e.target.value })}\n placeholder=\"1\"\n />\n </div>\n </div>\n\n {usesOptions && (\n <div className=\"grid gap-2\">\n <Label>Options{form.type !== 'TRUE_FALSE' && ' *'}</Label>\n <p className=\"-mt-1 text-xs text-text-secondary\">\n Tick the {isExclusive(form.type) ? 'single correct answer' : 'correct answers'}.\n </p>\n <div className=\"flex flex-col gap-2\">\n {form.options.map((opt, i) => (\n <div\n key={i}\n className={cn(\n 'flex items-center gap-2.5 rounded-lg border px-2.5 py-1.5 transition-colors',\n opt.correct\n ? 'border-status-success-text bg-status-success-bg-subtle'\n : 'border-border-seam bg-bg-surface'\n )}\n >\n <Checkbox\n checked={opt.correct}\n onCheckedChange={(c) => toggleCorrect(i, c === true)}\n aria-label=\"Mark option correct\"\n />\n <Input\n value={opt.label}\n readOnly={form.type === 'TRUE_FALSE'}\n onChange={(e) => setOptionLabel(i, e.target.value)}\n placeholder={`Option ${i + 1}`}\n className=\"flex-1 border-0 bg-transparent px-1 shadow-none focus-visible:ring-0\"\n />\n {opt.correct && (\n <span className=\"shrink-0 text-xs font-medium text-status-success-text\">\n Correct\n </span>\n )}\n {form.type !== 'TRUE_FALSE' && form.options.length > 2 && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => removeOption(i)}\n aria-label=\"Remove option\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n )}\n </div>\n ))}\n </div>\n {form.type !== 'TRUE_FALSE' && (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={addOption}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add option\n </Button>\n )}\n </div>\n )}\n\n {form.type === 'SHORT_ANSWER' && (\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"shortAnswer\">Expected answer</Label>\n <Input\n id=\"shortAnswer\"\n value={form.shortAnswer}\n onChange={(e) => setForm({ ...form, shortAnswer: e.target.value })}\n placeholder=\"The accepted answer (used as the grading key).\"\n />\n </div>\n )}\n\n {(form.type === 'ESSAY' || form.type === 'CODE') && (\n <p className=\"rounded-lg bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n {formatEnumLabel(form.type)} responses are graded manually from the Submissions\n view.\n </p>\n )}\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"explanation\">Explanation</Label>\n <Textarea\n id=\"explanation\"\n value={form.explanation}\n onChange={(e) => setForm({ ...form, explanation: e.target.value })}\n placeholder=\"Shown to learners after grading (optional).\"\n rows={2}\n />\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button onClick={handleAddQuestion} disabled={createQuestion.isPending}>\n {createQuestion.isPending ? 'Adding…' : 'Add question'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Meta tile — an icon-circle stat used in the assessment summary card.\n// ---------------------------------------------------------------------------\ninterface MetaTileProps {\n icon: ReactNode;\n label: string;\n value: ReactNode;\n /** Draw a subtle left divider on ≥sm (all tiles except the first in the row). */\n divider?: boolean;\n}\n\nconst MetaTile: FC<MetaTileProps> = ({ icon, label, value, divider }) => (\n <div\n className={cn(\n 'flex items-center gap-3',\n divider && 'sm:border-l sm:border-border-seam sm:pl-5'\n )}\n >\n <span className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n {icon}\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">{label}</p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">{value}</p>\n </div>\n </div>\n);\n\n// ---------------------------------------------------------------------------\n// Question row\n// ---------------------------------------------------------------------------\nconst freeTextHint = (type: BuiQuestionType): string => {\n switch (type) {\n case 'SHORT_ANSWER':\n return 'Learners type a short answer — auto-graded against the answer key.';\n case 'ESSAY':\n return 'Open long-form response — graded manually from Submissions.';\n case 'CODE':\n return 'Code response — graded manually from Submissions.';\n default:\n return 'Free-text response.';\n }\n};\n\ninterface QuestionRowProps {\n index: number;\n question: BuiQuestion;\n}\n\nconst QuestionRow: FC<QuestionRowProps> = ({ index, question }) => {\n const options = parseOptions(question.options);\n const isChoice = CHOICE_TYPES.has(question.type) || question.type === 'TRUE_FALSE';\n const isMulti = question.type === 'MULTIPLE_CHOICE';\n\n return (\n <li>\n <GlassCard treatment=\"plain\" className=\"p-4 sm:p-5\">\n <div className=\"flex items-start gap-3.5\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-sm font-bold tabular-nums text-[var(--color-accent-teal)]\">\n {index}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={question.type} />\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium tabular-nums text-text-secondary\">\n {question.points} pt{question.points === 1 ? '' : 's'}\n </span>\n </div>\n\n <p className=\"mt-2.5 font-medium text-text-primary\">{question.prompt}</p>\n\n {isChoice && options.length > 0 && (\n <ul className=\"mt-3 flex flex-col gap-2\">\n {options.map((opt, i) => (\n <li\n key={opt.id ?? `${i}-${opt.label}`}\n className=\"flex items-center gap-2.5 rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n >\n {isMulti ? (\n <Square className=\"h-4 w-4 shrink-0 text-text-secondary opacity-60\" />\n ) : (\n <Circle className=\"h-4 w-4 shrink-0 text-text-secondary opacity-60\" />\n )}\n <span className=\"min-w-0\">{opt.label}</span>\n </li>\n ))}\n </ul>\n )}\n\n {!isChoice && (\n <p className=\"mt-3 rounded-lg border border-dashed border-border-seam bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n {freeTextHint(question.type)}\n </p>\n )}\n\n {question.explanation && (\n <p className=\"mt-3 flex items-start gap-1.5 rounded-lg bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n <Lightbulb className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-status-warning-text\" />\n <span>{question.explanation}</span>\n </p>\n )}\n </div>\n </div>\n </GlassCard>\n </li>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"AssessmentDetailPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/AssessmentDetailPage.tsx"],"sourcesContent":["/**\n * AssessmentDetailPage — a single assessment + its question builder.\n * @module buildmyiq/pages/assessments\n *\n * Reads `buiAssessment` (which includes `questions[]`). Educators can publish a\n * DRAFT assessment (`useBuiPublishAssessment`) and add questions\n * (`useBuiCreateQuestion`). The question builder writes an `options` JSON array\n * of `{ label }` for choice questions and an optional input-only `answerKey`\n * (correct label(s)) — the latter is never read back (not exposed on the type).\n */\n\nimport { useMemo, useState, type FC, type ReactNode } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Plus,\n Send,\n ClipboardCheck,\n ClipboardEdit,\n Circle,\n Square,\n Clock,\n Trophy,\n Repeat,\n Trash2,\n Lightbulb,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Label,\n Textarea,\n Checkbox,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n Section,\n EmptyState,\n LoadingView,\n ErrorView,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiAssessment, useBuiPublishAssessment, useBuiCreateQuestion } from '../../hooks';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport { QUESTION_TYPES } from '../../types';\nimport type { BuiQuestion, BuiQuestionType } from '../../types';\n\n// ---------------------------------------------------------------------------\n// Safe narrowing for the untyped `options` JSON blob (array of { id?, label }).\n// ---------------------------------------------------------------------------\ninterface QuestionOption {\n id?: string;\n label: string;\n}\n\nconst parseOptions = (raw: unknown): QuestionOption[] => {\n if (!Array.isArray(raw)) return [];\n const out: QuestionOption[] = [];\n for (const item of raw) {\n if (item && typeof item === 'object' && 'label' in item) {\n const label = (item as { label: unknown }).label;\n if (typeof label === 'string') {\n const idVal = (item as { id?: unknown }).id;\n out.push({ label, id: typeof idVal === 'string' ? idVal : undefined });\n }\n }\n }\n return out;\n};\n\nconst PROMPT_MAX = 1000;\n\nconst CHOICE_TYPES = new Set<BuiQuestionType>(['SINGLE_CHOICE', 'MULTIPLE_CHOICE']);\nconst isExclusive = (type: BuiQuestionType): boolean =>\n type === 'SINGLE_CHOICE' || type === 'TRUE_FALSE';\n\ninterface OptionDraft {\n label: string;\n correct: boolean;\n}\n\ninterface QuestionFormState {\n prompt: string;\n type: BuiQuestionType;\n points: string;\n explanation: string;\n options: OptionDraft[];\n shortAnswer: string;\n}\n\nconst blankChoiceOptions = (): OptionDraft[] => [\n { label: '', correct: false },\n { label: '', correct: false },\n];\n\nconst trueFalseOptions = (): OptionDraft[] => [\n { label: 'True', correct: false },\n { label: 'False', correct: false },\n];\n\nconst makeEmptyForm = (): QuestionFormState => ({\n prompt: '',\n type: 'SINGLE_CHOICE',\n points: '1',\n explanation: '',\n options: blankChoiceOptions(),\n shortAnswer: '',\n});\n\nexport const AssessmentDetailPage: FC = () => {\n const { assessmentId } = useParams<{ assessmentId: string }>();\n const { navigateTo } = useBuildmyiq();\n\n const assessmentQuery = useBuiAssessment(assessmentId);\n const publishAssessment = useBuiPublishAssessment();\n const createQuestion = useBuiCreateQuestion();\n\n const [dialogOpen, setDialogOpen] = useState(false);\n const [form, setForm] = useState<QuestionFormState>(makeEmptyForm);\n const [formError, setFormError] = useState<string | null>(null);\n\n const assessment = assessmentQuery.data ?? null;\n const questions = useMemo(\n () => [...(assessment?.questions ?? [])].sort((a, b) => a.position - b.position),\n [assessment]\n );\n\n const nextPosition = useMemo(\n () => (questions.length ? Math.max(...questions.map((q) => q.position)) + 1 : 1),\n [questions]\n );\n\n const totalPoints = useMemo(() => questions.reduce((sum, q) => sum + q.points, 0), [questions]);\n\n const usesOptions = form.type === 'TRUE_FALSE' || CHOICE_TYPES.has(form.type);\n\n const openDialog = () => {\n setForm(makeEmptyForm());\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = () => {\n setDialogOpen(false);\n setForm(makeEmptyForm());\n setFormError(null);\n };\n\n const changeType = (type: BuiQuestionType) => {\n setForm((prev) => {\n let options = prev.options;\n if (type === 'TRUE_FALSE') options = trueFalseOptions();\n else if (CHOICE_TYPES.has(type)) {\n options = prev.options.some((o) => o.label === 'True' || o.label === 'False')\n ? blankChoiceOptions()\n : prev.options;\n }\n return { ...prev, type, options };\n });\n };\n\n const setOptionLabel = (index: number, label: string) => {\n setForm((prev) => ({\n ...prev,\n options: prev.options.map((o, i) => (i === index ? { ...o, label } : o)),\n }));\n };\n\n const toggleCorrect = (index: number, value: boolean) => {\n setForm((prev) => ({\n ...prev,\n options: prev.options.map((o, i) =>\n i === index\n ? { ...o, correct: value }\n : isExclusive(prev.type)\n ? { ...o, correct: false }\n : o\n ),\n }));\n };\n\n const addOption = () => {\n setForm((prev) => ({ ...prev, options: [...prev.options, { label: '', correct: false }] }));\n };\n\n const removeOption = (index: number) => {\n setForm((prev) => ({ ...prev, options: prev.options.filter((_, i) => i !== index) }));\n };\n\n const handlePublish = async () => {\n if (!assessment) return;\n try {\n await publishAssessment.mutateAsync(assessment.id);\n } catch {\n // The mutation surfaces its own error state; nothing else to do here.\n }\n };\n\n const handleAddQuestion = async () => {\n if (!assessmentId) return;\n if (!form.prompt.trim()) {\n setFormError('Question prompt is required.');\n return;\n }\n\n let options: { label: string }[] | undefined;\n let answerKey: string[] | string | undefined;\n\n if (usesOptions) {\n const cleaned = form.options\n .map((o) => ({ label: o.label.trim(), correct: o.correct }))\n .filter((o) => o.label.length > 0);\n if (CHOICE_TYPES.has(form.type) && cleaned.length < 2) {\n setFormError('Add at least two options.');\n return;\n }\n options = cleaned.map((o) => ({ label: o.label }));\n const correct = cleaned.filter((o) => o.correct).map((o) => o.label);\n if (correct.length) answerKey = correct;\n } else if (form.type === 'SHORT_ANSWER') {\n answerKey = form.shortAnswer.trim() || undefined;\n }\n\n try {\n await createQuestion.mutateAsync({\n assessmentId,\n prompt: form.prompt.trim(),\n type: form.type,\n points: form.points ? Number(form.points) : undefined,\n position: nextPosition,\n explanation: form.explanation.trim() || undefined,\n options,\n answerKey,\n });\n closeDialog();\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to add question.');\n }\n };\n\n // ----- Loading / error / not-found -----\n if (assessmentQuery.isLoading) {\n return (\n <PageLayout title=\"Assessment\">\n <LoadingView label=\"Loading assessment…\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isError) {\n return (\n <PageLayout title=\"Assessment\">\n <ErrorView\n message=\"Couldn't load this assessment.\"\n onRetry={() => assessmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!assessment) {\n return (\n <PageLayout title=\"Assessment\">\n <EmptyState\n illustration=\"empty-search\"\n title=\"Assessment not found\"\n description=\"This assessment may have been removed or you don't have access to it.\"\n action={<Button onClick={() => navigateTo('assessments')}>Back to assessments</Button>}\n />\n </PageLayout>\n );\n }\n\n const isDraft = assessment.status === 'DRAFT';\n\n return (\n <PageLayout\n title={\n <span className=\"flex min-w-0 items-center gap-3\">\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n <ClipboardEdit className=\"h-5 w-5\" />\n </span>\n <span className=\"truncate\">{assessment.title}</span>\n </span>\n }\n description={assessment.description ?? undefined}\n actions={\n <>\n <Button variant=\"ghost\" onClick={() => navigateTo('submissions')}>\n <ClipboardCheck className=\"mr-1.5 h-4 w-4\" /> Submissions\n </Button>\n {isDraft ? (\n <Button onClick={handlePublish} disabled={publishAssessment.isPending}>\n <Send className=\"mr-1.5 h-4 w-4\" />\n {publishAssessment.isPending ? 'Publishing…' : 'Publish'}\n </Button>\n ) : (\n <StatusBadge status={assessment.status} className=\"px-3 py-1.5\" />\n )}\n </>\n }\n >\n <button\n type=\"button\"\n onClick={() => navigateTo('assessments')}\n className=\"mb-4 inline-flex items-center gap-1.5 text-sm text-text-secondary transition-colors hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> All assessments\n </button>\n\n {/* Meta */}\n <GlassCard treatment=\"glass\" className=\"mb-6 p-5 sm:p-6\">\n <div className=\"mb-5 flex flex-wrap items-center gap-2\">\n <StatusBadge status={assessment.type} />\n <StatusBadge status={assessment.status} />\n {assessment.isGraded ? (\n <StatusBadge status=\"GRADED\" />\n ) : (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\">\n Not graded\n </span>\n )}\n </div>\n <div className=\"grid grid-cols-2 gap-y-6 sm:grid-cols-4\">\n <MetaTile\n icon={<Trophy className=\"h-5 w-5\" />}\n label=\"Passing score\"\n value={`${assessment.passingScore}%`}\n />\n <MetaTile\n icon={<Clock className=\"h-5 w-5\" />}\n label=\"Time limit\"\n value={formatDuration(assessment.timeLimitMin)}\n divider\n />\n <MetaTile\n icon={<Repeat className=\"h-5 w-5\" />}\n label=\"Max attempts\"\n value={assessment.maxAttempts}\n divider\n />\n <MetaTile\n icon={<ClipboardCheck className=\"h-5 w-5\" />}\n label=\"Graded\"\n value={assessment.isGraded ? 'Yes' : 'No'}\n divider\n />\n </div>\n </GlassCard>\n\n {/* Questions */}\n <Section\n title={`Questions (${questions.length})`}\n description={\n questions.length > 0\n ? `${totalPoints} point${totalPoints === 1 ? '' : 's'} across ${questions.length} question${\n questions.length === 1 ? '' : 's'\n }`\n : undefined\n }\n action={\n <Button variant=\"outline\" onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add question\n </Button>\n }\n >\n {questions.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No questions yet\"\n description=\"Build this assessment by adding its first question.\"\n action={\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add question\n </Button>\n }\n />\n ) : (\n <ol className=\"flex flex-col gap-3\">\n {questions.map((q, idx) => (\n <QuestionRow key={q.id} index={idx + 1} question={q} />\n ))}\n </ol>\n )}\n </Section>\n\n {/* Add-question dialog */}\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add question</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Question {nextPosition} · pick a type, write the prompt and mark the correct\n answer(s).\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"prompt\">Prompt *</Label>\n <Textarea\n id=\"prompt\"\n value={form.prompt}\n onChange={(e) => setForm({ ...form, prompt: e.target.value.slice(0, PROMPT_MAX) })}\n placeholder=\"Enter your question prompt…\"\n rows={3}\n maxLength={PROMPT_MAX}\n />\n <p className=\"text-right text-xs tabular-nums text-text-secondary\">\n {form.prompt.length} / {PROMPT_MAX}\n </p>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"questionType\">Type</Label>\n <Select value={form.type} onValueChange={(v) => changeType(v as BuiQuestionType)}>\n <SelectTrigger id=\"questionType\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {QUESTION_TYPES.map((t) => (\n <SelectItem key={t} value={t}>\n {formatEnumLabel(t)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"points\">Points</Label>\n <Input\n id=\"points\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={form.points}\n onChange={(e) => setForm({ ...form, points: e.target.value })}\n placeholder=\"1\"\n />\n </div>\n </div>\n\n {usesOptions && (\n <div className=\"grid gap-2\">\n <Label>Options{form.type !== 'TRUE_FALSE' && ' *'}</Label>\n <p className=\"-mt-1 text-xs text-text-secondary\">\n Tick the {isExclusive(form.type) ? 'single correct answer' : 'correct answers'}.\n </p>\n <div className=\"flex flex-col gap-2\">\n {form.options.map((opt, i) => (\n <div\n key={i}\n className={cn(\n 'flex items-center gap-2.5 rounded-lg border px-2.5 py-1.5 transition-colors',\n opt.correct\n ? 'border-status-success-text bg-status-success-bg-subtle'\n : 'border-border-seam bg-bg-surface'\n )}\n >\n <Checkbox\n checked={opt.correct}\n onCheckedChange={(c) => toggleCorrect(i, c === true)}\n aria-label=\"Mark option correct\"\n />\n <Input\n value={opt.label}\n readOnly={form.type === 'TRUE_FALSE'}\n onChange={(e) => setOptionLabel(i, e.target.value)}\n placeholder={`Option ${i + 1}`}\n className=\"flex-1 border-0 bg-transparent px-1 shadow-none focus-visible:ring-0\"\n />\n {opt.correct && (\n <span className=\"shrink-0 text-xs font-medium text-status-success-text\">\n Correct\n </span>\n )}\n {form.type !== 'TRUE_FALSE' && form.options.length > 2 && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => removeOption(i)}\n aria-label=\"Remove option\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n )}\n </div>\n ))}\n </div>\n {form.type !== 'TRUE_FALSE' && (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={addOption}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add option\n </Button>\n )}\n </div>\n )}\n\n {form.type === 'SHORT_ANSWER' && (\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"shortAnswer\">Expected answer</Label>\n <Input\n id=\"shortAnswer\"\n value={form.shortAnswer}\n onChange={(e) => setForm({ ...form, shortAnswer: e.target.value })}\n placeholder=\"The accepted answer (used as the grading key).\"\n />\n </div>\n )}\n\n {(form.type === 'ESSAY' || form.type === 'CODE') && (\n <p className=\"rounded-lg bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n {formatEnumLabel(form.type)} responses are graded manually from the Submissions\n view.\n </p>\n )}\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"explanation\">Explanation</Label>\n <Textarea\n id=\"explanation\"\n value={form.explanation}\n onChange={(e) => setForm({ ...form, explanation: e.target.value })}\n placeholder=\"Shown to learners after grading (optional).\"\n rows={2}\n />\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button onClick={handleAddQuestion} disabled={createQuestion.isPending}>\n {createQuestion.isPending ? 'Adding…' : 'Add question'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Meta tile — an icon-circle stat used in the assessment summary card.\n// ---------------------------------------------------------------------------\ninterface MetaTileProps {\n icon: ReactNode;\n label: string;\n value: ReactNode;\n /** Draw a subtle left divider on ≥sm (all tiles except the first in the row). */\n divider?: boolean;\n}\n\nconst MetaTile: FC<MetaTileProps> = ({ icon, label, value, divider }) => (\n <div\n className={cn(\n 'flex items-center gap-3',\n divider && 'sm:border-l sm:border-border-seam sm:pl-5'\n )}\n >\n <span className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n {icon}\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">{label}</p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">{value}</p>\n </div>\n </div>\n);\n\n// ---------------------------------------------------------------------------\n// Question row\n// ---------------------------------------------------------------------------\nconst freeTextHint = (type: BuiQuestionType): string => {\n switch (type) {\n case 'SHORT_ANSWER':\n return 'Learners type a short answer — auto-graded against the answer key.';\n case 'ESSAY':\n return 'Open long-form response — graded manually from Submissions.';\n case 'CODE':\n return 'Code response — graded manually from Submissions.';\n default:\n return 'Free-text response.';\n }\n};\n\ninterface QuestionRowProps {\n index: number;\n question: BuiQuestion;\n}\n\nconst QuestionRow: FC<QuestionRowProps> = ({ index, question }) => {\n const options = parseOptions(question.options);\n const isChoice = CHOICE_TYPES.has(question.type) || question.type === 'TRUE_FALSE';\n const isMulti = question.type === 'MULTIPLE_CHOICE';\n\n return (\n <li>\n <GlassCard treatment=\"plain\" className=\"p-4 sm:p-5\">\n <div className=\"flex items-start gap-3.5\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-sm font-bold tabular-nums text-[var(--color-accent-teal)]\">\n {index}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={question.type} />\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium tabular-nums text-text-secondary\">\n {question.points} pt{question.points === 1 ? '' : 's'}\n </span>\n </div>\n\n <p className=\"mt-2.5 font-medium text-text-primary\">{question.prompt}</p>\n\n {isChoice && options.length > 0 && (\n <ul className=\"mt-3 flex flex-col gap-2\">\n {options.map((opt, i) => (\n <li\n key={opt.id ?? `${i}-${opt.label}`}\n className=\"flex items-center gap-2.5 rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n >\n {isMulti ? (\n <Square className=\"h-4 w-4 shrink-0 text-text-secondary opacity-60\" />\n ) : (\n <Circle className=\"h-4 w-4 shrink-0 text-text-secondary opacity-60\" />\n )}\n <span className=\"min-w-0\">{opt.label}</span>\n </li>\n ))}\n </ul>\n )}\n\n {!isChoice && (\n <p className=\"mt-3 rounded-lg border border-dashed border-border-seam bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n {freeTextHint(question.type)}\n </p>\n )}\n\n {question.explanation && (\n <p className=\"mt-3 flex items-start gap-1.5 rounded-lg bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n <Lightbulb className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-status-warning-text\" />\n <span>{question.explanation}</span>\n </p>\n )}\n </div>\n </div>\n </GlassCard>\n </li>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"AssessmentsPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/AssessmentsPage.tsx"],"sourcesContent":["/**\n * AssessmentsPage — the assessment authoring surface (primary page of the section).\n * @module buildmyiq/pages/assessments\n *\n * `buiListAssessments` REQUIRES a courseId, so this page is course-scoped: the\n * educator first picks a course, then sees its assessments filtered by status.\n * Everything is real `bui*` data — no fabrication. Create flow wires\n * `useBuiCreateAssessment`.\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { Plus, ClipboardList, CheckCircle2, FileEdit, ListChecks } from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Label,\n Textarea,\n Switch,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n StatusBadge,\n FilterTabs,\n DataTable,\n EmptyState,\n LoadingView,\n ErrorView,\n type Column,\n type FilterOption,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiCourses, useBuiAssessments, useBuiCreateAssessment } from '../../hooks';\nimport { formatEnumLabel, formatRelativeTime } from '../../utils/formatters';\nimport { ASSESSMENT_TYPES } from '../../types';\nimport type { BuiAssessment, BuiAssessmentStatus, BuiAssessmentType } from '../../types';\n\ntype StatusFilter = 'ALL' | BuiAssessmentStatus;\n\nconst STATUS_TABS: { value: StatusFilter; label: string }[] = [\n { value: 'ALL', label: 'All' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'PUBLISHED', label: 'Published' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\ninterface AssessmentFormState {\n title: string;\n description: string;\n type: BuiAssessmentType;\n passingScore: string;\n maxAttempts: string;\n timeLimitMin: string;\n isGraded: boolean;\n}\n\nconst makeEmptyForm = (): AssessmentFormState => ({\n title: '',\n description: '',\n type: 'QUIZ',\n passingScore: '70',\n maxAttempts: '1',\n timeLimitMin: '0',\n isGraded: true,\n});\n\nexport const AssessmentsPage: FC = () => {\n const { navigateTo } = useBuildmyiq();\n\n const [courseId, setCourseId] = useState<string>('');\n const [status, setStatus] = useState<StatusFilter>('ALL');\n const [dialogOpen, setDialogOpen] = useState(false);\n const [form, setForm] = useState<AssessmentFormState>(makeEmptyForm);\n const [formError, setFormError] = useState<string | null>(null);\n\n const coursesQuery = useBuiCourses({ limit: 100 });\n // Fetch the full (unfiltered) list so the tab counts stay accurate; filter client-side.\n const assessmentsQuery = useBuiAssessments(courseId || undefined);\n const createAssessment = useBuiCreateAssessment();\n\n const courses = coursesQuery.data?.items ?? [];\n const assessments = useMemo(() => assessmentsQuery.data ?? [], [assessmentsQuery.data]);\n\n const statusCounts = useMemo(() => {\n const counts: Record<StatusFilter, number> = {\n ALL: assessments.length,\n DRAFT: 0,\n PUBLISHED: 0,\n ARCHIVED: 0,\n };\n for (const a of assessments) counts[a.status] += 1;\n return counts;\n }, [assessments]);\n\n const rows = useMemo(\n () => (status === 'ALL' ? assessments : assessments.filter((a) => a.status === status)),\n [assessments, status]\n );\n\n const totalQuestions = useMemo(\n () => assessments.reduce((sum, a) => sum + (a.questions?.length ?? 0), 0),\n [assessments]\n );\n\n const tabOptions: FilterOption<StatusFilter>[] = STATUS_TABS.map((t) => ({\n value: t.value,\n label: t.label,\n count: statusCounts[t.value],\n }));\n\n const columns: Column<BuiAssessment>[] = [\n {\n key: 'title',\n header: 'Assessment',\n render: (a) => (\n <div className=\"min-w-0\">\n <p className=\"font-semibold text-text-primary\">{a.title}</p>\n {a.description && (\n <p\n className=\"mt-0.5 max-w-md truncate text-xs text-text-secondary\"\n title={a.description}\n >\n {a.description}\n </p>\n )}\n </div>\n ),\n },\n {\n key: 'type',\n header: 'Type',\n render: (a) => <StatusBadge status={a.type} />,\n },\n {\n key: 'status',\n header: 'Status',\n sortValue: (a) => a.status,\n render: (a) => <StatusBadge status={a.status} />,\n },\n {\n key: 'passingScore',\n header: 'Pass',\n align: 'right',\n hideOnMobile: true,\n sortValue: (a) => a.passingScore,\n render: (a) => <span className=\"tabular-nums text-text-secondary\">{a.passingScore}%</span>,\n },\n {\n key: 'questions',\n header: 'Questions',\n align: 'right',\n hideOnMobile: true,\n render: (a) => (\n <span className=\"tabular-nums text-text-secondary\">{a.questions?.length ?? '—'}</span>\n ),\n },\n {\n key: 'updatedAt',\n header: 'Updated',\n hideOnMobile: true,\n sortValue: (a) => a.updatedAt,\n render: (a) => (\n <span className=\"whitespace-nowrap text-text-secondary\">\n {formatRelativeTime(a.updatedAt)}\n </span>\n ),\n },\n ];\n\n const openDialog = () => {\n setForm(makeEmptyForm());\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = () => {\n setDialogOpen(false);\n setForm(makeEmptyForm());\n setFormError(null);\n };\n\n const handleCreate = async () => {\n if (!courseId) {\n setFormError('Pick a course first.');\n return;\n }\n if (!form.title.trim()) {\n setFormError('Title is required.');\n return;\n }\n try {\n const created = await createAssessment.mutateAsync({\n courseId,\n title: form.title.trim(),\n description: form.description.trim() || undefined,\n type: form.type,\n passingScore: form.passingScore ? Number(form.passingScore) : undefined,\n maxAttempts: form.maxAttempts ? Number(form.maxAttempts) : undefined,\n timeLimitMin: form.timeLimitMin ? Number(form.timeLimitMin) : undefined,\n isGraded: form.isGraded,\n });\n closeDialog();\n navigateTo(`assessments/${created.id}`);\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to create assessment.');\n }\n };\n\n return (\n <PageLayout\n title=\"Assessments\"\n description=\"Quizzes, exams and graded assignments for every course in your workspace.\"\n titleGradient\n aurora\n actions={\n <Button onClick={openDialog} disabled={!courseId}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New assessment\n </Button>\n }\n >\n <GlassCard treatment=\"plain\" className=\"mb-5 p-4\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end\">\n <div className=\"grid gap-1.5\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Course\n </span>\n <Select\n value={courseId}\n onValueChange={(v) => {\n setCourseId(v);\n setStatus('ALL');\n }}\n >\n <SelectTrigger className=\"w-full sm:w-72\">\n <SelectValue placeholder=\"Select a course\" />\n </SelectTrigger>\n <SelectContent>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {coursesQuery.isSuccess && courses.length === 0 && (\n <p className=\"text-xs text-text-secondary\">\n No courses yet — create one in Authoring first.\n </p>\n )}\n </div>\n {courseId && (\n <div className=\"grid gap-1.5\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Status\n </span>\n <FilterTabs options={tabOptions} value={status} onChange={setStatus} />\n </div>\n )}\n </div>\n </GlassCard>\n\n {courseId && assessmentsQuery.isSuccess && assessments.length > 0 && (\n <div className=\"mb-5 grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <MetricsCard\n icon={<ClipboardList className=\"h-5 w-5\" />}\n value={statusCounts.ALL}\n label=\"Assessments\"\n />\n <MetricsCard\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n value={statusCounts.PUBLISHED}\n label=\"Published\"\n accent=\"success\"\n />\n <MetricsCard\n icon={<FileEdit className=\"h-5 w-5\" />}\n value={statusCounts.DRAFT}\n label=\"Drafts\"\n />\n <MetricsCard\n icon={<ListChecks className=\"h-5 w-5\" />}\n value={totalQuestions}\n label=\"Questions\"\n />\n </div>\n )}\n\n {!courseId ? (\n <EmptyState\n illustration=\"onboarding\"\n icon={<ClipboardList className=\"h-10 w-10\" />}\n title=\"Pick a course to see its assessments\"\n description=\"Assessments are scoped to a course. Choose a course above to view, filter and create its quizzes and exams.\"\n />\n ) : assessmentsQuery.isLoading ? (\n <LoadingView label=\"Loading assessments…\" />\n ) : assessmentsQuery.isError ? (\n <ErrorView\n message=\"Couldn't load assessments for this course.\"\n onRetry={() => assessmentsQuery.refetch()}\n />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<ClipboardList className=\"h-10 w-10\" />}\n title={status === 'ALL' ? 'No assessments yet' : 'No assessments match this filter'}\n description={\n status === 'ALL'\n ? 'Create the first quiz, exam or assignment for this course.'\n : 'Try a different status filter, or create a new assessment.'\n }\n action={\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New assessment\n </Button>\n }\n />\n ) : (\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(a) => a.id}\n onRowClick={(a) => navigateTo(`assessments/${a.id}`)}\n defaultSort={{ key: 'updatedAt', dir: 'desc' }}\n />\n )}\n\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New assessment</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Add a quiz, exam or assignment to this course. You can add questions after it&apos;s\n created.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentTitle\">Title *</Label>\n <Input\n id=\"assessmentTitle\"\n value={form.title}\n onChange={(e) => setForm({ ...form, title: e.target.value })}\n placeholder=\"e.g. Module 1 knowledge check\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentDescription\">Description</Label>\n <Textarea\n id=\"assessmentDescription\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n placeholder=\"What this assessment covers and how it's graded.\"\n rows={2}\n />\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentType\">Type</Label>\n <Select\n value={form.type}\n onValueChange={(v) => setForm({ ...form, type: v as BuiAssessmentType })}\n >\n <SelectTrigger id=\"assessmentType\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ASSESSMENT_TYPES.map((t) => (\n <SelectItem key={t} value={t}>\n {formatEnumLabel(t)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"passingScore\">Passing score (%)</Label>\n <Input\n id=\"passingScore\"\n type=\"number\"\n min=\"0\"\n max=\"100\"\n step=\"1\"\n value={form.passingScore}\n onChange={(e) => setForm({ ...form, passingScore: e.target.value })}\n placeholder=\"70\"\n />\n </div>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"maxAttempts\">Max attempts</Label>\n <Input\n id=\"maxAttempts\"\n type=\"number\"\n min=\"1\"\n step=\"1\"\n value={form.maxAttempts}\n onChange={(e) => setForm({ ...form, maxAttempts: e.target.value })}\n placeholder=\"1\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"timeLimitMin\">Time limit (minutes)</Label>\n <Input\n id=\"timeLimitMin\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={form.timeLimitMin}\n onChange={(e) => setForm({ ...form, timeLimitMin: e.target.value })}\n placeholder=\"0 = no limit\"\n />\n </div>\n </div>\n\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface px-3 py-2.5\">\n <div className=\"min-w-0\">\n <Label htmlFor=\"isGraded\" className=\"cursor-pointer\">\n Graded\n </Label>\n <p className=\"text-xs text-text-secondary\">\n Contributes to the learner&apos;s score and pass/fail outcome.\n </p>\n </div>\n <Switch\n id=\"isGraded\"\n checked={form.isGraded}\n onCheckedChange={(checked) => setForm({ ...form, isGraded: checked })}\n />\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button onClick={handleCreate} disabled={createAssessment.isPending}>\n {createAssessment.isPending ? 'Creating…' : 'Create assessment'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"AssessmentsPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/AssessmentsPage.tsx"],"sourcesContent":["/**\n * AssessmentsPage — the assessment authoring surface (primary page of the section).\n * @module buildmyiq/pages/assessments\n *\n * `buiListAssessments` REQUIRES a courseId, so this page is course-scoped: the\n * educator first picks a course, then sees its assessments filtered by status.\n * Everything is real `bui*` data — no fabrication. Create flow wires\n * `useBuiCreateAssessment`.\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { Plus, ClipboardList, CheckCircle2, FileEdit, ListChecks } from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Label,\n Textarea,\n Switch,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n StatusBadge,\n FilterTabs,\n DataTable,\n EmptyState,\n LoadingView,\n ErrorView,\n type Column,\n type FilterOption,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiCourses, useBuiAssessments, useBuiCreateAssessment } from '../../hooks';\nimport { formatEnumLabel, formatRelativeTime } from '../../utils/formatters';\nimport { ASSESSMENT_TYPES } from '../../types';\nimport type { BuiAssessment, BuiAssessmentStatus, BuiAssessmentType } from '../../types';\n\ntype StatusFilter = 'ALL' | BuiAssessmentStatus;\n\nconst STATUS_TABS: { value: StatusFilter; label: string }[] = [\n { value: 'ALL', label: 'All' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'PUBLISHED', label: 'Published' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\ninterface AssessmentFormState {\n title: string;\n description: string;\n type: BuiAssessmentType;\n passingScore: string;\n maxAttempts: string;\n timeLimitMin: string;\n isGraded: boolean;\n}\n\nconst makeEmptyForm = (): AssessmentFormState => ({\n title: '',\n description: '',\n type: 'QUIZ',\n passingScore: '70',\n maxAttempts: '1',\n timeLimitMin: '0',\n isGraded: true,\n});\n\nexport const AssessmentsPage: FC = () => {\n const { navigateTo } = useBuildmyiq();\n\n const [courseId, setCourseId] = useState<string>('');\n const [status, setStatus] = useState<StatusFilter>('ALL');\n const [dialogOpen, setDialogOpen] = useState(false);\n const [form, setForm] = useState<AssessmentFormState>(makeEmptyForm);\n const [formError, setFormError] = useState<string | null>(null);\n\n const coursesQuery = useBuiCourses({ limit: 100 });\n // Fetch the full (unfiltered) list so the tab counts stay accurate; filter client-side.\n const assessmentsQuery = useBuiAssessments(courseId || undefined);\n const createAssessment = useBuiCreateAssessment();\n\n const courses = coursesQuery.data?.items ?? [];\n const assessments = useMemo(() => assessmentsQuery.data ?? [], [assessmentsQuery.data]);\n\n const statusCounts = useMemo(() => {\n const counts: Record<StatusFilter, number> = {\n ALL: assessments.length,\n DRAFT: 0,\n PUBLISHED: 0,\n ARCHIVED: 0,\n };\n for (const a of assessments) counts[a.status] += 1;\n return counts;\n }, [assessments]);\n\n const rows = useMemo(\n () => (status === 'ALL' ? assessments : assessments.filter((a) => a.status === status)),\n [assessments, status]\n );\n\n const totalQuestions = useMemo(\n () => assessments.reduce((sum, a) => sum + (a.questions?.length ?? 0), 0),\n [assessments]\n );\n\n const tabOptions: FilterOption<StatusFilter>[] = STATUS_TABS.map((t) => ({\n value: t.value,\n label: t.label,\n count: statusCounts[t.value],\n }));\n\n const columns: Column<BuiAssessment>[] = [\n {\n key: 'title',\n header: 'Assessment',\n render: (a) => (\n <div className=\"min-w-0\">\n <p className=\"font-semibold text-text-primary\">{a.title}</p>\n {a.description && (\n <p\n className=\"mt-0.5 max-w-md truncate text-xs text-text-secondary\"\n title={a.description}\n >\n {a.description}\n </p>\n )}\n </div>\n ),\n },\n {\n key: 'type',\n header: 'Type',\n render: (a) => <StatusBadge status={a.type} />,\n },\n {\n key: 'status',\n header: 'Status',\n sortValue: (a) => a.status,\n render: (a) => <StatusBadge status={a.status} />,\n },\n {\n key: 'passingScore',\n header: 'Pass',\n align: 'right',\n hideOnMobile: true,\n sortValue: (a) => a.passingScore,\n render: (a) => <span className=\"tabular-nums text-text-secondary\">{a.passingScore}%</span>,\n },\n {\n key: 'questions',\n header: 'Questions',\n align: 'right',\n hideOnMobile: true,\n render: (a) => (\n <span className=\"tabular-nums text-text-secondary\">{a.questions?.length ?? '—'}</span>\n ),\n },\n {\n key: 'updatedAt',\n header: 'Updated',\n hideOnMobile: true,\n sortValue: (a) => a.updatedAt,\n render: (a) => (\n <span className=\"whitespace-nowrap text-text-secondary\">\n {formatRelativeTime(a.updatedAt)}\n </span>\n ),\n },\n ];\n\n const openDialog = () => {\n setForm(makeEmptyForm());\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = () => {\n setDialogOpen(false);\n setForm(makeEmptyForm());\n setFormError(null);\n };\n\n const handleCreate = async () => {\n if (!courseId) {\n setFormError('Pick a course first.');\n return;\n }\n if (!form.title.trim()) {\n setFormError('Title is required.');\n return;\n }\n try {\n const created = await createAssessment.mutateAsync({\n courseId,\n title: form.title.trim(),\n description: form.description.trim() || undefined,\n type: form.type,\n passingScore: form.passingScore ? Number(form.passingScore) : undefined,\n maxAttempts: form.maxAttempts ? Number(form.maxAttempts) : undefined,\n timeLimitMin: form.timeLimitMin ? Number(form.timeLimitMin) : undefined,\n isGraded: form.isGraded,\n });\n closeDialog();\n navigateTo(`assessments/${created.id}`);\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to create assessment.');\n }\n };\n\n return (\n <PageLayout\n title=\"Assessments\"\n description=\"Quizzes, exams and graded assignments for every course in your workspace.\"\n titleGradient\n aurora\n actions={\n <Button onClick={openDialog} disabled={!courseId}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New assessment\n </Button>\n }\n >\n <GlassCard treatment=\"plain\" className=\"mb-5 p-4\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end\">\n <div className=\"grid gap-1.5\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Course\n </span>\n <Select\n value={courseId}\n onValueChange={(v) => {\n setCourseId(v);\n setStatus('ALL');\n }}\n >\n <SelectTrigger className=\"w-full sm:w-72\">\n <SelectValue placeholder=\"Select a course\" />\n </SelectTrigger>\n <SelectContent>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {coursesQuery.isSuccess && courses.length === 0 && (\n <p className=\"text-xs text-text-secondary\">\n No courses yet — create one in Authoring first.\n </p>\n )}\n </div>\n {courseId && (\n <div className=\"grid gap-1.5\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Status\n </span>\n <FilterTabs options={tabOptions} value={status} onChange={setStatus} />\n </div>\n )}\n </div>\n </GlassCard>\n\n {courseId && assessmentsQuery.isSuccess && assessments.length > 0 && (\n <div className=\"mb-5 grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <MetricsCard\n icon={<ClipboardList className=\"h-5 w-5\" />}\n value={statusCounts.ALL}\n label=\"Assessments\"\n />\n <MetricsCard\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n value={statusCounts.PUBLISHED}\n label=\"Published\"\n accent=\"success\"\n />\n <MetricsCard\n icon={<FileEdit className=\"h-5 w-5\" />}\n value={statusCounts.DRAFT}\n label=\"Drafts\"\n />\n <MetricsCard\n icon={<ListChecks className=\"h-5 w-5\" />}\n value={totalQuestions}\n label=\"Questions\"\n />\n </div>\n )}\n\n {!courseId ? (\n <EmptyState\n illustration=\"onboarding\"\n icon={<ClipboardList className=\"h-10 w-10\" />}\n title=\"Pick a course to see its assessments\"\n description=\"Assessments are scoped to a course. Choose a course above to view, filter and create its quizzes and exams.\"\n />\n ) : assessmentsQuery.isLoading ? (\n <LoadingView label=\"Loading assessments…\" />\n ) : assessmentsQuery.isError ? (\n <ErrorView\n message=\"Couldn't load assessments for this course.\"\n onRetry={() => assessmentsQuery.refetch()}\n />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<ClipboardList className=\"h-10 w-10\" />}\n title={status === 'ALL' ? 'No assessments yet' : 'No assessments match this filter'}\n description={\n status === 'ALL'\n ? 'Create the first quiz, exam or assignment for this course.'\n : 'Try a different status filter, or create a new assessment.'\n }\n action={\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New assessment\n </Button>\n }\n />\n ) : (\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(a) => a.id}\n onRowClick={(a) => navigateTo(`assessments/${a.id}`)}\n defaultSort={{ key: 'updatedAt', dir: 'desc' }}\n />\n )}\n\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New assessment</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Add a quiz, exam or assignment to this course. You can add questions after it&apos;s\n created.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentTitle\">Title *</Label>\n <Input\n id=\"assessmentTitle\"\n value={form.title}\n onChange={(e) => setForm({ ...form, title: e.target.value })}\n placeholder=\"e.g. Module 1 knowledge check\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentDescription\">Description</Label>\n <Textarea\n id=\"assessmentDescription\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n placeholder=\"What this assessment covers and how it's graded.\"\n rows={2}\n />\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentType\">Type</Label>\n <Select\n value={form.type}\n onValueChange={(v) => setForm({ ...form, type: v as BuiAssessmentType })}\n >\n <SelectTrigger id=\"assessmentType\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ASSESSMENT_TYPES.map((t) => (\n <SelectItem key={t} value={t}>\n {formatEnumLabel(t)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"passingScore\">Passing score (%)</Label>\n <Input\n id=\"passingScore\"\n type=\"number\"\n min=\"0\"\n max=\"100\"\n step=\"1\"\n value={form.passingScore}\n onChange={(e) => setForm({ ...form, passingScore: e.target.value })}\n placeholder=\"70\"\n />\n </div>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"maxAttempts\">Max attempts</Label>\n <Input\n id=\"maxAttempts\"\n type=\"number\"\n min=\"1\"\n step=\"1\"\n value={form.maxAttempts}\n onChange={(e) => setForm({ ...form, maxAttempts: e.target.value })}\n placeholder=\"1\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"timeLimitMin\">Time limit (minutes)</Label>\n <Input\n id=\"timeLimitMin\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={form.timeLimitMin}\n onChange={(e) => setForm({ ...form, timeLimitMin: e.target.value })}\n placeholder=\"0 = no limit\"\n />\n </div>\n </div>\n\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface px-3 py-2.5\">\n <div className=\"min-w-0\">\n <Label htmlFor=\"isGraded\" className=\"cursor-pointer\">\n Graded\n </Label>\n <p className=\"text-xs text-text-secondary\">\n Contributes to the learner&apos;s score and pass/fail outcome.\n </p>\n </div>\n <Switch\n id=\"isGraded\"\n checked={form.isGraded}\n onCheckedChange={(checked) => setForm({ ...form, isGraded: checked })}\n />\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button onClick={handleCreate} disabled={createAssessment.isPending}>\n {createAssessment.isPending ? 'Creating…' : 'Create assessment'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TakeAssessmentPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/TakeAssessmentPage.tsx"],"sourcesContent":["/**\n * TakeAssessmentPage — the LEARNER-facing surface to attempt a published\n * assessment and submit answers.\n * @module buildmyiq/pages/assessments\n *\n * Loads the assessment + its questions, collects an answer per question (typed\n * to the question kind), resolves the caller's enrollment for the assessment's\n * course, and submits via `buiSubmitAssessment` ({ assessmentId, enrollmentId?,\n * answers }). Grading is done by an educator in the Grading queue — this page is\n * honest about that (\"submitted, awaiting grading\"). All data is live.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n CheckCircle2,\n ClipboardCheck,\n Clock,\n FileText,\n RefreshCw,\n Send,\n Target,\n} from 'lucide-react';\nimport { Button, GlassCard, Input, Textarea } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n ProgressRing,\n LoadingView,\n ErrorView,\n EmptyState,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiAssessment, useBuiEnrollments, useBuiSubmitAssessment } from '../../hooks';\nimport { cn } from '../../utils/cn';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport type { BuiQuestion } from '../../types';\n\ninterface Choice {\n label: string;\n}\n\n/** Narrow the untyped `options` JSON blob to a list of `{ label }` choices. */\nfunction parseChoices(options: unknown): Choice[] {\n if (!Array.isArray(options)) return [];\n return options\n .map((o) =>\n o && typeof o === 'object' && 'label' in o\n ? { label: String((o as { label: unknown }).label) }\n : null\n )\n .filter((c): c is Choice => c !== null && c.label.length > 0);\n}\n\ntype AnswerValue = string | string[];\n\nconst TRUE_FALSE_CHOICES: Choice[] = [{ label: 'True' }, { label: 'False' }];\n\ninterface MetaCellProps {\n icon: ReactNode;\n label: string;\n value: ReactNode;\n}\n\nconst MetaCell: FC<MetaCellProps> = ({ icon, label, value }) => (\n <div className=\"flex items-center gap-3 py-3 first:pt-0 last:pb-0 sm:px-5 sm:py-0 sm:first:pl-0 sm:last:pr-0\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n {icon}\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-xs text-text-secondary\">{label}</p>\n <p className=\"text-sm font-semibold text-text-primary\">{value}</p>\n </div>\n </div>\n);\n\nexport const TakeAssessmentPage: FC = () => {\n const { assessmentId } = useParams<{ assessmentId: string }>();\n const { currentUser, navigateTo } = useBuildmyiq();\n\n const assessmentQuery = useBuiAssessment(assessmentId);\n const assessment = assessmentQuery.data ?? null;\n\n const enrollmentsQuery = useBuiEnrollments({\n learnerId: currentUser?.id,\n courseId: assessment?.courseId,\n enabled: Boolean(currentUser?.id && assessment?.courseId),\n });\n const enrollmentId = enrollmentsQuery.data?.items?.[0]?.id;\n\n const submit = useBuiSubmitAssessment();\n const [answers, setAnswers] = useState<Record<string, AnswerValue>>({});\n const [submittedId, setSubmittedId] = useState<string | null>(null);\n\n const questions = useMemo<BuiQuestion[]>(\n () => [...(assessment?.questions ?? [])].sort((a, b) => a.position - b.position),\n [assessment]\n );\n\n const setAnswer = (qid: string, value: AnswerValue): void => {\n setAnswers((prev) => ({ ...prev, [qid]: value }));\n };\n\n const toggleMulti = (qid: string, label: string): void => {\n setAnswers((prev) => {\n const cur = Array.isArray(prev[qid]) ? (prev[qid] as string[]) : [];\n return {\n ...prev,\n [qid]: cur.includes(label) ? cur.filter((l) => l !== label) : [...cur, label],\n };\n });\n };\n\n const isAnswered = (q: BuiQuestion): boolean => {\n const a = answers[q.id];\n return Array.isArray(a) ? a.length > 0 : Boolean(a && String(a).trim());\n };\n\n const answeredCount = questions.filter(isAnswered).length;\n const answeredPct = questions.length > 0 ? (answeredCount / questions.length) * 100 : 0;\n const remaining = questions.length - answeredCount;\n const encouragement =\n answeredCount === 0\n ? 'Answer a question to get started.'\n : remaining === 0\n ? 'All questions answered — ready to submit.'\n : `${remaining} question${remaining === 1 ? '' : 's'} left to answer.`;\n\n const handleSubmit = async (): Promise<void> => {\n if (!assessmentId) return;\n const result = await submit.mutateAsync({\n assessmentId,\n enrollmentId,\n attempt: 1,\n answers,\n });\n setSubmittedId(result.id);\n };\n\n const backToCourse = assessment ? (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo(`catalog/${assessment.courseId}`)}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" /> Back to course\n </Button>\n ) : undefined;\n\n if (!assessmentId) {\n return (\n <PageLayout title=\"Assessment\">\n <EmptyState illustration=\"empty-data\" title=\"No assessment selected\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isLoading) {\n return (\n <PageLayout title=\"Assessment\">\n <LoadingView label=\"Loading the assessment…\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isError || !assessment) {\n return (\n <PageLayout title=\"Assessment\" actions={backToCourse}>\n <ErrorView\n title=\"Couldn't load this assessment\"\n message={\n assessmentQuery.error instanceof Error ? assessmentQuery.error.message : undefined\n }\n onRetry={() => assessmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (assessment.status !== 'PUBLISHED') {\n return (\n <PageLayout title={assessment.title} actions={backToCourse}>\n <EmptyState\n illustration=\"empty-data\"\n title=\"This assessment isn't open yet\"\n description=\"It has to be published by an educator before you can take it.\"\n />\n </PageLayout>\n );\n }\n\n // Submitted confirmation\n if (submittedId) {\n return (\n <PageLayout title={assessment.title} actions={backToCourse}>\n <div className=\"mx-auto max-w-lg\">\n <GlassCard\n treatment=\"glass\"\n glow\n className=\"flex flex-col items-center gap-4 p-8 text-center\"\n >\n <div className=\"flex h-14 w-14 items-center justify-center rounded-full bg-status-success-bg-subtle\">\n <CheckCircle2 className=\"h-7 w-7 text-status-success-text\" />\n </div>\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Submitted</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Your answers for “{assessment.title}” were submitted.\n {assessment.isGraded\n ? ' An educator will grade them — you’ll see your score and feedback in My Learning.'\n : ' Thanks for completing it.'}\n </p>\n </div>\n <div className=\"flex flex-wrap justify-center gap-2\">\n <Button variant=\"secondary\" onClick={() => navigateTo('learning')}>\n Go to My Learning\n </Button>\n <Button variant=\"ghost\" onClick={() => navigateTo(`catalog/${assessment.courseId}`)}>\n Back to course\n </Button>\n </div>\n </GlassCard>\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={assessment.title}\n description={assessment.description ?? undefined}\n actions={backToCourse}\n >\n <div className=\"mx-auto max-w-3xl pb-4\">\n {/* Meta strip — type · passing score · time limit · attempts · answered */}\n <GlassCard treatment=\"glass\" className=\"mb-6 p-4 sm:px-5\">\n <div className=\"flex flex-col divide-y divide-border-seam sm:flex-row sm:flex-wrap sm:items-center sm:divide-x sm:divide-y-0\">\n <MetaCell\n icon={<FileText className=\"h-4 w-4\" />}\n label=\"Type\"\n value={<StatusBadge status={assessment.type} />}\n />\n <MetaCell\n icon={<Target className=\"h-4 w-4\" />}\n label=\"Passing score\"\n value={`${assessment.passingScore}%`}\n />\n {assessment.timeLimitMin > 0 && (\n <MetaCell\n icon={<Clock className=\"h-4 w-4\" />}\n label=\"Time limit\"\n value={formatDuration(assessment.timeLimitMin)}\n />\n )}\n {assessment.maxAttempts > 0 && (\n <MetaCell\n icon={<RefreshCw className=\"h-4 w-4\" />}\n label=\"Attempts\"\n value={assessment.maxAttempts}\n />\n )}\n <div className=\"flex items-center gap-3 py-3 sm:ml-auto sm:py-0 sm:pl-5\">\n <ProgressRing\n value={answeredPct}\n size={44}\n stroke={5}\n label={`${answeredCount}/${questions.length}`}\n />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-semibold text-text-primary tabular-nums\">\n {answeredCount} of {questions.length}\n </p>\n <p className=\"text-xs text-text-secondary\">answered</p>\n </div>\n </div>\n </div>\n </GlassCard>\n\n {questions.length === 0 ? (\n <EmptyState\n icon={<ClipboardCheck className=\"h-8 w-8\" />}\n title=\"No questions yet\"\n description=\"This assessment doesn't have any questions to answer.\"\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n {questions.map((q, idx) => {\n const choices =\n q.type === 'TRUE_FALSE' ? TRUE_FALSE_CHOICES : parseChoices(q.options);\n const answer = answers[q.id];\n const answered = isAnswered(q);\n return (\n <GlassCard\n key={q.id}\n treatment={answered ? 'insight' : 'plain'}\n className={cn(\n 'p-5 transition-shadow',\n answered && 'ring-1 ring-[var(--color-accent-soft)]'\n )}\n >\n <div className=\"flex items-start gap-3 sm:gap-4\">\n <span\n className={cn(\n 'mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm font-semibold tabular-nums transition-colors',\n answered\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-[var(--color-accent-soft)] text-text-primary'\n )}\n >\n {answered ? <CheckCircle2 className=\"h-4 w-4\" /> : idx + 1}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"rounded-md bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n {formatEnumLabel(q.type)}\n </span>\n <span className=\"ml-auto text-xs text-text-secondary tabular-nums\">\n {q.points} {q.points === 1 ? 'point' : 'points'}\n </span>\n </div>\n <p className=\"mt-2.5 font-medium text-text-primary\">{q.prompt}</p>\n\n <div className=\"mt-4\">\n {(q.type === 'SINGLE_CHOICE' || q.type === 'TRUE_FALSE') && (\n <div className=\"flex flex-col gap-2\">\n {choices.map((c) => (\n <label\n key={c.label}\n className={cn(\n 'flex min-h-[44px] cursor-pointer items-center gap-3 rounded-xl border px-3.5 py-2.5 text-sm transition-colors',\n answer === c.label\n ? 'border-[var(--color-accent-teal)] bg-[var(--color-accent-soft)] text-text-primary'\n : 'border-border-seam text-text-secondary hover:border-border-strong hover:bg-bg-sunken'\n )}\n >\n <input\n type=\"radio\"\n name={q.id}\n className=\"h-4 w-4 accent-[var(--color-accent-teal)]\"\n checked={answer === c.label}\n onChange={() => setAnswer(q.id, c.label)}\n />\n {c.label}\n </label>\n ))}\n </div>\n )}\n\n {q.type === 'MULTIPLE_CHOICE' && (\n <div className=\"flex flex-col gap-2\">\n {choices.map((c) => {\n const checked = Array.isArray(answer) && answer.includes(c.label);\n return (\n <label\n key={c.label}\n className={cn(\n 'flex min-h-[44px] cursor-pointer items-center gap-3 rounded-xl border px-3.5 py-2.5 text-sm transition-colors',\n checked\n ? 'border-[var(--color-accent-teal)] bg-[var(--color-accent-soft)] text-text-primary'\n : 'border-border-seam text-text-secondary hover:border-border-strong hover:bg-bg-sunken'\n )}\n >\n <input\n type=\"checkbox\"\n className=\"h-4 w-4 rounded accent-[var(--color-accent-teal)]\"\n checked={checked}\n onChange={() => toggleMulti(q.id, c.label)}\n />\n {c.label}\n </label>\n );\n })}\n </div>\n )}\n\n {q.type === 'SHORT_ANSWER' && (\n <Input\n value={typeof answer === 'string' ? answer : ''}\n onChange={(e) => setAnswer(q.id, e.target.value)}\n placeholder=\"Type your answer here…\"\n />\n )}\n\n {(q.type === 'ESSAY' || q.type === 'CODE') && (\n <Textarea\n rows={q.type === 'CODE' ? 6 : 4}\n value={typeof answer === 'string' ? answer : ''}\n onChange={(e) => setAnswer(q.id, e.target.value)}\n placeholder={\n q.type === 'CODE' ? 'Write your code…' : 'Write your answer…'\n }\n className={q.type === 'CODE' ? 'font-mono text-sm' : undefined}\n />\n )}\n </div>\n </div>\n </div>\n </GlassCard>\n );\n })}\n\n {submit.isError && (\n <p className=\"text-sm text-status-error-text\">\n {submit.error instanceof Error\n ? submit.error.message\n : 'Could not submit your answers.'}\n </p>\n )}\n\n {/* Sticky submit bar */}\n <div className=\"sticky bottom-0 z-10 pt-2\">\n <div className=\"flex flex-col gap-3 rounded-2xl border border-border-seam bg-bg-surface/90 p-4 shadow-elevation-2 backdrop-blur sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <ProgressRing\n value={answeredPct}\n size={44}\n stroke={5}\n label={`${answeredCount}/${questions.length}`}\n />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-semibold text-text-primary tabular-nums\">\n {answeredCount} of {questions.length} answered\n </p>\n <p className=\"text-xs text-text-secondary\">{encouragement}</p>\n </div>\n </div>\n <Button\n size=\"lg\"\n className=\"max-sm:w-full\"\n onClick={handleSubmit}\n disabled={submit.isPending || answeredCount === 0}\n >\n <Send className=\"mr-1.5 h-4 w-4\" />\n {submit.isPending ? 'Submitting…' : 'Submit assessment'}\n </Button>\n </div>\n </div>\n </div>\n )}\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"TakeAssessmentPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/TakeAssessmentPage.tsx"],"sourcesContent":["/**\n * TakeAssessmentPage — the LEARNER-facing surface to attempt a published\n * assessment and submit answers.\n * @module buildmyiq/pages/assessments\n *\n * Loads the assessment + its questions, collects an answer per question (typed\n * to the question kind), resolves the caller's enrollment for the assessment's\n * course, and submits via `buiSubmitAssessment` ({ assessmentId, enrollmentId?,\n * answers }). Grading is done by an educator in the Grading queue — this page is\n * honest about that (\"submitted, awaiting grading\"). All data is live.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n CheckCircle2,\n ClipboardCheck,\n Clock,\n FileText,\n RefreshCw,\n Send,\n Target,\n} from 'lucide-react';\nimport { Button, GlassCard, Input, Textarea } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n ProgressRing,\n LoadingView,\n ErrorView,\n EmptyState,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiAssessment, useBuiEnrollments, useBuiSubmitAssessment } from '../../hooks';\nimport { cn } from '../../utils/cn';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport type { BuiQuestion } from '../../types';\n\ninterface Choice {\n label: string;\n}\n\n/** Narrow the untyped `options` JSON blob to a list of `{ label }` choices. */\nfunction parseChoices(options: unknown): Choice[] {\n if (!Array.isArray(options)) return [];\n return options\n .map((o) =>\n o && typeof o === 'object' && 'label' in o\n ? { label: String((o as { label: unknown }).label) }\n : null\n )\n .filter((c): c is Choice => c !== null && c.label.length > 0);\n}\n\ntype AnswerValue = string | string[];\n\nconst TRUE_FALSE_CHOICES: Choice[] = [{ label: 'True' }, { label: 'False' }];\n\ninterface MetaCellProps {\n icon: ReactNode;\n label: string;\n value: ReactNode;\n}\n\nconst MetaCell: FC<MetaCellProps> = ({ icon, label, value }) => (\n <div className=\"flex items-center gap-3 py-3 first:pt-0 last:pb-0 sm:px-5 sm:py-0 sm:first:pl-0 sm:last:pr-0\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n {icon}\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-xs text-text-secondary\">{label}</p>\n <p className=\"text-sm font-semibold text-text-primary\">{value}</p>\n </div>\n </div>\n);\n\nexport const TakeAssessmentPage: FC = () => {\n const { assessmentId } = useParams<{ assessmentId: string }>();\n const { currentUser, navigateTo } = useBuildmyiq();\n\n const assessmentQuery = useBuiAssessment(assessmentId);\n const assessment = assessmentQuery.data ?? null;\n\n const enrollmentsQuery = useBuiEnrollments({\n learnerId: currentUser?.id,\n courseId: assessment?.courseId,\n enabled: Boolean(currentUser?.id && assessment?.courseId),\n });\n const enrollmentId = enrollmentsQuery.data?.items?.[0]?.id;\n\n const submit = useBuiSubmitAssessment();\n const [answers, setAnswers] = useState<Record<string, AnswerValue>>({});\n const [submittedId, setSubmittedId] = useState<string | null>(null);\n\n const questions = useMemo<BuiQuestion[]>(\n () => [...(assessment?.questions ?? [])].sort((a, b) => a.position - b.position),\n [assessment]\n );\n\n const setAnswer = (qid: string, value: AnswerValue): void => {\n setAnswers((prev) => ({ ...prev, [qid]: value }));\n };\n\n const toggleMulti = (qid: string, label: string): void => {\n setAnswers((prev) => {\n const cur = Array.isArray(prev[qid]) ? (prev[qid] as string[]) : [];\n return {\n ...prev,\n [qid]: cur.includes(label) ? cur.filter((l) => l !== label) : [...cur, label],\n };\n });\n };\n\n const isAnswered = (q: BuiQuestion): boolean => {\n const a = answers[q.id];\n return Array.isArray(a) ? a.length > 0 : Boolean(a && String(a).trim());\n };\n\n const answeredCount = questions.filter(isAnswered).length;\n const answeredPct = questions.length > 0 ? (answeredCount / questions.length) * 100 : 0;\n const remaining = questions.length - answeredCount;\n const encouragement =\n answeredCount === 0\n ? 'Answer a question to get started.'\n : remaining === 0\n ? 'All questions answered — ready to submit.'\n : `${remaining} question${remaining === 1 ? '' : 's'} left to answer.`;\n\n const handleSubmit = async (): Promise<void> => {\n if (!assessmentId) return;\n const result = await submit.mutateAsync({\n assessmentId,\n enrollmentId,\n attempt: 1,\n answers,\n });\n setSubmittedId(result.id);\n };\n\n const backToCourse = assessment ? (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo(`catalog/${assessment.courseId}`)}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" /> Back to course\n </Button>\n ) : undefined;\n\n if (!assessmentId) {\n return (\n <PageLayout title=\"Assessment\">\n <EmptyState illustration=\"empty-data\" title=\"No assessment selected\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isLoading) {\n return (\n <PageLayout title=\"Assessment\">\n <LoadingView label=\"Loading the assessment…\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isError || !assessment) {\n return (\n <PageLayout title=\"Assessment\" actions={backToCourse}>\n <ErrorView\n title=\"Couldn't load this assessment\"\n message={\n assessmentQuery.error instanceof Error ? assessmentQuery.error.message : undefined\n }\n onRetry={() => assessmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (assessment.status !== 'PUBLISHED') {\n return (\n <PageLayout title={assessment.title} actions={backToCourse}>\n <EmptyState\n illustration=\"empty-data\"\n title=\"This assessment isn't open yet\"\n description=\"It has to be published by an educator before you can take it.\"\n />\n </PageLayout>\n );\n }\n\n // Submitted confirmation\n if (submittedId) {\n return (\n <PageLayout title={assessment.title} actions={backToCourse}>\n <div className=\"mx-auto max-w-lg\">\n <GlassCard\n treatment=\"glass\"\n glow\n className=\"flex flex-col items-center gap-4 p-8 text-center\"\n >\n <div className=\"flex h-14 w-14 items-center justify-center rounded-full bg-status-success-bg-subtle\">\n <CheckCircle2 className=\"h-7 w-7 text-status-success-text\" />\n </div>\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Submitted</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Your answers for “{assessment.title}” were submitted.\n {assessment.isGraded\n ? ' An educator will grade them — you’ll see your score and feedback in My Learning.'\n : ' Thanks for completing it.'}\n </p>\n </div>\n <div className=\"flex flex-wrap justify-center gap-2\">\n <Button variant=\"secondary\" onClick={() => navigateTo('learning')}>\n Go to My Learning\n </Button>\n <Button variant=\"ghost\" onClick={() => navigateTo(`catalog/${assessment.courseId}`)}>\n Back to course\n </Button>\n </div>\n </GlassCard>\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={assessment.title}\n description={assessment.description ?? undefined}\n actions={backToCourse}\n >\n <div className=\"mx-auto max-w-3xl pb-4\">\n {/* Meta strip — type · passing score · time limit · attempts · answered */}\n <GlassCard treatment=\"glass\" className=\"mb-6 p-4 sm:px-5\">\n <div className=\"flex flex-col divide-y divide-border-seam sm:flex-row sm:flex-wrap sm:items-center sm:divide-x sm:divide-y-0\">\n <MetaCell\n icon={<FileText className=\"h-4 w-4\" />}\n label=\"Type\"\n value={<StatusBadge status={assessment.type} />}\n />\n <MetaCell\n icon={<Target className=\"h-4 w-4\" />}\n label=\"Passing score\"\n value={`${assessment.passingScore}%`}\n />\n {assessment.timeLimitMin > 0 && (\n <MetaCell\n icon={<Clock className=\"h-4 w-4\" />}\n label=\"Time limit\"\n value={formatDuration(assessment.timeLimitMin)}\n />\n )}\n {assessment.maxAttempts > 0 && (\n <MetaCell\n icon={<RefreshCw className=\"h-4 w-4\" />}\n label=\"Attempts\"\n value={assessment.maxAttempts}\n />\n )}\n <div className=\"flex items-center gap-3 py-3 sm:ml-auto sm:py-0 sm:pl-5\">\n <ProgressRing\n value={answeredPct}\n size={44}\n stroke={5}\n label={`${answeredCount}/${questions.length}`}\n />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-semibold text-text-primary tabular-nums\">\n {answeredCount} of {questions.length}\n </p>\n <p className=\"text-xs text-text-secondary\">answered</p>\n </div>\n </div>\n </div>\n </GlassCard>\n\n {questions.length === 0 ? (\n <EmptyState\n icon={<ClipboardCheck className=\"h-8 w-8\" />}\n title=\"No questions yet\"\n description=\"This assessment doesn't have any questions to answer.\"\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n {questions.map((q, idx) => {\n const choices =\n q.type === 'TRUE_FALSE' ? TRUE_FALSE_CHOICES : parseChoices(q.options);\n const answer = answers[q.id];\n const answered = isAnswered(q);\n return (\n <GlassCard\n key={q.id}\n treatment={answered ? 'insight' : 'plain'}\n className={cn(\n 'p-5 transition-shadow',\n answered && 'ring-1 ring-[var(--color-accent-soft)]'\n )}\n >\n <div className=\"flex items-start gap-3 sm:gap-4\">\n <span\n className={cn(\n 'mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm font-semibold tabular-nums transition-colors',\n answered\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-[var(--color-accent-soft)] text-text-primary'\n )}\n >\n {answered ? <CheckCircle2 className=\"h-4 w-4\" /> : idx + 1}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"rounded-md bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n {formatEnumLabel(q.type)}\n </span>\n <span className=\"ml-auto text-xs text-text-secondary tabular-nums\">\n {q.points} {q.points === 1 ? 'point' : 'points'}\n </span>\n </div>\n <p className=\"mt-2.5 font-medium text-text-primary\">{q.prompt}</p>\n\n <div className=\"mt-4\">\n {(q.type === 'SINGLE_CHOICE' || q.type === 'TRUE_FALSE') && (\n <div className=\"flex flex-col gap-2\">\n {choices.map((c) => (\n <label\n key={c.label}\n className={cn(\n 'flex min-h-[44px] cursor-pointer items-center gap-3 rounded-xl border px-3.5 py-2.5 text-sm transition-colors',\n answer === c.label\n ? 'border-[var(--color-accent-teal)] bg-[var(--color-accent-soft)] text-text-primary'\n : 'border-border-seam text-text-secondary hover:border-border-strong hover:bg-bg-sunken'\n )}\n >\n <input\n type=\"radio\"\n name={q.id}\n className=\"h-4 w-4 accent-[var(--color-accent-teal)]\"\n checked={answer === c.label}\n onChange={() => setAnswer(q.id, c.label)}\n />\n {c.label}\n </label>\n ))}\n </div>\n )}\n\n {q.type === 'MULTIPLE_CHOICE' && (\n <div className=\"flex flex-col gap-2\">\n {choices.map((c) => {\n const checked = Array.isArray(answer) && answer.includes(c.label);\n return (\n <label\n key={c.label}\n className={cn(\n 'flex min-h-[44px] cursor-pointer items-center gap-3 rounded-xl border px-3.5 py-2.5 text-sm transition-colors',\n checked\n ? 'border-[var(--color-accent-teal)] bg-[var(--color-accent-soft)] text-text-primary'\n : 'border-border-seam text-text-secondary hover:border-border-strong hover:bg-bg-sunken'\n )}\n >\n <input\n type=\"checkbox\"\n className=\"h-4 w-4 rounded accent-[var(--color-accent-teal)]\"\n checked={checked}\n onChange={() => toggleMulti(q.id, c.label)}\n />\n {c.label}\n </label>\n );\n })}\n </div>\n )}\n\n {q.type === 'SHORT_ANSWER' && (\n <Input\n value={typeof answer === 'string' ? answer : ''}\n onChange={(e) => setAnswer(q.id, e.target.value)}\n placeholder=\"Type your answer here…\"\n />\n )}\n\n {(q.type === 'ESSAY' || q.type === 'CODE') && (\n <Textarea\n rows={q.type === 'CODE' ? 6 : 4}\n value={typeof answer === 'string' ? answer : ''}\n onChange={(e) => setAnswer(q.id, e.target.value)}\n placeholder={\n q.type === 'CODE' ? 'Write your code…' : 'Write your answer…'\n }\n className={q.type === 'CODE' ? 'font-mono text-sm' : undefined}\n />\n )}\n </div>\n </div>\n </div>\n </GlassCard>\n );\n })}\n\n {submit.isError && (\n <p className=\"text-sm text-status-error-text\">\n {submit.error instanceof Error\n ? submit.error.message\n : 'Could not submit your answers.'}\n </p>\n )}\n\n {/* Sticky submit bar */}\n <div className=\"sticky bottom-0 z-10 pt-2\">\n <div className=\"flex flex-col gap-3 rounded-2xl border border-border-seam bg-bg-surface/90 p-4 shadow-elevation-2 backdrop-blur sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <ProgressRing\n value={answeredPct}\n size={44}\n stroke={5}\n label={`${answeredCount}/${questions.length}`}\n />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-semibold text-text-primary tabular-nums\">\n {answeredCount} of {questions.length} answered\n </p>\n <p className=\"text-xs text-text-secondary\">{encouragement}</p>\n </div>\n </div>\n <Button\n size=\"lg\"\n className=\"max-sm:w-full\"\n onClick={handleSubmit}\n disabled={submit.isPending || answeredCount === 0}\n >\n <Send className=\"mr-1.5 h-4 w-4\" />\n {submit.isPending ? 'Submitting…' : 'Submit assessment'}\n </Button>\n </div>\n </div>\n </div>\n )}\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"AuthoringPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/authoring/AuthoringPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAmLhC,eAAO,MAAM,aAAa,EAAE,EA0H3B,CAAC"}
1
+ {"version":3,"file":"AuthoringPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/authoring/AuthoringPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAoLhC,eAAO,MAAM,aAAa,EAAE,EAkI3B,CAAC"}