@burdenoff/microfe-buildmyiq 2026.1025.2 → 2026.1025.3

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.
@@ -1,5 +1,18 @@
1
1
  import { FC } from 'react';
2
+ /** The question fields AnswersView needs to label an answer with its prompt. */
3
+ export interface AnswerQuestion {
4
+ id: string;
5
+ prompt: string;
6
+ position: number;
7
+ }
8
+ /**
9
+ * Answers are keyed by question id. When the assessment's questions are known,
10
+ * each answer is labelled with its question prompt and listed in question
11
+ * order; keys that match no question (or every key, while the questions are
12
+ * still loading) fall back to the raw key.
13
+ */
2
14
  export declare const AnswersView: FC<{
3
15
  answers: unknown;
16
+ questions?: readonly AnswerQuestion[];
4
17
  }>;
5
18
  //# sourceMappingURL=AnswersView.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AnswersView.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/submissions/AnswersView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAahC,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAmChD,CAAC"}
1
+ {"version":3,"file":"AnswersView.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/submissions/AnswersView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAahC,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;CAAE,CAiDvF,CAAC"}
@@ -1,33 +1,37 @@
1
1
  import { jsx as e, jsxs as t } from "react/jsx-runtime";
2
2
  //#region src/buildmyiq/pages/submissions/AnswersView.tsx
3
- var n = (e) => typeof e == "object" && !!e && !Array.isArray(e), r = (e) => e == null ? "—" : typeof e == "string" ? e : typeof e == "number" || typeof e == "boolean" ? String(e) : JSON.stringify(e, null, 2), i = ({ answers: i }) => {
3
+ var n = (e) => typeof e == "object" && !!e && !Array.isArray(e), r = (e) => e == null ? "—" : typeof e == "string" ? e : typeof e == "number" || typeof e == "boolean" ? String(e) : JSON.stringify(e, null, 2), i = ({ answers: i, questions: a }) => {
4
4
  if (i == null) return /* @__PURE__ */ e("p", {
5
5
  className: "text-sm text-text-secondary",
6
6
  children: "No answers were submitted."
7
7
  });
8
8
  if (n(i)) {
9
- let n = Object.entries(i);
10
- return n.length === 0 ? /* @__PURE__ */ e("p", {
9
+ let n = new Map((a ?? []).map((e) => [e.id, e])), o = Object.entries(i).sort(([e], [t]) => (n.get(e)?.position ?? 2 ** 53 - 1) - (n.get(t)?.position ?? 2 ** 53 - 1));
10
+ return o.length === 0 ? /* @__PURE__ */ e("p", {
11
11
  className: "text-sm text-text-secondary",
12
12
  children: "No answers were submitted."
13
13
  }) : /* @__PURE__ */ e("ol", {
14
14
  className: "grid gap-2.5",
15
- children: n.map(([n, i], a) => /* @__PURE__ */ t("li", {
15
+ children: o.map(([i, a], o) => /* @__PURE__ */ t("li", {
16
16
  className: "grid gap-1 rounded-lg border border-border-seam/70 bg-bg-surface/60 p-3 sm:grid-cols-[minmax(0,7rem)_1fr] sm:gap-3",
17
17
  children: [/* @__PURE__ */ t("div", {
18
18
  className: "flex items-baseline gap-2 sm:col-span-1",
19
19
  children: [/* @__PURE__ */ t("span", {
20
20
  className: "shrink-0 text-xs font-semibold text-[var(--color-accent-teal)] tabular-nums",
21
- children: ["Q", a + 1]
22
- }), /* @__PURE__ */ e("span", {
21
+ children: ["Q", o + 1]
22
+ }), n.has(i) ? /* @__PURE__ */ e("span", {
23
+ className: "break-words text-xs text-text-secondary",
24
+ title: i,
25
+ children: n.get(i)?.prompt
26
+ }) : /* @__PURE__ */ e("span", {
23
27
  className: "break-words font-mono text-xs text-text-secondary",
24
- children: n
28
+ children: i
25
29
  })]
26
30
  }), /* @__PURE__ */ e("span", {
27
31
  className: "whitespace-pre-wrap break-words text-sm text-text-primary sm:col-span-1",
28
- children: r(i)
32
+ children: r(a)
29
33
  })]
30
- }, n))
34
+ }, i))
31
35
  });
32
36
  }
33
37
  return /* @__PURE__ */ e("pre", {
@@ -1 +1 @@
1
- {"version":3,"file":"AnswersView.js","names":[],"sources":["../../../../src/buildmyiq/pages/submissions/AnswersView.tsx"],"sourcesContent":["/**\n * AnswersView — read-only rendering of a submission's `answers` JSON blob.\n * @module buildmyiq/pages/submissions\n *\n * Shared by the one-at-a-time grading slide-over (SubmissionsPage) and the\n * SpeedGrader-like bulk grading queue (BulkGradingPage, BOFF-4429) so both\n * surfaces show the learner's answers exactly the same way. Values render\n * safely from whatever JSON shape the backend returns — never `dangerouslySetInnerHTML`.\n */\n\nimport type { FC } from 'react';\n\nconst isPlainRecord = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null && !Array.isArray(v);\n\n/** Render a single answer value safely, whatever its JSON shape. */\nconst formatAnswerValue = (v: unknown): string => {\n if (v == null) return '—';\n if (typeof v === 'string') return v;\n if (typeof v === 'number' || typeof v === 'boolean') return String(v);\n return JSON.stringify(v, null, 2);\n};\n\nexport const AnswersView: FC<{ answers: unknown }> = ({ answers }) => {\n if (answers == null) {\n return <p className=\"text-sm text-text-secondary\">No answers were submitted.</p>;\n }\n if (isPlainRecord(answers)) {\n const entries = Object.entries(answers);\n if (entries.length === 0) {\n return <p className=\"text-sm text-text-secondary\">No answers were submitted.</p>;\n }\n return (\n <ol className=\"grid gap-2.5\">\n {entries.map(([key, value], i) => (\n <li\n key={key}\n className=\"grid gap-1 rounded-lg border border-border-seam/70 bg-bg-surface/60 p-3 sm:grid-cols-[minmax(0,7rem)_1fr] sm:gap-3\"\n >\n <div className=\"flex items-baseline gap-2 sm:col-span-1\">\n <span className=\"shrink-0 text-xs font-semibold text-[var(--color-accent-teal)] tabular-nums\">\n Q{i + 1}\n </span>\n <span className=\"break-words font-mono text-xs text-text-secondary\">{key}</span>\n </div>\n <span className=\"whitespace-pre-wrap break-words text-sm text-text-primary sm:col-span-1\">\n {formatAnswerValue(value)}\n </span>\n </li>\n ))}\n </ol>\n );\n }\n return (\n <pre className=\"overflow-x-auto whitespace-pre-wrap break-words rounded-lg border border-border-seam/70 bg-bg-surface/60 p-3 text-xs text-text-primary\">\n {JSON.stringify(answers, null, 2)}\n </pre>\n );\n};\n"],"mappings":";;AAYA,IAAM,KAAiB,MACrB,OAAO,KAAM,cAAY,KAAc,CAAC,MAAM,QAAQ,CAAC,GAGnD,KAAqB,MACrB,KAAK,OAAa,MAClB,OAAO,KAAM,WAAiB,IAC9B,OAAO,KAAM,YAAY,OAAO,KAAM,YAAkB,OAAO,CAAC,IAC7D,KAAK,UAAU,GAAG,MAAM,CAAC,GAGrB,KAAyC,EAAE,iBAAc;CACpE,IAAI,KAAW,MACb,OAAO,kBAAC,KAAD;EAAG,WAAU;YAA8B;CAA6B,CAAA;CAEjF,IAAI,EAAc,CAAO,GAAG;EAC1B,IAAM,IAAU,OAAO,QAAQ,CAAO;EAItC,OAHI,EAAQ,WAAW,IACd,kBAAC,KAAD;GAAG,WAAU;aAA8B;EAA6B,CAAA,IAG/E,kBAAC,MAAD;GAAI,WAAU;aACX,EAAQ,KAAK,CAAC,GAAK,IAAQ,MAC1B,kBAAC,MAAD;IAEE,WAAU;cAFZ,CAIE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CAA8F,KAC1F,IAAI,CACF;SACN,kBAAC,QAAD;MAAM,WAAU;gBAAqD;KAAU,CAAA,CAC5E;QACL,kBAAC,QAAD;KAAM,WAAU;eACb,EAAkB,CAAK;IACpB,CAAA,CACJ;MAZG,CAYH,CACL;EACC,CAAA;CAER;CACA,OACE,kBAAC,OAAD;EAAK,WAAU;YACZ,KAAK,UAAU,GAAS,MAAM,CAAC;CAC7B,CAAA;AAET"}
1
+ {"version":3,"file":"AnswersView.js","names":[],"sources":["../../../../src/buildmyiq/pages/submissions/AnswersView.tsx"],"sourcesContent":["/**\n * AnswersView — read-only rendering of a submission's `answers` JSON blob.\n * @module buildmyiq/pages/submissions\n *\n * Shared by the one-at-a-time grading slide-over (SubmissionsPage) and the\n * SpeedGrader-like bulk grading queue (BulkGradingPage, BOFF-4429) so both\n * surfaces show the learner's answers exactly the same way. Values render\n * safely from whatever JSON shape the backend returns — never `dangerouslySetInnerHTML`.\n */\n\nimport type { FC } from 'react';\n\nconst isPlainRecord = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null && !Array.isArray(v);\n\n/** Render a single answer value safely, whatever its JSON shape. */\nconst formatAnswerValue = (v: unknown): string => {\n if (v == null) return '—';\n if (typeof v === 'string') return v;\n if (typeof v === 'number' || typeof v === 'boolean') return String(v);\n return JSON.stringify(v, null, 2);\n};\n\n/** The question fields AnswersView needs to label an answer with its prompt. */\nexport interface AnswerQuestion {\n id: string;\n prompt: string;\n position: number;\n}\n\n/**\n * Answers are keyed by question id. When the assessment's questions are known,\n * each answer is labelled with its question prompt and listed in question\n * order; keys that match no question (or every key, while the questions are\n * still loading) fall back to the raw key.\n */\nexport const AnswersView: FC<{ answers: unknown; questions?: readonly AnswerQuestion[] }> = ({\n answers,\n questions,\n}) => {\n if (answers == null) {\n return <p className=\"text-sm text-text-secondary\">No answers were submitted.</p>;\n }\n if (isPlainRecord(answers)) {\n const byId = new Map((questions ?? []).map((q) => [q.id, q]));\n const entries = Object.entries(answers).sort(([a], [b]) => {\n const pa = byId.get(a)?.position ?? Number.MAX_SAFE_INTEGER;\n const pb = byId.get(b)?.position ?? Number.MAX_SAFE_INTEGER;\n return pa - pb;\n });\n if (entries.length === 0) {\n return <p className=\"text-sm text-text-secondary\">No answers were submitted.</p>;\n }\n return (\n <ol className=\"grid gap-2.5\">\n {entries.map(([key, value], i) => (\n <li\n key={key}\n className=\"grid gap-1 rounded-lg border border-border-seam/70 bg-bg-surface/60 p-3 sm:grid-cols-[minmax(0,7rem)_1fr] sm:gap-3\"\n >\n <div className=\"flex items-baseline gap-2 sm:col-span-1\">\n <span className=\"shrink-0 text-xs font-semibold text-[var(--color-accent-teal)] tabular-nums\">\n Q{i + 1}\n </span>\n {byId.has(key) ? (\n <span className=\"break-words text-xs text-text-secondary\" title={key}>\n {byId.get(key)?.prompt}\n </span>\n ) : (\n <span className=\"break-words font-mono text-xs text-text-secondary\">{key}</span>\n )}\n </div>\n <span className=\"whitespace-pre-wrap break-words text-sm text-text-primary sm:col-span-1\">\n {formatAnswerValue(value)}\n </span>\n </li>\n ))}\n </ol>\n );\n }\n return (\n <pre className=\"overflow-x-auto whitespace-pre-wrap break-words rounded-lg border border-border-seam/70 bg-bg-surface/60 p-3 text-xs text-text-primary\">\n {JSON.stringify(answers, null, 2)}\n </pre>\n );\n};\n"],"mappings":";;AAYA,IAAM,KAAiB,MACrB,OAAO,KAAM,cAAY,KAAc,CAAC,MAAM,QAAQ,CAAC,GAGnD,KAAqB,MACrB,KAAK,OAAa,MAClB,OAAO,KAAM,WAAiB,IAC9B,OAAO,KAAM,YAAY,OAAO,KAAM,YAAkB,OAAO,CAAC,IAC7D,KAAK,UAAU,GAAG,MAAM,CAAC,GAgBrB,KAAgF,EAC3F,YACA,mBACI;CACJ,IAAI,KAAW,MACb,OAAO,kBAAC,KAAD;EAAG,WAAU;YAA8B;CAA6B,CAAA;CAEjF,IAAI,EAAc,CAAO,GAAG;EAC1B,IAAM,IAAO,IAAI,KAAK,KAAa,CAAC,GAAG,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,GACtD,IAAU,OAAO,QAAQ,CAAO,EAAE,MAAM,CAAC,IAAI,CAAC,QACvC,EAAK,IAAI,CAAC,GAAG,4BACb,EAAK,IAAI,CAAC,GAAG,wBAEzB;EAID,OAHI,EAAQ,WAAW,IACd,kBAAC,KAAD;GAAG,WAAU;aAA8B;EAA6B,CAAA,IAG/E,kBAAC,MAAD;GAAI,WAAU;aACX,EAAQ,KAAK,CAAC,GAAK,IAAQ,MAC1B,kBAAC,MAAD;IAEE,WAAU;cAFZ,CAIE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CAA8F,KAC1F,IAAI,CACF;SACL,EAAK,IAAI,CAAG,IACX,kBAAC,QAAD;MAAM,WAAU;MAA0C,OAAO;gBAC9D,EAAK,IAAI,CAAG,GAAG;KACZ,CAAA,IAEN,kBAAC,QAAD;MAAM,WAAU;gBAAqD;KAAU,CAAA,CAE9E;QACL,kBAAC,QAAD;KAAM,WAAU;eACb,EAAkB,CAAK;IACpB,CAAA,CACJ;MAlBG,CAkBH,CACL;EACC,CAAA;CAER;CACA,OACE,kBAAC,OAAD;EAAK,WAAU;YACZ,KAAK,UAAU,GAAS,MAAM,CAAC;CAC7B,CAAA;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"BulkGradingPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/submissions/BulkGradingPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAwShC,eAAO,MAAM,eAAe,EAAE,EA0I7B,CAAC"}
1
+ {"version":3,"file":"BulkGradingPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/submissions/BulkGradingPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AA+ShC,eAAO,MAAM,eAAe,EAAE,EA0I7B,CAAC"}
@@ -6,170 +6,171 @@ import { EmptyState as r } from "../../components/EmptyState.js";
6
6
  import { ErrorView as i, LoadingView as a } from "../../components/StateViews.js";
7
7
  import { Pagination as o } from "../../components/Pagination.js";
8
8
  import { Avatar as s } from "../../components/Avatar.js";
9
- import { useBuiGradeSubmission as c, useBuiReviewGradeSuggestion as l, useBuiSubmissions as u, useBuiSuggestGrade as d } from "../../hooks/useBuildmyiqApi.js";
10
- import { useCursorPager as f } from "../../hooks/useCursorPager.js";
9
+ import { useBuiAssessment as c, useBuiGradeSubmission as l, useBuiReviewGradeSuggestion as u, useBuiSubmissions as d, useBuiSuggestGrade as f } from "../../hooks/useBuildmyiqApi.js";
10
+ import { useCursorPager as p } from "../../hooks/useCursorPager.js";
11
11
  import "../../hooks/index.js";
12
- import { BuiPermissionButton as p } from "../../components/BuiPermissionButton.js";
12
+ import { BuiPermissionButton as m } from "../../components/BuiPermissionButton.js";
13
13
  import "../../components/index.js";
14
- import { AnswersView as m } from "./AnswersView.js";
15
- import { FEEDBACK_TEMPLATES as h, isLowConfidence as g } from "./aiGrading.js";
16
- import { useCallback as _, useEffect as v, useMemo as y, useRef as b, useState as x } from "react";
17
- import { ArrowLeft as S, CheckCircle2 as C, ClipboardList as w, Keyboard as T, Sparkles as E, TriangleAlert as D } from "lucide-react";
18
- import { Fragment as O, jsx as k, jsxs as A } from "react/jsx-runtime";
19
- import { Button as j, GlassCard as M, Input as N, Label as P, Switch as F, Textarea as I } from "@burdenoff/fe-libs/ui";
14
+ import { AnswersView as h } from "./AnswersView.js";
15
+ import { FEEDBACK_TEMPLATES as g, isLowConfidence as _ } from "./aiGrading.js";
16
+ import { useCallback as v, useEffect as y, useMemo as b, useRef as x, useState as S } from "react";
17
+ import { ArrowLeft as C, CheckCircle2 as w, ClipboardList as T, Keyboard as E, Sparkles as D, TriangleAlert as O } from "lucide-react";
18
+ import { Fragment as k, jsx as A, jsxs as j } from "react/jsx-runtime";
19
+ import { Button as M, GlassCard as N, Input as P, Label as F, Switch as I, Textarea as L } from "@burdenoff/fe-libs/ui";
20
20
  //#region src/buildmyiq/pages/submissions/BulkGradingPage.tsx
21
- var L = 12, R = (e) => e.length > 10 ? `${e.slice(0, 8)}…` : e, z = ({ submission: e, isMe: t, isFocused: r, registerActions: i }) => {
22
- let a = c(), o = d(), u = l(), [f, _] = x(""), [y, S] = x(!1), [w, T] = x(""), [O, L] = x(null), [z, B] = x(!1), [V, H] = x(null), U = async () => {
23
- L(null);
21
+ var R = 12, z = (e) => e.length > 10 ? `${e.slice(0, 8)}…` : e, B = ({ submission: e, isMe: t, isFocused: r, registerActions: i }) => {
22
+ let a = l(), o = f(), d = u(), { data: p } = c(e.assessmentId), [v, b] = S(""), [C, T] = S(!1), [E, k] = S(""), [R, B] = S(null), [V, H] = S(!1), [U, W] = S(null), G = async () => {
23
+ B(null);
24
24
  try {
25
25
  let t = await o.mutateAsync(e.id);
26
- _(String(Math.round(t.proposedScore))), T(t.perQuestionFeedback.map((e, t) => `Q${t + 1}: ${e.feedback}`).join("\n")), H({
26
+ b(String(Math.round(t.proposedScore))), p && T(t.proposedScore >= p.passingScore), k(t.perQuestionFeedback.map((e, t) => `Q${t + 1}: ${e.feedback}`).join("\n")), W({
27
27
  id: t.id,
28
28
  confidence: t.confidence,
29
29
  criterionScores: t.perQuestionFeedback.flatMap((e) => e.criterionScores ?? [])
30
30
  });
31
31
  } catch (e) {
32
- L(e instanceof Error ? e.message : "AI grading assistant is unavailable.");
32
+ B(e instanceof Error ? e.message : "AI grading assistant is unavailable.");
33
33
  }
34
- }, W = async () => {
35
- let t = f.trim(), n = Number(t);
34
+ }, K = async () => {
35
+ let t = v.trim(), n = Number(t);
36
36
  if (t === "" || Number.isNaN(n) || n < 0) {
37
- L("Enter a valid non-negative score.");
37
+ B("Enter a valid non-negative score.");
38
38
  return;
39
39
  }
40
- L(null);
40
+ B(null);
41
41
  try {
42
42
  await a.mutateAsync({
43
43
  id: e.id,
44
44
  input: {
45
45
  score: n,
46
- passed: y,
47
- feedback: w.trim() || void 0
46
+ passed: C,
47
+ feedback: E.trim() || void 0
48
48
  }
49
- }), V && u.mutate({
50
- id: V.id,
49
+ }), U && d.mutate({
50
+ id: U.id,
51
51
  outcome: "ACCEPTED"
52
- }), B(!0);
52
+ }), H(!0);
53
53
  } catch (e) {
54
- L(e instanceof Error ? e.message : "Failed to save grade.");
54
+ B(e instanceof Error ? e.message : "Failed to save grade.");
55
55
  }
56
- }, G = b({
57
- suggest: U,
58
- save: W
56
+ }, q = x({
57
+ suggest: G,
58
+ save: K
59
59
  });
60
- v(() => {
61
- G.current = {
62
- suggest: U,
63
- save: W
60
+ y(() => {
61
+ q.current = {
62
+ suggest: G,
63
+ save: K
64
64
  };
65
- }), v(() => (i(e.id, {
66
- suggest: () => void G.current.suggest(),
67
- save: () => void G.current.save()
65
+ }), y(() => (i(e.id, {
66
+ suggest: () => void q.current.suggest(),
67
+ save: () => void q.current.save()
68
68
  }), () => i(e.id, null)), [e.id, i]);
69
- let K = (e) => {
70
- T((t) => t.trim().length > 0 ? `${t}\n\n${e}` : e);
69
+ let J = (e) => {
70
+ k((t) => t.trim().length > 0 ? `${t}\n\n${e}` : e);
71
71
  };
72
- return z ? /* @__PURE__ */ A(M, {
72
+ return V ? /* @__PURE__ */ j(N, {
73
73
  treatment: "plain",
74
74
  className: "flex items-center gap-3 p-4",
75
- children: [/* @__PURE__ */ k(C, { className: "h-5 w-5 shrink-0 text-status-success-text" }), /* @__PURE__ */ A("p", {
75
+ children: [/* @__PURE__ */ A(w, { className: "h-5 w-5 shrink-0 text-status-success-text" }), /* @__PURE__ */ j("p", {
76
76
  className: "text-sm text-text-secondary",
77
77
  children: [
78
78
  "Graded ",
79
- t ? "your own attempt" : `learner ${R(e.learnerId)}`,
79
+ t ? "your own attempt" : `learner ${z(e.learnerId)}`,
80
80
  " — score",
81
81
  " ",
82
- /* @__PURE__ */ k("span", {
82
+ /* @__PURE__ */ A("span", {
83
83
  className: "font-semibold text-text-primary tabular-nums",
84
- children: f
84
+ children: v
85
85
  }),
86
86
  "."
87
87
  ]
88
88
  })]
89
- }) : /* @__PURE__ */ A(M, {
89
+ }) : /* @__PURE__ */ j(N, {
90
90
  treatment: "plain",
91
91
  id: `bulk-grade-row-${e.id}`,
92
92
  className: `flex flex-col gap-3 p-4 transition-shadow ${r ? "ring-2 ring-[var(--color-accent-teal)]" : ""}`,
93
- children: [/* @__PURE__ */ A("div", {
93
+ children: [/* @__PURE__ */ j("div", {
94
94
  className: "flex items-center gap-3",
95
95
  children: [
96
- /* @__PURE__ */ k(s, {
96
+ /* @__PURE__ */ A(s, {
97
97
  size: "sm",
98
98
  seed: e.learnerId
99
99
  }),
100
- /* @__PURE__ */ A("div", {
100
+ /* @__PURE__ */ j("div", {
101
101
  className: "min-w-0 flex-1",
102
- children: [/* @__PURE__ */ k("p", {
102
+ children: [/* @__PURE__ */ A("p", {
103
103
  className: "truncate text-sm font-medium text-text-primary",
104
- children: t ? "You" : `Learner ${R(e.learnerId)}`
105
- }), /* @__PURE__ */ A("p", {
104
+ children: t ? "You" : `Learner ${z(e.learnerId)}`
105
+ }), /* @__PURE__ */ j("p", {
106
106
  className: "truncate text-xs text-text-tertiary",
107
107
  title: e.assessmentId,
108
108
  children: [
109
- "Assessment ",
110
- R(e.assessmentId),
109
+ p?.title ?? `Assessment ${z(e.assessmentId)}`,
111
110
  " · Attempt #",
112
111
  e.attempt,
113
- " ·",
114
- " ",
112
+ " · ",
115
113
  n(e.submittedAt)
116
114
  ]
117
115
  })]
118
116
  }),
119
- /* @__PURE__ */ A(j, {
117
+ /* @__PURE__ */ j(M, {
120
118
  type: "button",
121
119
  variant: "outline",
122
120
  size: "sm",
123
- onClick: U,
121
+ onClick: G,
124
122
  disabled: o.isPending,
125
123
  className: "shrink-0",
126
- children: [/* @__PURE__ */ k(E, { className: "mr-1.5 h-3.5 w-3.5" }), o.isPending ? "Asking AI…" : "Suggest"]
124
+ children: [/* @__PURE__ */ A(D, { className: "mr-1.5 h-3.5 w-3.5" }), o.isPending ? "Asking AI…" : "Suggest"]
127
125
  })
128
126
  ]
129
- }), /* @__PURE__ */ A("div", {
127
+ }), /* @__PURE__ */ j("div", {
130
128
  className: "grid gap-4 lg:grid-cols-2",
131
- children: [/* @__PURE__ */ A("div", {
129
+ children: [/* @__PURE__ */ j("div", {
132
130
  className: "min-w-0",
133
- children: [/* @__PURE__ */ k("p", {
131
+ children: [/* @__PURE__ */ A("p", {
134
132
  className: "mb-1.5 text-xs font-medium uppercase tracking-wide text-text-secondary",
135
133
  children: "Learner’s answers"
136
- }), /* @__PURE__ */ k("div", {
134
+ }), /* @__PURE__ */ A("div", {
137
135
  className: "max-h-72 overflow-y-auto pr-1",
138
- children: /* @__PURE__ */ k(m, { answers: e.answers })
136
+ children: /* @__PURE__ */ A(h, {
137
+ answers: e.answers,
138
+ questions: p?.questions
139
+ })
139
140
  })]
140
- }), /* @__PURE__ */ A("div", {
141
+ }), /* @__PURE__ */ j("div", {
141
142
  className: "grid content-start gap-3",
142
143
  children: [
143
- V && /* @__PURE__ */ A("div", {
144
+ U && /* @__PURE__ */ j("div", {
144
145
  className: "rounded-lg border border-border-seam/70 bg-bg-surface/60 p-2.5",
145
146
  children: [
146
- /* @__PURE__ */ A("div", {
147
+ /* @__PURE__ */ j("div", {
147
148
  className: "flex items-center gap-2 text-xs",
148
- children: [/* @__PURE__ */ k(E, { className: "h-3.5 w-3.5 shrink-0 text-[var(--color-accent-teal)]" }), /* @__PURE__ */ A("span", {
149
+ children: [/* @__PURE__ */ A(D, { className: "h-3.5 w-3.5 shrink-0 text-[var(--color-accent-teal)]" }), /* @__PURE__ */ j("span", {
149
150
  className: "text-text-secondary",
150
151
  children: [
151
152
  "AI confidence:",
152
153
  " ",
153
- /* @__PURE__ */ A("span", {
154
- className: `tabular-nums ${g(V.confidence) ? "font-semibold text-status-warning-text" : "font-medium text-text-primary"}`,
155
- children: [Math.round(V.confidence * 100), "%"]
154
+ /* @__PURE__ */ j("span", {
155
+ className: `tabular-nums ${_(U.confidence) ? "font-semibold text-status-warning-text" : "font-medium text-text-primary"}`,
156
+ children: [Math.round(U.confidence * 100), "%"]
156
157
  })
157
158
  ]
158
159
  })]
159
160
  }),
160
- g(V.confidence) && /* @__PURE__ */ A("p", {
161
+ _(U.confidence) && /* @__PURE__ */ j("p", {
161
162
  className: "mt-1.5 flex items-start gap-1.5 text-xs text-status-warning-text",
162
- children: [/* @__PURE__ */ k(D, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }), "Low AI confidence — review this suggestion especially carefully before saving."]
163
+ children: [/* @__PURE__ */ A(O, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }), "Low AI confidence — review this suggestion especially carefully before saving."]
163
164
  }),
164
- V.criterionScores.length > 0 && /* @__PURE__ */ k("ul", {
165
+ U.criterionScores.length > 0 && /* @__PURE__ */ A("ul", {
165
166
  className: "mt-1.5 flex flex-col gap-0.5 border-l border-border-seam/70 pl-2.5 text-xs text-text-tertiary",
166
- children: V.criterionScores.map((e) => /* @__PURE__ */ A("li", { children: [
167
- /* @__PURE__ */ k("span", {
167
+ children: U.criterionScores.map((e) => /* @__PURE__ */ j("li", { children: [
168
+ /* @__PURE__ */ A("span", {
168
169
  className: "font-medium text-text-secondary",
169
170
  children: e.title
170
171
  }),
171
172
  " ",
172
- /* @__PURE__ */ A("span", {
173
+ /* @__PURE__ */ j("span", {
173
174
  className: "tabular-nums",
174
175
  children: [
175
176
  e.suggestedPoints,
@@ -182,68 +183,68 @@ var L = 12, R = (e) => e.length > 10 ? `${e.slice(0, 8)}…` : e, z = ({ submiss
182
183
  })
183
184
  ]
184
185
  }),
185
- /* @__PURE__ */ A("div", {
186
+ /* @__PURE__ */ j("div", {
186
187
  className: "grid gap-3 sm:grid-cols-[7rem_auto_1fr] sm:items-start",
187
188
  children: [
188
- /* @__PURE__ */ A("div", {
189
+ /* @__PURE__ */ j("div", {
189
190
  className: "grid gap-1",
190
- children: [/* @__PURE__ */ k(P, {
191
+ children: [/* @__PURE__ */ A(F, {
191
192
  htmlFor: `score-${e.id}`,
192
193
  className: "text-xs",
193
194
  children: "Score"
194
- }), /* @__PURE__ */ k(N, {
195
+ }), /* @__PURE__ */ A(P, {
195
196
  id: `score-${e.id}`,
196
197
  type: "number",
197
198
  min: "0",
198
- value: f,
199
- onChange: (e) => _(e.target.value),
199
+ value: v,
200
+ onChange: (e) => b(e.target.value),
200
201
  placeholder: "0",
201
202
  className: "h-9"
202
203
  })]
203
204
  }),
204
- /* @__PURE__ */ A("div", {
205
+ /* @__PURE__ */ j("div", {
205
206
  className: "grid gap-1",
206
- children: [/* @__PURE__ */ k(P, {
207
+ children: [/* @__PURE__ */ A(F, {
207
208
  className: "text-xs",
208
209
  children: "Result"
209
- }), /* @__PURE__ */ A("div", {
210
+ }), /* @__PURE__ */ j("div", {
210
211
  className: "flex h-9 items-center gap-2",
211
- children: [/* @__PURE__ */ k(F, {
212
- checked: y,
213
- onCheckedChange: S
214
- }), /* @__PURE__ */ k("span", {
212
+ children: [/* @__PURE__ */ A(I, {
213
+ checked: C,
214
+ onCheckedChange: T
215
+ }), /* @__PURE__ */ A("span", {
215
216
  className: "text-xs text-text-secondary",
216
- children: y ? "Passed" : "Not passed"
217
+ children: C ? "Passed" : "Not passed"
217
218
  })]
218
219
  })]
219
220
  }),
220
- /* @__PURE__ */ A("div", {
221
+ /* @__PURE__ */ j("div", {
221
222
  className: "grid gap-1",
222
223
  children: [
223
- /* @__PURE__ */ k(P, {
224
+ /* @__PURE__ */ A(F, {
224
225
  htmlFor: `feedback-${e.id}`,
225
226
  className: "text-xs",
226
227
  children: "Feedback (optional)"
227
228
  }),
228
- /* @__PURE__ */ k(I, {
229
+ /* @__PURE__ */ A(L, {
229
230
  id: `feedback-${e.id}`,
230
231
  rows: 3,
231
- value: w,
232
- onChange: (e) => T(e.target.value),
232
+ value: E,
233
+ onChange: (e) => k(e.target.value),
233
234
  className: "text-sm"
234
235
  }),
235
- /* @__PURE__ */ A("select", {
236
- "aria-label": `Insert feedback template for learner ${R(e.learnerId)}`,
236
+ /* @__PURE__ */ j("select", {
237
+ "aria-label": `Insert feedback template for learner ${z(e.learnerId)}`,
237
238
  className: "h-8 rounded-lg border border-border-seam bg-bg-surface px-2 text-xs text-text-secondary",
238
239
  value: "",
239
240
  onChange: (e) => {
240
- let t = h.find((t) => t.text === e.target.value);
241
- t && K(t.text);
241
+ let t = g.find((t) => t.text === e.target.value);
242
+ t && J(t.text);
242
243
  },
243
- children: [/* @__PURE__ */ k("option", {
244
+ children: [/* @__PURE__ */ A("option", {
244
245
  value: "",
245
246
  children: "Insert template…"
246
- }), h.map((e) => /* @__PURE__ */ k("option", {
247
+ }), g.map((e) => /* @__PURE__ */ A("option", {
247
248
  value: e.text,
248
249
  children: e.label
249
250
  }, e.label))]
@@ -252,19 +253,19 @@ var L = 12, R = (e) => e.length > 10 ? `${e.slice(0, 8)}…` : e, z = ({ submiss
252
253
  })
253
254
  ]
254
255
  }),
255
- O && /* @__PURE__ */ k("p", {
256
+ R && /* @__PURE__ */ A("p", {
256
257
  className: "text-xs text-status-error-text",
257
- children: O
258
+ children: R
258
259
  }),
259
- /* @__PURE__ */ k("div", {
260
+ /* @__PURE__ */ A("div", {
260
261
  className: "flex justify-end",
261
- children: /* @__PURE__ */ k(p, {
262
+ children: /* @__PURE__ */ A(m, {
262
263
  resource: "buildmyiq-submission",
263
264
  action: "update",
264
265
  deniedBehavior: "hide",
265
- children: /* @__PURE__ */ k(j, {
266
+ children: /* @__PURE__ */ A(M, {
266
267
  size: "sm",
267
- onClick: W,
268
+ onClick: K,
268
269
  disabled: a.isPending,
269
270
  children: a.isPending ? "Saving…" : "Save grade"
270
271
  })
@@ -274,106 +275,106 @@ var L = 12, R = (e) => e.length > 10 ? `${e.slice(0, 8)}…` : e, z = ({ submiss
274
275
  })]
275
276
  })]
276
277
  });
277
- }, B = () => {
278
- let { currentUser: n, navigateTo: s } = e(), c = f(), { data: l, isLoading: d, isError: p, refetch: m } = u({
278
+ }, V = () => {
279
+ let { currentUser: n, navigateTo: s } = e(), c = p(), { data: l, isLoading: u, isError: f, refetch: m } = d({
279
280
  status: "SUBMITTED",
280
281
  cursor: c.cursor,
281
- limit: L
282
- }), h = y(() => l?.items ?? [], [l]), [g, C] = x(0), E = b(/* @__PURE__ */ new Map()), D = _((e, t) => {
283
- t ? E.current.set(e, t) : E.current.delete(e);
284
- }, []), M = h.length === 0 ? 0 : Math.min(g, h.length - 1);
285
- return v(() => {
286
- let e = h[M];
282
+ limit: R
283
+ }), h = b(() => l?.items ?? [], [l]), [g, _] = S(0), w = x(/* @__PURE__ */ new Map()), D = v((e, t) => {
284
+ t ? w.current.set(e, t) : w.current.delete(e);
285
+ }, []), O = h.length === 0 ? 0 : Math.min(g, h.length - 1);
286
+ return y(() => {
287
+ let e = h[O];
287
288
  e && document.getElementById(`bulk-grade-row-${e.id}`)?.scrollIntoView({
288
289
  block: "nearest",
289
290
  behavior: "smooth"
290
291
  });
291
- }, [M, h]), v(() => {
292
+ }, [O, h]), y(() => {
292
293
  let e = (e) => {
293
294
  if (h.length === 0) return;
294
295
  let t = e.target;
295
296
  if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT" || t.isContentEditable) || e.ctrlKey || e.metaKey || e.altKey) return;
296
297
  let n = (e) => {
297
- C((t) => Math.min(Math.max(t + e, 0), h.length - 1));
298
- }, r = h[M], i = r ? E.current.get(r.id) : void 0;
298
+ _((t) => Math.min(Math.max(t + e, 0), h.length - 1));
299
+ }, r = h[O], i = r ? w.current.get(r.id) : void 0;
299
300
  e.key === "j" || e.key === "ArrowDown" ? (e.preventDefault(), n(1)) : e.key === "k" || e.key === "ArrowUp" ? (e.preventDefault(), n(-1)) : e.key === "s" && i ? (e.preventDefault(), i.suggest()) : e.key === "Enter" && i && (e.preventDefault(), i.save());
300
301
  };
301
302
  return window.addEventListener("keydown", e), () => window.removeEventListener("keydown", e);
302
- }, [h, M]), /* @__PURE__ */ k(t, {
303
+ }, [h, O]), /* @__PURE__ */ A(t, {
303
304
  title: "Bulk grade",
304
305
  description: "Work through every awaiting-grade submission in one pass — read the answers, suggest, adjust, save.",
305
- actions: /* @__PURE__ */ A(j, {
306
+ actions: /* @__PURE__ */ j(M, {
306
307
  variant: "ghost",
307
308
  onClick: () => s("submissions"),
308
- children: [/* @__PURE__ */ k(S, { className: "mr-1.5 h-4 w-4" }), " Back to grading queue"]
309
+ children: [/* @__PURE__ */ A(C, { className: "mr-1.5 h-4 w-4" }), " Back to grading queue"]
309
310
  }),
310
- children: /* @__PURE__ */ k("div", {
311
+ children: /* @__PURE__ */ A("div", {
311
312
  className: "flex flex-col gap-4",
312
- children: d ? /* @__PURE__ */ k(a, { label: "Loading submissions awaiting grade…" }) : p ? /* @__PURE__ */ k(i, {
313
+ children: u ? /* @__PURE__ */ A(a, { label: "Loading submissions awaiting grade…" }) : f ? /* @__PURE__ */ A(i, {
313
314
  title: "Couldn't load submissions",
314
315
  message: "The grading queue failed to load.",
315
316
  onRetry: () => m()
316
- }) : h.length === 0 ? /* @__PURE__ */ k(r, {
317
+ }) : h.length === 0 ? /* @__PURE__ */ A(r, {
317
318
  illustration: "empty-data",
318
- icon: /* @__PURE__ */ k(w, { className: "h-10 w-10" }),
319
+ icon: /* @__PURE__ */ A(T, { className: "h-10 w-10" }),
319
320
  title: "Nothing to grade",
320
321
  description: "Every submission is already graded, or none have been submitted yet.",
321
- action: /* @__PURE__ */ k(j, {
322
+ action: /* @__PURE__ */ A(M, {
322
323
  variant: "outline",
323
324
  onClick: () => s("submissions"),
324
325
  children: "Back to grading queue"
325
326
  })
326
- }) : /* @__PURE__ */ A(O, { children: [
327
- /* @__PURE__ */ A("p", {
327
+ }) : /* @__PURE__ */ j(k, { children: [
328
+ /* @__PURE__ */ j("p", {
328
329
  className: "flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-text-tertiary",
329
- children: [/* @__PURE__ */ k(T, { className: "h-3.5 w-3.5 shrink-0" }), /* @__PURE__ */ A("span", { children: [
330
+ children: [/* @__PURE__ */ A(E, { className: "h-3.5 w-3.5 shrink-0" }), /* @__PURE__ */ j("span", { children: [
330
331
  "Shortcuts: ",
331
- /* @__PURE__ */ k("kbd", {
332
+ /* @__PURE__ */ A("kbd", {
332
333
  className: "font-semibold",
333
334
  children: "J"
334
335
  }),
335
336
  "/",
336
- /* @__PURE__ */ k("kbd", {
337
+ /* @__PURE__ */ A("kbd", {
337
338
  className: "font-semibold",
338
339
  children: "↓"
339
340
  }),
340
341
  " next · ",
341
- /* @__PURE__ */ k("kbd", {
342
+ /* @__PURE__ */ A("kbd", {
342
343
  className: "font-semibold",
343
344
  children: "K"
344
345
  }),
345
346
  "/",
346
- /* @__PURE__ */ k("kbd", {
347
+ /* @__PURE__ */ A("kbd", {
347
348
  className: "font-semibold",
348
349
  children: "↑"
349
350
  }),
350
351
  " previous ·",
351
352
  " ",
352
- /* @__PURE__ */ k("kbd", {
353
+ /* @__PURE__ */ A("kbd", {
353
354
  className: "font-semibold",
354
355
  children: "S"
355
356
  }),
356
357
  " suggest AI ·",
357
358
  " ",
358
- /* @__PURE__ */ k("kbd", {
359
+ /* @__PURE__ */ A("kbd", {
359
360
  className: "font-semibold",
360
361
  children: "Enter"
361
362
  }),
362
363
  " save focused — inactive while typing in a field."
363
364
  ] })]
364
365
  }),
365
- /* @__PURE__ */ k("div", {
366
+ /* @__PURE__ */ A("div", {
366
367
  className: "flex flex-col gap-3",
367
- children: h.map((e, t) => /* @__PURE__ */ k(z, {
368
+ children: h.map((e, t) => /* @__PURE__ */ A(B, {
368
369
  submission: e,
369
370
  isMe: e.learnerId === n?.id,
370
- isFocused: t === M,
371
+ isFocused: t === O,
371
372
  registerActions: D
372
373
  }, e.id))
373
374
  }),
374
- /* @__PURE__ */ k(o, {
375
+ /* @__PURE__ */ A(o, {
375
376
  page: c.page,
376
- pageSize: L,
377
+ pageSize: R,
377
378
  itemCount: h.length,
378
379
  hasMore: l?.pageInfo?.hasMore ?? !1,
379
380
  onPageChange: (e) => c.onPageChange(e, l?.pageInfo?.cursor)
@@ -383,6 +384,6 @@ var L = 12, R = (e) => e.length > 10 ? `${e.slice(0, 8)}…` : e, z = ({ submiss
383
384
  });
384
385
  };
385
386
  //#endregion
386
- export { B as BulkGradingPage };
387
+ export { V as BulkGradingPage };
387
388
 
388
389
  //# sourceMappingURL=BulkGradingPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"BulkGradingPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/submissions/BulkGradingPage.tsx"],"sourcesContent":["/**\n * BulkGradingPage — SpeedGrader-like bulk grading queue (BOFF-4429).\n * @module buildmyiq/pages/submissions\n *\n * Shows every SUBMITTED submission in the workspace as an inline card with the\n * learner's answers side-by-side with a compact grading form, plus per-row\n * quick actions: get an AI grading suggestion (`buiSuggestGrade`, including\n * per-criterion rubric scores when the question carries a rubric), tweak\n * score/passed/feedback inline, and save (`buiGradeSubmission`) — all without\n * navigating away or opening the full slide-over per submission, so an\n * educator can work through a whole queue in one pass. The AI suggestion is\n * always a proposal the educator edits/accepts before saving — it never\n * auto-commits a grade.\n *\n * SpeedGrader-style affordances:\n * - Keyboard shortcuts: J/↓ next row, K/↑ previous row, S suggest AI for the\n * focused row, Enter save the focused row (ignored while typing in a field).\n * - Reusable feedback templates that prefill (never replace) the feedback box.\n * - Confidence badge on every AI suggestion; below the LOW_AI_CONFIDENCE\n * threshold the row shows an explicit \"review carefully\" warning. Educator\n * approval is mandatory either way — suggestions are never auto-committed.\n */\n\nimport type { FC } from 'react';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n ArrowLeft,\n CheckCircle2,\n ClipboardList,\n Keyboard,\n Sparkles,\n TriangleAlert,\n} from 'lucide-react';\nimport { Button, GlassCard, Input, Label, Switch, Textarea } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n EmptyState,\n LoadingView,\n ErrorView,\n Pagination,\n Avatar,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiSubmissions,\n useBuiGradeSubmission,\n useBuiSuggestGrade,\n useBuiReviewGradeSuggestion,\n useCursorPager,\n} from '../../hooks';\nimport type { BuiSubmission } from '../../types';\nimport { formatDateTime } from '../../utils/formatters';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\nimport { AnswersView } from './AnswersView';\nimport { FEEDBACK_TEMPLATES, isLowConfidence } from './aiGrading';\n\nconst PAGE_SIZE = 12;\n\n/** Truncate an opaque id for compact display, keeping the full value in `title`. */\nconst shortId = (id: string): string => (id.length > 10 ? `${id.slice(0, 8)}…` : id);\n\n/** Actions a focused row exposes to the page-level keyboard handler. */\ninterface RowActions {\n suggest: () => void;\n save: () => void;\n}\n\ninterface BulkGradeRowProps {\n submission: BuiSubmission;\n isMe: boolean;\n isFocused: boolean;\n registerActions: (submissionId: string, actions: RowActions | null) => void;\n}\n\nconst BulkGradeRow: FC<BulkGradeRowProps> = ({ submission, isMe, isFocused, registerActions }) => {\n const gradeSubmission = useBuiGradeSubmission();\n const suggestGrade = useBuiSuggestGrade();\n const reviewSuggestion = useBuiReviewGradeSuggestion();\n\n const [scoreInput, setScoreInput] = useState('');\n const [passed, setPassed] = useState(false);\n const [feedback, setFeedback] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [saved, setSaved] = useState(false);\n const [suggestion, setSuggestion] = useState<{\n id: string;\n confidence: number;\n criterionScores: Array<{\n criterionId: string;\n title: string;\n maxPoints: number;\n suggestedPoints: number;\n comment?: string | null;\n }>;\n } | null>(null);\n\n const handleSuggest = async () => {\n setError(null);\n try {\n const result = await suggestGrade.mutateAsync(submission.id);\n setScoreInput(String(Math.round(result.proposedScore)));\n setFeedback(result.perQuestionFeedback.map((f, i) => `Q${i + 1}: ${f.feedback}`).join('\\n'));\n setSuggestion({\n id: result.id,\n confidence: result.confidence,\n criterionScores: result.perQuestionFeedback.flatMap((f) => f.criterionScores ?? []),\n });\n } catch (e) {\n setError(e instanceof Error ? e.message : 'AI grading assistant is unavailable.');\n }\n };\n\n const handleSave = async () => {\n const trimmed = scoreInput.trim();\n const scoreNum = Number(trimmed);\n if (trimmed === '' || Number.isNaN(scoreNum) || scoreNum < 0) {\n setError('Enter a valid non-negative score.');\n return;\n }\n setError(null);\n try {\n await gradeSubmission.mutateAsync({\n id: submission.id,\n input: { score: scoreNum, passed, feedback: feedback.trim() || undefined },\n });\n if (suggestion) reviewSuggestion.mutate({ id: suggestion.id, outcome: 'ACCEPTED' });\n setSaved(true);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to save grade.');\n }\n };\n\n // Expose suggest/save to the page-level keyboard handler while mounted.\n // The handlers close over per-render state, so keep them in a ref and\n // register stable wrappers exactly once per submission.\n const latestHandlers = useRef({ suggest: handleSuggest, save: handleSave });\n useEffect(() => {\n latestHandlers.current = { suggest: handleSuggest, save: handleSave };\n });\n useEffect(() => {\n registerActions(submission.id, {\n suggest: () => void latestHandlers.current.suggest(),\n save: () => void latestHandlers.current.save(),\n });\n return () => registerActions(submission.id, null);\n }, [submission.id, registerActions]);\n\n const handleInsertTemplate = (templateText: string) => {\n setFeedback((prev) => (prev.trim().length > 0 ? `${prev}\\n\\n${templateText}` : templateText));\n };\n\n if (saved) {\n return (\n <GlassCard treatment=\"plain\" className=\"flex items-center gap-3 p-4\">\n <CheckCircle2 className=\"h-5 w-5 shrink-0 text-status-success-text\" />\n <p className=\"text-sm text-text-secondary\">\n Graded {isMe ? 'your own attempt' : `learner ${shortId(submission.learnerId)}`} — score{' '}\n <span className=\"font-semibold text-text-primary tabular-nums\">{scoreInput}</span>.\n </p>\n </GlassCard>\n );\n }\n\n return (\n <GlassCard\n treatment=\"plain\"\n id={`bulk-grade-row-${submission.id}`}\n className={`flex flex-col gap-3 p-4 transition-shadow ${\n isFocused ? 'ring-2 ring-[var(--color-accent-teal)]' : ''\n }`}\n >\n <div className=\"flex items-center gap-3\">\n <Avatar size=\"sm\" seed={submission.learnerId} />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {isMe ? 'You' : `Learner ${shortId(submission.learnerId)}`}\n </p>\n <p className=\"truncate text-xs text-text-tertiary\" title={submission.assessmentId}>\n Assessment {shortId(submission.assessmentId)} · Attempt #{submission.attempt} ·{' '}\n {formatDateTime(submission.submittedAt)}\n </p>\n </div>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={handleSuggest}\n disabled={suggestGrade.isPending}\n className=\"shrink-0\"\n >\n <Sparkles className=\"mr-1.5 h-3.5 w-3.5\" />\n {suggestGrade.isPending ? 'Asking AI…' : 'Suggest'}\n </Button>\n </div>\n\n {/* Side-by-side: learner's answers | grading form (stacks on small screens). */}\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <div className=\"min-w-0\">\n <p className=\"mb-1.5 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Learner&rsquo;s answers\n </p>\n <div className=\"max-h-72 overflow-y-auto pr-1\">\n <AnswersView answers={submission.answers} />\n </div>\n </div>\n\n <div className=\"grid content-start gap-3\">\n {suggestion && (\n <div className=\"rounded-lg border border-border-seam/70 bg-bg-surface/60 p-2.5\">\n <div className=\"flex items-center gap-2 text-xs\">\n <Sparkles className=\"h-3.5 w-3.5 shrink-0 text-[var(--color-accent-teal)]\" />\n <span className=\"text-text-secondary\">\n AI confidence:{' '}\n <span\n className={`tabular-nums ${\n isLowConfidence(suggestion.confidence)\n ? 'font-semibold text-status-warning-text'\n : 'font-medium text-text-primary'\n }`}\n >\n {Math.round(suggestion.confidence * 100)}%\n </span>\n </span>\n </div>\n {isLowConfidence(suggestion.confidence) && (\n <p className=\"mt-1.5 flex items-start gap-1.5 text-xs text-status-warning-text\">\n <TriangleAlert className=\"mt-0.5 h-3.5 w-3.5 shrink-0\" />\n Low AI confidence — review this suggestion especially carefully before saving.\n </p>\n )}\n {suggestion.criterionScores.length > 0 && (\n <ul className=\"mt-1.5 flex flex-col gap-0.5 border-l border-border-seam/70 pl-2.5 text-xs text-text-tertiary\">\n {suggestion.criterionScores.map((c) => (\n <li key={c.criterionId}>\n <span className=\"font-medium text-text-secondary\">{c.title}</span>{' '}\n <span className=\"tabular-nums\">\n {c.suggestedPoints}/{c.maxPoints}\n </span>\n {c.comment ? ` — ${c.comment}` : ''}\n </li>\n ))}\n </ul>\n )}\n </div>\n )}\n\n <div className=\"grid gap-3 sm:grid-cols-[7rem_auto_1fr] sm:items-start\">\n <div className=\"grid gap-1\">\n <Label htmlFor={`score-${submission.id}`} className=\"text-xs\">\n Score\n </Label>\n <Input\n id={`score-${submission.id}`}\n type=\"number\"\n min=\"0\"\n value={scoreInput}\n onChange={(e) => setScoreInput(e.target.value)}\n placeholder=\"0\"\n className=\"h-9\"\n />\n </div>\n <div className=\"grid gap-1\">\n <Label className=\"text-xs\">Result</Label>\n <div className=\"flex h-9 items-center gap-2\">\n <Switch checked={passed} onCheckedChange={setPassed} />\n <span className=\"text-xs text-text-secondary\">\n {passed ? 'Passed' : 'Not passed'}\n </span>\n </div>\n </div>\n <div className=\"grid gap-1\">\n <Label htmlFor={`feedback-${submission.id}`} className=\"text-xs\">\n Feedback (optional)\n </Label>\n <Textarea\n id={`feedback-${submission.id}`}\n rows={3}\n value={feedback}\n onChange={(e) => setFeedback(e.target.value)}\n className=\"text-sm\"\n />\n <select\n aria-label={`Insert feedback template for learner ${shortId(submission.learnerId)}`}\n className=\"h-8 rounded-lg border border-border-seam bg-bg-surface px-2 text-xs text-text-secondary\"\n value=\"\"\n onChange={(e) => {\n const template = FEEDBACK_TEMPLATES.find((t) => t.text === e.target.value);\n if (template) handleInsertTemplate(template.text);\n }}\n >\n <option value=\"\">Insert template…</option>\n {FEEDBACK_TEMPLATES.map((t) => (\n <option key={t.label} value={t.text}>\n {t.label}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n\n <div className=\"flex justify-end\">\n <BuiPermissionButton\n resource=\"buildmyiq-submission\"\n action=\"update\"\n deniedBehavior=\"hide\"\n >\n <Button size=\"sm\" onClick={handleSave} disabled={gradeSubmission.isPending}>\n {gradeSubmission.isPending ? 'Saving…' : 'Save grade'}\n </Button>\n </BuiPermissionButton>\n </div>\n </div>\n </div>\n </GlassCard>\n );\n};\n\nexport const BulkGradingPage: FC = () => {\n const { currentUser, navigateTo } = useBuildmyiq();\n const pager = useCursorPager();\n const { data, isLoading, isError, refetch } = useBuiSubmissions({\n status: 'SUBMITTED',\n cursor: pager.cursor,\n limit: PAGE_SIZE,\n });\n\n const rows = useMemo<BuiSubmission[]>(() => data?.items ?? [], [data]);\n const [focusedIndex, setFocusedIndex] = useState(0);\n const rowActions = useRef(new Map<string, RowActions>());\n\n const registerActions = useCallback((submissionId: string, actions: RowActions | null) => {\n if (actions) rowActions.current.set(submissionId, actions);\n else rowActions.current.delete(submissionId);\n }, []);\n\n // Clamp to the visible page bounds at render time (no effect needed).\n const safeFocusedIndex = rows.length === 0 ? 0 : Math.min(focusedIndex, rows.length - 1);\n\n // Scroll the focused row into view when it changes.\n useEffect(() => {\n const focused = rows[safeFocusedIndex];\n if (!focused) return;\n document\n .getElementById(`bulk-grade-row-${focused.id}`)\n ?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });\n }, [safeFocusedIndex, rows]);\n\n // SpeedGrader-style keyboard shortcuts. Ignored entirely while the user is\n // typing in a form field, so Enter in the feedback box stays a newline.\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (rows.length === 0) return;\n const target = e.target as HTMLElement | null;\n if (\n target &&\n (target.tagName === 'INPUT' ||\n target.tagName === 'TEXTAREA' ||\n target.tagName === 'SELECT' ||\n target.isContentEditable)\n ) {\n return;\n }\n if (e.ctrlKey || e.metaKey || e.altKey) return;\n\n const move = (delta: number) => {\n setFocusedIndex((prev) => Math.min(Math.max(prev + delta, 0), rows.length - 1));\n };\n const focusedSubmission = rows[safeFocusedIndex];\n const actions = focusedSubmission ? rowActions.current.get(focusedSubmission.id) : undefined;\n\n if (e.key === 'j' || e.key === 'ArrowDown') {\n e.preventDefault();\n move(1);\n } else if (e.key === 'k' || e.key === 'ArrowUp') {\n e.preventDefault();\n move(-1);\n } else if (e.key === 's' && actions) {\n e.preventDefault();\n actions.suggest();\n } else if (e.key === 'Enter' && actions) {\n e.preventDefault();\n actions.save();\n }\n };\n window.addEventListener('keydown', onKeyDown);\n return () => window.removeEventListener('keydown', onKeyDown);\n }, [rows, safeFocusedIndex]);\n\n return (\n <PageLayout\n title=\"Bulk grade\"\n description=\"Work through every awaiting-grade submission in one pass — read the answers, suggest, adjust, save.\"\n actions={\n <Button variant=\"ghost\" onClick={() => navigateTo('submissions')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Back to grading queue\n </Button>\n }\n >\n <div className=\"flex flex-col gap-4\">\n {isLoading ? (\n <LoadingView label=\"Loading submissions awaiting grade…\" />\n ) : isError ? (\n <ErrorView\n title=\"Couldn't load submissions\"\n message=\"The grading queue failed to load.\"\n onRetry={() => refetch()}\n />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<ClipboardList className=\"h-10 w-10\" />}\n title=\"Nothing to grade\"\n description=\"Every submission is already graded, or none have been submitted yet.\"\n action={\n <Button variant=\"outline\" onClick={() => navigateTo('submissions')}>\n Back to grading queue\n </Button>\n }\n />\n ) : (\n <>\n <p className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-text-tertiary\">\n <Keyboard className=\"h-3.5 w-3.5 shrink-0\" />\n <span>\n Shortcuts: <kbd className=\"font-semibold\">J</kbd>/\n <kbd className=\"font-semibold\">↓</kbd> next · <kbd className=\"font-semibold\">K</kbd>\n /<kbd className=\"font-semibold\">↑</kbd> previous ·{' '}\n <kbd className=\"font-semibold\">S</kbd> suggest AI ·{' '}\n <kbd className=\"font-semibold\">Enter</kbd> save focused — inactive while typing in a\n field.\n </span>\n </p>\n <div className=\"flex flex-col gap-3\">\n {rows.map((s, i) => (\n <BulkGradeRow\n key={s.id}\n submission={s}\n isMe={s.learnerId === currentUser?.id}\n isFocused={i === safeFocusedIndex}\n registerActions={registerActions}\n />\n ))}\n </div>\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={rows.length}\n hasMore={data?.pageInfo?.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target, data?.pageInfo?.cursor)}\n />\n </>\n )}\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"BulkGradingPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/submissions/BulkGradingPage.tsx"],"sourcesContent":["/**\n * BulkGradingPage — SpeedGrader-like bulk grading queue (BOFF-4429).\n * @module buildmyiq/pages/submissions\n *\n * Shows every SUBMITTED submission in the workspace as an inline card with the\n * learner's answers side-by-side with a compact grading form, plus per-row\n * quick actions: get an AI grading suggestion (`buiSuggestGrade`, including\n * per-criterion rubric scores when the question carries a rubric), tweak\n * score/passed/feedback inline, and save (`buiGradeSubmission`) — all without\n * navigating away or opening the full slide-over per submission, so an\n * educator can work through a whole queue in one pass. The AI suggestion is\n * always a proposal the educator edits/accepts before saving — it never\n * auto-commits a grade.\n *\n * SpeedGrader-style affordances:\n * - Keyboard shortcuts: J/↓ next row, K/↑ previous row, S suggest AI for the\n * focused row, Enter save the focused row (ignored while typing in a field).\n * - Reusable feedback templates that prefill (never replace) the feedback box.\n * - Confidence badge on every AI suggestion; below the LOW_AI_CONFIDENCE\n * threshold the row shows an explicit \"review carefully\" warning. Educator\n * approval is mandatory either way — suggestions are never auto-committed.\n */\n\nimport type { FC } from 'react';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n ArrowLeft,\n CheckCircle2,\n ClipboardList,\n Keyboard,\n Sparkles,\n TriangleAlert,\n} from 'lucide-react';\nimport { Button, GlassCard, Input, Label, Switch, Textarea } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n EmptyState,\n LoadingView,\n ErrorView,\n Pagination,\n Avatar,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiAssessment,\n useBuiSubmissions,\n useBuiGradeSubmission,\n useBuiSuggestGrade,\n useBuiReviewGradeSuggestion,\n useCursorPager,\n} from '../../hooks';\nimport type { BuiSubmission } from '../../types';\nimport { formatDateTime } from '../../utils/formatters';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\nimport { AnswersView } from './AnswersView';\nimport { FEEDBACK_TEMPLATES, isLowConfidence } from './aiGrading';\n\nconst PAGE_SIZE = 12;\n\n/** Truncate an opaque id for compact display, keeping the full value in `title`. */\nconst shortId = (id: string): string => (id.length > 10 ? `${id.slice(0, 8)}…` : id);\n\n/** Actions a focused row exposes to the page-level keyboard handler. */\ninterface RowActions {\n suggest: () => void;\n save: () => void;\n}\n\ninterface BulkGradeRowProps {\n submission: BuiSubmission;\n isMe: boolean;\n isFocused: boolean;\n registerActions: (submissionId: string, actions: RowActions | null) => void;\n}\n\nconst BulkGradeRow: FC<BulkGradeRowProps> = ({ submission, isMe, isFocused, registerActions }) => {\n const gradeSubmission = useBuiGradeSubmission();\n const suggestGrade = useBuiSuggestGrade();\n const reviewSuggestion = useBuiReviewGradeSuggestion();\n // One cached query per assessment (rows on the same assessment share it):\n // labels the card and each answer, and supplies the passing score.\n const { data: assessment } = useBuiAssessment(submission.assessmentId);\n\n const [scoreInput, setScoreInput] = useState('');\n const [passed, setPassed] = useState(false);\n const [feedback, setFeedback] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [saved, setSaved] = useState(false);\n const [suggestion, setSuggestion] = useState<{\n id: string;\n confidence: number;\n criterionScores: Array<{\n criterionId: string;\n title: string;\n maxPoints: number;\n suggestedPoints: number;\n comment?: string | null;\n }>;\n } | null>(null);\n\n const handleSuggest = async () => {\n setError(null);\n try {\n const result = await suggestGrade.mutateAsync(submission.id);\n setScoreInput(String(Math.round(result.proposedScore)));\n // Prefill the result from the assessment's passing score; the educator\n // can still flip it before saving.\n if (assessment) setPassed(result.proposedScore >= assessment.passingScore);\n setFeedback(result.perQuestionFeedback.map((f, i) => `Q${i + 1}: ${f.feedback}`).join('\\n'));\n setSuggestion({\n id: result.id,\n confidence: result.confidence,\n criterionScores: result.perQuestionFeedback.flatMap((f) => f.criterionScores ?? []),\n });\n } catch (e) {\n setError(e instanceof Error ? e.message : 'AI grading assistant is unavailable.');\n }\n };\n\n const handleSave = async () => {\n const trimmed = scoreInput.trim();\n const scoreNum = Number(trimmed);\n if (trimmed === '' || Number.isNaN(scoreNum) || scoreNum < 0) {\n setError('Enter a valid non-negative score.');\n return;\n }\n setError(null);\n try {\n await gradeSubmission.mutateAsync({\n id: submission.id,\n input: { score: scoreNum, passed, feedback: feedback.trim() || undefined },\n });\n if (suggestion) reviewSuggestion.mutate({ id: suggestion.id, outcome: 'ACCEPTED' });\n setSaved(true);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to save grade.');\n }\n };\n\n // Expose suggest/save to the page-level keyboard handler while mounted.\n // The handlers close over per-render state, so keep them in a ref and\n // register stable wrappers exactly once per submission.\n const latestHandlers = useRef({ suggest: handleSuggest, save: handleSave });\n useEffect(() => {\n latestHandlers.current = { suggest: handleSuggest, save: handleSave };\n });\n useEffect(() => {\n registerActions(submission.id, {\n suggest: () => void latestHandlers.current.suggest(),\n save: () => void latestHandlers.current.save(),\n });\n return () => registerActions(submission.id, null);\n }, [submission.id, registerActions]);\n\n const handleInsertTemplate = (templateText: string) => {\n setFeedback((prev) => (prev.trim().length > 0 ? `${prev}\\n\\n${templateText}` : templateText));\n };\n\n if (saved) {\n return (\n <GlassCard treatment=\"plain\" className=\"flex items-center gap-3 p-4\">\n <CheckCircle2 className=\"h-5 w-5 shrink-0 text-status-success-text\" />\n <p className=\"text-sm text-text-secondary\">\n Graded {isMe ? 'your own attempt' : `learner ${shortId(submission.learnerId)}`} — score{' '}\n <span className=\"font-semibold text-text-primary tabular-nums\">{scoreInput}</span>.\n </p>\n </GlassCard>\n );\n }\n\n return (\n <GlassCard\n treatment=\"plain\"\n id={`bulk-grade-row-${submission.id}`}\n className={`flex flex-col gap-3 p-4 transition-shadow ${\n isFocused ? 'ring-2 ring-[var(--color-accent-teal)]' : ''\n }`}\n >\n <div className=\"flex items-center gap-3\">\n <Avatar size=\"sm\" seed={submission.learnerId} />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {isMe ? 'You' : `Learner ${shortId(submission.learnerId)}`}\n </p>\n <p className=\"truncate text-xs text-text-tertiary\" title={submission.assessmentId}>\n {assessment?.title ?? `Assessment ${shortId(submission.assessmentId)}`} · Attempt #\n {submission.attempt} · {formatDateTime(submission.submittedAt)}\n </p>\n </div>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={handleSuggest}\n disabled={suggestGrade.isPending}\n className=\"shrink-0\"\n >\n <Sparkles className=\"mr-1.5 h-3.5 w-3.5\" />\n {suggestGrade.isPending ? 'Asking AI…' : 'Suggest'}\n </Button>\n </div>\n\n {/* Side-by-side: learner's answers | grading form (stacks on small screens). */}\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <div className=\"min-w-0\">\n <p className=\"mb-1.5 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Learner&rsquo;s answers\n </p>\n <div className=\"max-h-72 overflow-y-auto pr-1\">\n <AnswersView answers={submission.answers} questions={assessment?.questions} />\n </div>\n </div>\n\n <div className=\"grid content-start gap-3\">\n {suggestion && (\n <div className=\"rounded-lg border border-border-seam/70 bg-bg-surface/60 p-2.5\">\n <div className=\"flex items-center gap-2 text-xs\">\n <Sparkles className=\"h-3.5 w-3.5 shrink-0 text-[var(--color-accent-teal)]\" />\n <span className=\"text-text-secondary\">\n AI confidence:{' '}\n <span\n className={`tabular-nums ${\n isLowConfidence(suggestion.confidence)\n ? 'font-semibold text-status-warning-text'\n : 'font-medium text-text-primary'\n }`}\n >\n {Math.round(suggestion.confidence * 100)}%\n </span>\n </span>\n </div>\n {isLowConfidence(suggestion.confidence) && (\n <p className=\"mt-1.5 flex items-start gap-1.5 text-xs text-status-warning-text\">\n <TriangleAlert className=\"mt-0.5 h-3.5 w-3.5 shrink-0\" />\n Low AI confidence — review this suggestion especially carefully before saving.\n </p>\n )}\n {suggestion.criterionScores.length > 0 && (\n <ul className=\"mt-1.5 flex flex-col gap-0.5 border-l border-border-seam/70 pl-2.5 text-xs text-text-tertiary\">\n {suggestion.criterionScores.map((c) => (\n <li key={c.criterionId}>\n <span className=\"font-medium text-text-secondary\">{c.title}</span>{' '}\n <span className=\"tabular-nums\">\n {c.suggestedPoints}/{c.maxPoints}\n </span>\n {c.comment ? ` — ${c.comment}` : ''}\n </li>\n ))}\n </ul>\n )}\n </div>\n )}\n\n <div className=\"grid gap-3 sm:grid-cols-[7rem_auto_1fr] sm:items-start\">\n <div className=\"grid gap-1\">\n <Label htmlFor={`score-${submission.id}`} className=\"text-xs\">\n Score\n </Label>\n <Input\n id={`score-${submission.id}`}\n type=\"number\"\n min=\"0\"\n value={scoreInput}\n onChange={(e) => setScoreInput(e.target.value)}\n placeholder=\"0\"\n className=\"h-9\"\n />\n </div>\n <div className=\"grid gap-1\">\n <Label className=\"text-xs\">Result</Label>\n <div className=\"flex h-9 items-center gap-2\">\n <Switch checked={passed} onCheckedChange={setPassed} />\n <span className=\"text-xs text-text-secondary\">\n {passed ? 'Passed' : 'Not passed'}\n </span>\n </div>\n </div>\n <div className=\"grid gap-1\">\n <Label htmlFor={`feedback-${submission.id}`} className=\"text-xs\">\n Feedback (optional)\n </Label>\n <Textarea\n id={`feedback-${submission.id}`}\n rows={3}\n value={feedback}\n onChange={(e) => setFeedback(e.target.value)}\n className=\"text-sm\"\n />\n <select\n aria-label={`Insert feedback template for learner ${shortId(submission.learnerId)}`}\n className=\"h-8 rounded-lg border border-border-seam bg-bg-surface px-2 text-xs text-text-secondary\"\n value=\"\"\n onChange={(e) => {\n const template = FEEDBACK_TEMPLATES.find((t) => t.text === e.target.value);\n if (template) handleInsertTemplate(template.text);\n }}\n >\n <option value=\"\">Insert template…</option>\n {FEEDBACK_TEMPLATES.map((t) => (\n <option key={t.label} value={t.text}>\n {t.label}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n\n <div className=\"flex justify-end\">\n <BuiPermissionButton\n resource=\"buildmyiq-submission\"\n action=\"update\"\n deniedBehavior=\"hide\"\n >\n <Button size=\"sm\" onClick={handleSave} disabled={gradeSubmission.isPending}>\n {gradeSubmission.isPending ? 'Saving…' : 'Save grade'}\n </Button>\n </BuiPermissionButton>\n </div>\n </div>\n </div>\n </GlassCard>\n );\n};\n\nexport const BulkGradingPage: FC = () => {\n const { currentUser, navigateTo } = useBuildmyiq();\n const pager = useCursorPager();\n const { data, isLoading, isError, refetch } = useBuiSubmissions({\n status: 'SUBMITTED',\n cursor: pager.cursor,\n limit: PAGE_SIZE,\n });\n\n const rows = useMemo<BuiSubmission[]>(() => data?.items ?? [], [data]);\n const [focusedIndex, setFocusedIndex] = useState(0);\n const rowActions = useRef(new Map<string, RowActions>());\n\n const registerActions = useCallback((submissionId: string, actions: RowActions | null) => {\n if (actions) rowActions.current.set(submissionId, actions);\n else rowActions.current.delete(submissionId);\n }, []);\n\n // Clamp to the visible page bounds at render time (no effect needed).\n const safeFocusedIndex = rows.length === 0 ? 0 : Math.min(focusedIndex, rows.length - 1);\n\n // Scroll the focused row into view when it changes.\n useEffect(() => {\n const focused = rows[safeFocusedIndex];\n if (!focused) return;\n document\n .getElementById(`bulk-grade-row-${focused.id}`)\n ?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });\n }, [safeFocusedIndex, rows]);\n\n // SpeedGrader-style keyboard shortcuts. Ignored entirely while the user is\n // typing in a form field, so Enter in the feedback box stays a newline.\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (rows.length === 0) return;\n const target = e.target as HTMLElement | null;\n if (\n target &&\n (target.tagName === 'INPUT' ||\n target.tagName === 'TEXTAREA' ||\n target.tagName === 'SELECT' ||\n target.isContentEditable)\n ) {\n return;\n }\n if (e.ctrlKey || e.metaKey || e.altKey) return;\n\n const move = (delta: number) => {\n setFocusedIndex((prev) => Math.min(Math.max(prev + delta, 0), rows.length - 1));\n };\n const focusedSubmission = rows[safeFocusedIndex];\n const actions = focusedSubmission ? rowActions.current.get(focusedSubmission.id) : undefined;\n\n if (e.key === 'j' || e.key === 'ArrowDown') {\n e.preventDefault();\n move(1);\n } else if (e.key === 'k' || e.key === 'ArrowUp') {\n e.preventDefault();\n move(-1);\n } else if (e.key === 's' && actions) {\n e.preventDefault();\n actions.suggest();\n } else if (e.key === 'Enter' && actions) {\n e.preventDefault();\n actions.save();\n }\n };\n window.addEventListener('keydown', onKeyDown);\n return () => window.removeEventListener('keydown', onKeyDown);\n }, [rows, safeFocusedIndex]);\n\n return (\n <PageLayout\n title=\"Bulk grade\"\n description=\"Work through every awaiting-grade submission in one pass — read the answers, suggest, adjust, save.\"\n actions={\n <Button variant=\"ghost\" onClick={() => navigateTo('submissions')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Back to grading queue\n </Button>\n }\n >\n <div className=\"flex flex-col gap-4\">\n {isLoading ? (\n <LoadingView label=\"Loading submissions awaiting grade…\" />\n ) : isError ? (\n <ErrorView\n title=\"Couldn't load submissions\"\n message=\"The grading queue failed to load.\"\n onRetry={() => refetch()}\n />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<ClipboardList className=\"h-10 w-10\" />}\n title=\"Nothing to grade\"\n description=\"Every submission is already graded, or none have been submitted yet.\"\n action={\n <Button variant=\"outline\" onClick={() => navigateTo('submissions')}>\n Back to grading queue\n </Button>\n }\n />\n ) : (\n <>\n <p className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-text-tertiary\">\n <Keyboard className=\"h-3.5 w-3.5 shrink-0\" />\n <span>\n Shortcuts: <kbd className=\"font-semibold\">J</kbd>/\n <kbd className=\"font-semibold\">↓</kbd> next · <kbd className=\"font-semibold\">K</kbd>\n /<kbd className=\"font-semibold\">↑</kbd> previous ·{' '}\n <kbd className=\"font-semibold\">S</kbd> suggest AI ·{' '}\n <kbd className=\"font-semibold\">Enter</kbd> save focused — inactive while typing in a\n field.\n </span>\n </p>\n <div className=\"flex flex-col gap-3\">\n {rows.map((s, i) => (\n <BulkGradeRow\n key={s.id}\n submission={s}\n isMe={s.learnerId === currentUser?.id}\n isFocused={i === safeFocusedIndex}\n registerActions={registerActions}\n />\n ))}\n </div>\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={rows.length}\n hasMore={data?.pageInfo?.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target, data?.pageInfo?.cursor)}\n />\n </>\n )}\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
@@ -286,7 +286,10 @@ var X = 24, ie = [
286
286
  className: "font-normal text-text-secondary",
287
287
  children: "(read-only)"
288
288
  })]
289
- }), /* @__PURE__ */ z(w, { answers: e.answers })] }) : /* @__PURE__ */ B("div", {
289
+ }), /* @__PURE__ */ z(w, {
290
+ answers: e.answers,
291
+ questions: p?.questions
292
+ })] }) : /* @__PURE__ */ B("div", {
290
293
  className: "grid grid-cols-2 gap-4 rounded-xl bg-bg-sunken/60 p-4",
291
294
  children: [
292
295
  /* @__PURE__ */ z($, {
@@ -1 +1 @@
1
- {"version":3,"file":"SubmissionsPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/submissions/SubmissionsPage.tsx"],"sourcesContent":["/**\n * SubmissionsPage — the workspace-wide grading queue.\n * @module buildmyiq/pages/submissions\n *\n * The educator surface for assessment submissions. Reads every submission in the\n * workspace via `useBuiSubmissions` (no `assessmentId` → all assessments),\n * filterable by status, and paginated by cursor. Clicking a row opens a grading\n * slide-over — a right-hand panel on desktop, a bottom-sheet on mobile — that\n * shows the learner's answers read-only (rendered safely from the `answers` JSON\n * blob) under a Submission / Details tab pair, plus a grading form\n * (`useBuiGradeSubmission`: score, passed, feedback). Already-graded rows show\n * their result read-only with a \"Re-grade\" affordance.\n *\n * Every number is derived from live `bui*` data; the KPI row is explicitly scoped\n * to the current page (\"this page\") so it never overstates workspace totals.\n * Learner identity is shown honestly: the signed-in educator's own attempts get\n * their real name + \"You\"; everyone else is a deterministic initials avatar keyed\n * off the opaque learner id — never a fabricated name or stock photo.\n *\n * The open submission's assessment is resolved (`buiAssessment`) so the panel can\n * name what is being graded instead of showing an opaque id, and — when that\n * assessment is made up ENTIRELY of objective questions — offer the deterministic\n * `buiAutoScoreSubmission` pass. The id remains the honest fallback whenever the\n * assessment read has not landed.\n */\n\nimport { useEffect, useMemo, useState } from 'react';\nimport type { FC, ReactNode } from 'react';\nimport { useParams } from 'react-router';\nimport {\n ClipboardList,\n Clock,\n CheckCircle2,\n Undo2,\n GraduationCap,\n BookOpen,\n Sparkles,\n Layers,\n Wand2,\n} from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Switch,\n Sheet,\n SheetContent,\n SheetHeader,\n SheetTitle,\n SheetDescription,\n SheetFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { useMediaQuery } from '@burdenoff/fe-libs/shared/hooks';\nimport {\n PageLayout,\n MetricsCard,\n StatusBadge,\n DataTable,\n Tabs,\n Pagination,\n EmptyState,\n LoadingView,\n ErrorView,\n Avatar,\n type Column,\n type TabItem,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiSubmissions,\n useBuiSubmission,\n useBuiAssessment,\n useBuiAutoScoreSubmission,\n useBuiGradeSubmission,\n useBuiSuggestGrade,\n useBuiReviewGradeSuggestion,\n useCursorPager,\n} from '../../hooks';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { formatDate, formatDateTime, formatEnumLabel } from '../../utils/formatters';\nimport type {\n BuiSubmission,\n BuiSubmissionStatus,\n BuiGradeSuggestion,\n BuiQuestionType,\n} from '../../types';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\nimport { AnswersView } from './AnswersView';\nimport { isLowConfidence } from './aiGrading';\n\nconst PAGE_SIZE = 24;\n\ntype StatusFilter = BuiSubmissionStatus | 'ALL';\ntype PanelTab = 'submission' | 'details';\n\nconst STATUS_TABS: TabItem<StatusFilter>[] = [\n { value: 'ALL', label: 'All' },\n { value: 'SUBMITTED', label: 'Submitted' },\n { value: 'GRADING', label: 'Grading' },\n { value: 'GRADED', label: 'Graded' },\n { value: 'RETURNED', label: 'Returned' },\n];\n\nconst PANEL_TABS: TabItem<PanelTab>[] = [\n { value: 'submission', label: 'Submission' },\n { value: 'details', label: 'Details' },\n];\n\n/** Truncate an opaque id for compact display, keeping the full value in `title`. */\nconst shortId = (id: string): string => (id.length > 10 ? `${id.slice(0, 8)}…` : id);\n\n/**\n * Question types `buiAutoScoreSubmission` can grade deterministically against an\n * answer key. The svc refuses the whole submission if ANY question falls outside\n * this set, so the action is only offered when every question is in it.\n */\nconst OBJECTIVE_QUESTION_TYPES: ReadonlySet<BuiQuestionType> = new Set<BuiQuestionType>([\n 'SINGLE_CHOICE',\n 'MULTIPLE_CHOICE',\n 'TRUE_FALSE',\n]);\n\n/** Time-only rendering (e.g. \"10:24 AM\") reusing the locale-aware date formatter. */\nconst formatClock = (date: string | null | undefined): string =>\n formatDate(date, {\n day: undefined,\n month: undefined,\n year: undefined,\n hour: 'numeric',\n minute: '2-digit',\n });\n\n/** A labelled read-only field for the submission meta / graded-result panels. */\nconst MetaField: FC<{ label: string; children: ReactNode }> = ({ label, children }) => (\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">{label}</p>\n <div className=\"mt-0.5 text-sm text-text-primary\">{children}</div>\n </div>\n);\n\n/**\n * AI-assisted grading (BOFF-4429). Calls `buiSuggestGrade`, which proposes a\n * score + per-question feedback for the submission's open-ended questions\n * (deterministic scoring for objective ones folded in) but NEVER writes the\n * submission — this panel only offers to PREFILL the grade form below with\n * the suggestion; the instructor still reviews and hits \"Grade submission\"\n * to commit it. Fails honestly (visible error, no silent fallback) when the\n * backend has no LLM provider configured or the assessment has no\n * open-ended questions to assist with.\n */\nconst AiGradeSuggestionPanel: FC<{\n submissionId: string;\n onUseSuggestion: (proposedScore: number, feedback: string) => void;\n}> = ({ submissionId, onUseSuggestion }) => {\n const suggestGrade = useBuiSuggestGrade();\n const reviewSuggestion = useBuiReviewGradeSuggestion();\n const [suggestion, setSuggestion] = useState<BuiGradeSuggestion | null>(null);\n const [dismissed, setDismissed] = useState(false);\n\n const handleSuggest = async () => {\n setDismissed(false);\n try {\n const result = await suggestGrade.mutateAsync(submissionId);\n setSuggestion(result);\n } catch {\n // error surfaced below via suggestGrade.isError / suggestGrade.error\n }\n };\n\n const handleUse = () => {\n if (!suggestion) return;\n const feedback = suggestion.perQuestionFeedback\n .map((f, i) => `Q${i + 1}: ${f.feedback}`)\n .join('\\n');\n onUseSuggestion(Math.round(suggestion.proposedScore), feedback);\n reviewSuggestion.mutate({ id: suggestion.id, outcome: 'ACCEPTED' });\n setDismissed(true);\n };\n\n const handleDismiss = () => {\n if (suggestion) reviewSuggestion.mutate({ id: suggestion.id, outcome: 'REJECTED' });\n setSuggestion(null);\n setDismissed(true);\n };\n\n if (dismissed && !suggestion) {\n return null;\n }\n\n if (!suggestion) {\n return (\n <div className=\"mb-4 rounded-xl border border-dashed border-border-seam bg-bg-sunken/40 p-3\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={handleSuggest}\n disabled={suggestGrade.isPending}\n >\n <Sparkles className=\"mr-1.5 h-4 w-4\" />\n {suggestGrade.isPending ? 'Asking the AI grader…' : 'Suggest with AI'}\n </Button>\n {suggestGrade.isError && (\n <p className=\"mt-2 text-xs text-status-error-text\">\n {suggestGrade.error instanceof Error\n ? suggestGrade.error.message\n : 'The AI grading assistant is unavailable right now.'}\n </p>\n )}\n <p className=\"mt-1.5 text-xs text-text-tertiary\">\n Proposes a score + feedback for open-ended answers. You always review before it counts.\n </p>\n </div>\n );\n }\n\n return (\n <div className=\"mb-4 rounded-xl border border-[var(--color-accent-teal)]/30 bg-[color-mix(in_srgb,var(--color-accent-teal)_8%,transparent)] p-3.5\">\n <div className=\"mb-2 flex items-center gap-2\">\n <Sparkles className=\"h-4 w-4 shrink-0 text-[var(--color-accent-teal)]\" />\n <p className=\"text-sm font-semibold text-text-primary\">\n AI-suggested score:{' '}\n <span className=\"tabular-nums\">{suggestion.proposedScore.toFixed(1)}</span>\n </p>\n <span\n className={`ml-auto text-xs ${\n isLowConfidence(suggestion.confidence)\n ? 'font-semibold text-status-warning-text'\n : 'text-text-tertiary'\n }`}\n >\n {Math.round(suggestion.confidence * 100)}% confidence\n </span>\n </div>\n {isLowConfidence(suggestion.confidence) && (\n <p className=\"mb-2 rounded-lg border border-status-warning-border bg-status-warning-bg-subtle p-2 text-xs text-status-warning-text\">\n Low AI confidence — review this suggestion especially carefully before using it.\n </p>\n )}\n {suggestion.perQuestionFeedback.length > 0 && (\n <ul className=\"mb-3 flex flex-col gap-1.5\">\n {suggestion.perQuestionFeedback.map((f, i) => (\n <li key={f.questionId} className=\"text-xs text-text-secondary\">\n <span className=\"font-medium text-text-primary\">Q{i + 1}</span>\n {f.suggestedPoints != null && (\n <span className=\"tabular-nums\"> ({f.suggestedPoints} pts)</span>\n )}\n : {f.feedback}\n {f.criterionScores && f.criterionScores.length > 0 && (\n <ul className=\"mt-1 flex flex-col gap-0.5 border-l border-border-seam/70 pl-3\">\n {f.criterionScores.map((c) => (\n <li key={c.criterionId} className=\"text-text-tertiary\">\n <span className=\"font-medium text-text-secondary\">{c.title}</span>{' '}\n <span className=\"tabular-nums\">\n {c.suggestedPoints}/{c.maxPoints}\n </span>\n {c.comment ? ` — ${c.comment}` : ''}\n </li>\n ))}\n </ul>\n )}\n </li>\n ))}\n </ul>\n )}\n <p className=\"mb-3 text-xs text-text-tertiary\">\n This is a suggestion, not a grade — nothing is saved until you review it and click\n &ldquo;Grade submission&rdquo; below.\n </p>\n <div className=\"flex gap-2\">\n <Button type=\"button\" size=\"sm\" onClick={handleUse}>\n Use this suggestion\n </Button>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={handleDismiss}>\n Dismiss\n </Button>\n </div>\n </div>\n );\n};\n\n/**\n * Deterministic auto-scoring for an entirely-objective assessment\n * (`buiAutoScoreSubmission`): the svc compares the learner's answers to each\n * question's answer key — no model and no human judgement. Unlike the AI\n * suggestion panel above, this DOES write the grade, so it is a deliberate,\n * clearly-labelled action rather than a prefill.\n */\nconst AutoScorePanel: FC<{ submissionId: string; questionCount: number }> = ({\n submissionId,\n questionCount,\n}) => {\n const tr = useTr();\n const autoScore = useBuiAutoScoreSubmission();\n\n return (\n <div className=\"mb-4 rounded-xl border border-border-seam bg-bg-sunken/40 p-3\">\n <BuiPermissionButton resource=\"buildmyiq-submission\" action=\"update\" deniedBehavior=\"hide\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => autoScore.mutate(submissionId)}\n disabled={autoScore.isPending}\n >\n <Wand2 className=\"mr-1.5 h-4 w-4\" />\n {autoScore.isPending\n ? tr('buildmyiq.grading.autoScore.pending', 'Scoring…')\n : tr('buildmyiq.grading.autoScore.action', 'Auto-score')}\n </Button>\n </BuiPermissionButton>\n {autoScore.isError && (\n <p className=\"mt-2 text-xs text-status-error-text\">\n {autoScore.error instanceof Error\n ? autoScore.error.message\n : tr('buildmyiq.grading.autoScore.error', 'Auto-scoring failed.')}\n </p>\n )}\n <p className=\"mt-1.5 text-xs text-text-tertiary\">\n {tr(\n 'buildmyiq.grading.autoScore.help',\n 'All {{count}} questions are objective, so this attempt can be graded against the answer key. This saves the grade immediately.',\n { count: questionCount }\n )}\n </p>\n </div>\n );\n};\n\ninterface GradingPanelBodyProps {\n submission: BuiSubmission;\n currentUser: ReturnType<typeof useBuildmyiq>['currentUser'];\n meName: string | undefined;\n isMe: (learnerId: string) => boolean;\n learnerLabel: (learnerId: string) => string;\n gradeSubmission: ReturnType<typeof useBuiGradeSubmission>;\n onClose: () => void;\n}\n\n/**\n * The grading slide-over's content, for a single fixed `submission`. Rendered\n * with `key={submission.id}` by the parent so switching to a different\n * submission remounts this component — every local (editable) field below\n * initializes fresh from `submission` with no synchronizing effect needed.\n */\nconst GradingPanelBody: FC<GradingPanelBodyProps> = ({\n submission,\n currentUser,\n meName,\n isMe,\n learnerLabel,\n gradeSubmission,\n onClose,\n}) => {\n const tr = useTr();\n // Names WHAT is being graded. Falls back to the opaque id (never a guess)\n // while this read is in flight or if the assessment is not readable.\n const assessmentQuery = useBuiAssessment(submission.assessmentId);\n const assessment = assessmentQuery.data ?? null;\n const assessmentLabel = assessment?.title ?? shortId(submission.assessmentId);\n const questions = assessment?.questions ?? [];\n // Only offer the deterministic pass when we KNOW every question is objective.\n const canAutoScore =\n questions.length > 0 && questions.every((q) => OBJECTIVE_QUESTION_TYPES.has(q.type));\n\n const [panelTab, setPanelTab] = useState<PanelTab>('submission');\n const [editMode, setEditMode] = useState(false);\n const [scoreInput, setScoreInput] = useState(\n submission.score != null ? String(submission.score) : ''\n );\n const [passed, setPassed] = useState(submission.passed ?? false);\n const [feedback, setFeedback] = useState(submission.feedback ?? '');\n const [formError, setFormError] = useState<string | null>(null);\n\n const gradingAllowed = submission.status === 'SUBMITTED' || submission.status === 'GRADING';\n const alreadyGraded = submission.status === 'GRADED' || submission.status === 'RETURNED';\n const notSubmitted = submission.status === 'IN_PROGRESS';\n const showGradeForm = gradingAllowed || editMode;\n\n const handleGrade = async () => {\n const trimmed = scoreInput.trim();\n const scoreNum = Number(trimmed);\n if (trimmed === '' || Number.isNaN(scoreNum)) {\n setFormError('Enter a numeric score.');\n return;\n }\n if (scoreNum < 0) {\n setFormError('Score cannot be negative.');\n return;\n }\n try {\n await gradeSubmission.mutateAsync({\n id: submission.id,\n input: { score: scoreNum, passed, feedback: feedback.trim() || undefined },\n });\n onClose();\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to save grade.');\n }\n };\n\n return (\n <>\n <SheetHeader className=\"border-b border-border-seam p-5 pr-12\">\n <div className=\"flex items-center gap-3\">\n <Avatar\n size=\"md\"\n name={isMe(submission.learnerId) ? meName : undefined}\n seed={submission.learnerId}\n src={isMe(submission.learnerId) ? currentUser?.avatar : undefined}\n />\n <div className=\"min-w-0\">\n <SheetTitle className=\"truncate text-lg\">\n {learnerLabel(submission.learnerId)}\n </SheetTitle>\n <SheetDescription className=\"truncate text-xs\" title={submission.assessmentId}>\n {assessmentLabel}\n {assessment ? ` · ${formatEnumLabel(assessment.type)}` : null}\n </SheetDescription>\n </div>\n </div>\n <p className=\"text-xs text-text-secondary\">\n Attempt #{submission.attempt} · Submitted {formatDateTime(submission.submittedAt)}\n </p>\n <div className=\"pt-1\">\n <StatusBadge status={submission.status} />\n </div>\n </SheetHeader>\n\n <div className=\"flex-1 overflow-y-auto px-5 py-4\">\n <Tabs items={PANEL_TABS} value={panelTab} onChange={setPanelTab} className=\"mb-4\" />\n\n {panelTab === 'submission' ? (\n <div>\n <p className=\"mb-2 text-sm font-semibold text-text-primary\">\n Answers <span className=\"font-normal text-text-secondary\">(read-only)</span>\n </p>\n <AnswersView answers={submission.answers} />\n </div>\n ) : (\n <div className=\"grid grid-cols-2 gap-4 rounded-xl bg-bg-sunken/60 p-4\">\n <MetaField label=\"Status\">\n <StatusBadge status={submission.status} />\n </MetaField>\n <MetaField label=\"Attempt\">\n <span className=\"tabular-nums\">#{submission.attempt}</span>\n </MetaField>\n <MetaField label=\"Submitted\">{formatDateTime(submission.submittedAt)}</MetaField>\n <MetaField label=\"Assessment\">\n <span className=\"block truncate\" title={submission.assessmentId}>\n {assessmentLabel}\n </span>\n {assessment && (\n <span className=\"text-xs text-text-secondary\">\n {tr('buildmyiq.grading.passMark', 'Pass ≥ {{score}}%', {\n score: assessment.passingScore,\n })}\n </span>\n )}\n </MetaField>\n {submission.enrollmentId && (\n <MetaField label=\"Enrollment\">\n <span className=\"font-mono text-xs\" title={submission.enrollmentId}>\n {shortId(submission.enrollmentId)}\n </span>\n </MetaField>\n )}\n {submission.gradedAt && (\n <MetaField label=\"Graded\">{formatDateTime(submission.gradedAt)}</MetaField>\n )}\n </div>\n )}\n\n {/* Grade / result — persistent below the tab content. */}\n <div className=\"mt-6 border-t border-border-seam pt-5\">\n <p className=\"mb-3 text-sm font-semibold text-text-primary\">Grade</p>\n\n {notSubmitted && (\n <p className=\"rounded-lg bg-bg-sunken/60 px-4 py-3 text-sm text-text-secondary\">\n This attempt is still in progress — there is nothing to grade until the learner\n submits.\n </p>\n )}\n\n {alreadyGraded && !editMode && (\n <div className=\"rounded-xl border border-border-seam p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"grid grid-cols-2 gap-4\">\n <MetaField label=\"Score\">\n <span className=\"tabular-nums font-semibold\">{submission.score ?? '—'}</span>\n </MetaField>\n <MetaField label=\"Result\">\n {submission.passed == null ? (\n <span className=\"text-text-secondary\">—</span>\n ) : (\n <StatusBadge status={submission.passed ? 'GRADED' : 'RETURNED'} />\n )}\n </MetaField>\n </div>\n <Button variant=\"outline\" size=\"sm\" onClick={() => setEditMode(true)}>\n Re-grade\n </Button>\n </div>\n <div className=\"mt-3\">\n <MetaField label=\"Feedback\">\n {submission.feedback ? (\n <span className=\"whitespace-pre-wrap\">{submission.feedback}</span>\n ) : (\n <span className=\"text-text-secondary\">No feedback provided.</span>\n )}\n </MetaField>\n </div>\n </div>\n )}\n\n {showGradeForm && (\n <div className=\"grid gap-4\">\n {canAutoScore ? (\n <AutoScorePanel submissionId={submission.id} questionCount={questions.length} />\n ) : (\n <AiGradeSuggestionPanel\n submissionId={submission.id}\n onUseSuggestion={(proposedScore, aiFeedback) => {\n setScoreInput(String(proposedScore));\n setFeedback(aiFeedback);\n }}\n />\n )}\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"grade-score\">Score</Label>\n <Input\n id=\"grade-score\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={scoreInput}\n onChange={(e) => setScoreInput(e.target.value)}\n placeholder=\"e.g. 85\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"grade-passed\">Result</Label>\n <div className=\"flex h-10 items-center gap-3\">\n <Switch id=\"grade-passed\" checked={passed} onCheckedChange={setPassed} />\n <span className=\"text-sm text-text-secondary\">\n {passed ? 'Passed' : 'Not passed'}\n </span>\n </div>\n </div>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"grade-feedback\">Feedback</Label>\n <Textarea\n id=\"grade-feedback\"\n value={feedback}\n onChange={(e) => setFeedback(e.target.value)}\n placeholder=\"Notes, corrections or encouragement for the learner (optional).\"\n rows={4}\n />\n </div>\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n )}\n </div>\n </div>\n\n <SheetFooter className=\"flex-row justify-end gap-3 border-t border-border-seam p-4\">\n <Button variant=\"outline\" onClick={onClose}>\n {showGradeForm ? 'Cancel' : 'Close'}\n </Button>\n {showGradeForm && (\n <BuiPermissionButton\n resource=\"buildmyiq-submission\"\n action=\"update\"\n deniedBehavior=\"hide\"\n >\n <Button onClick={handleGrade} disabled={gradeSubmission.isPending}>\n {gradeSubmission.isPending\n ? 'Saving…'\n : alreadyGraded\n ? 'Update grade'\n : 'Grade submission'}\n </Button>\n </BuiPermissionButton>\n )}\n </SheetFooter>\n </>\n );\n};\n\nexport const SubmissionsPage: FC = () => {\n const { currentUser, navigateTo } = useBuildmyiq();\n const { submissionId } = useParams<{ submissionId?: string }>();\n const isDesktop = useMediaQuery('(min-width: 768px)');\n const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL');\n const pager = useCursorPager();\n\n const { data, isLoading, isError, refetch } = useBuiSubmissions({\n status: statusFilter === 'ALL' ? undefined : statusFilter,\n cursor: pager.cursor,\n limit: PAGE_SIZE,\n });\n const gradeSubmission = useBuiGradeSubmission();\n\n // Deep-link support: /buildmyiq/submissions/:submissionId opens the grading\n // panel directly, independent of which page of the list is currently loaded.\n // The URL is the single source of truth for \"which submission is open\" —\n // there is no separate `selected` state to keep in sync with it.\n const deepLink = useBuiSubmission(submissionId);\n\n const rows = useMemo<BuiSubmission[]>(() => data?.items ?? [], [data]);\n\n const activeId = submissionId ?? null;\n const activeSubmission = useMemo<BuiSubmission | null>(() => {\n if (!activeId) return null;\n return (\n rows.find((r) => r.id === activeId) ?? (deepLink.data?.id === activeId ? deepLink.data : null)\n );\n }, [activeId, rows, deepLink.data]);\n\n // The signed-in educator's own display name (used only for their own rows).\n const meName = useMemo<string | undefined>(() => {\n if (!currentUser) return undefined;\n const full = [currentUser.firstName, currentUser.lastName].filter(Boolean).join(' ').trim();\n return currentUser.name?.trim() || full || currentUser.username || currentUser.email;\n }, [currentUser]);\n\n // Counts are scoped to the loaded page — labelled \"this page\" to stay honest.\n const counts = useMemo(() => {\n const acc = { total: rows.length, awaiting: 0, graded: 0, returned: 0 };\n for (const s of rows) {\n if (s.status === 'SUBMITTED' || s.status === 'GRADING') acc.awaiting += 1;\n else if (s.status === 'GRADED') acc.graded += 1;\n else if (s.status === 'RETURNED') acc.returned += 1;\n }\n return acc;\n }, [rows]);\n\n const isMe = (learnerId: string): boolean => learnerId === currentUser?.id;\n const learnerLabel = (learnerId: string): string =>\n isMe(learnerId) ? 'You' : `Learner ${shortId(learnerId)}`;\n\n /** Row click: push the deep-linkable URL; the panel opens from the route param. */\n const selectSubmission = (s: BuiSubmission) => navigateTo(`submissions/${s.id}`);\n const closePanel = () => navigateTo('submissions');\n\n // If the id in the URL doesn't resolve to a real submission in this\n // workspace (wrong tenant, typo, or one that was later deleted), fall back\n // to the plain list once the lookup has definitively failed. This effect\n // only notifies the router — it never sets this component's own state, so\n // it can't cascade into an extra render loop.\n useEffect(() => {\n if (!activeId || activeSubmission) return;\n if (!deepLink.isLoading && (deepLink.isError || deepLink.data === null)) {\n navigateTo('submissions');\n }\n }, [activeId, activeSubmission, deepLink.isLoading, deepLink.isError, deepLink.data, navigateTo]);\n\n const handleFilterChange = (value: StatusFilter) => {\n setStatusFilter(value);\n pager.reset();\n };\n\n const columns: Column<BuiSubmission>[] = [\n {\n key: 'learner',\n header: 'Learner',\n sortValue: (s) => (isMe(s.learnerId) ? '' : s.learnerId),\n render: (s) => (\n <div className=\"flex items-center gap-3\">\n <Avatar\n size=\"sm\"\n name={isMe(s.learnerId) ? meName : undefined}\n seed={s.learnerId}\n src={isMe(s.learnerId) ? currentUser?.avatar : undefined}\n />\n <div className=\"min-w-0\">\n <span className=\"block truncate font-medium text-text-primary\" title={s.learnerId}>\n {isMe(s.learnerId) ? 'You' : shortId(s.learnerId)}\n </span>\n {isMe(s.learnerId) && meName && (\n <span className=\"block truncate text-xs text-text-secondary\">{meName}</span>\n )}\n </div>\n </div>\n ),\n },\n {\n key: 'assessment',\n header: 'Assessment',\n hideOnMobile: true,\n render: (s) => (\n <span className=\"inline-flex items-center gap-2 text-text-secondary\">\n <BookOpen className=\"h-4 w-4 shrink-0 opacity-70\" />\n <span className=\"font-mono text-xs\" title={s.assessmentId}>\n {shortId(s.assessmentId)}\n </span>\n </span>\n ),\n },\n {\n key: 'attempt',\n header: 'Attempt',\n align: 'right',\n hideOnMobile: true,\n sortValue: (s) => s.attempt,\n render: (s) => <span className=\"tabular-nums text-text-secondary\">#{s.attempt}</span>,\n },\n {\n key: 'status',\n header: 'Status',\n sortValue: (s) => s.status,\n render: (s) => <StatusBadge status={s.status} />,\n },\n {\n key: 'score',\n header: 'Score',\n align: 'right',\n sortValue: (s) => s.score ?? -1,\n render: (s) =>\n s.score == null ? (\n <span className=\"text-text-secondary\">—</span>\n ) : (\n <span className=\"tabular-nums font-semibold text-text-primary\">{s.score}</span>\n ),\n },\n {\n key: 'submittedAt',\n header: 'Submitted',\n align: 'right',\n hideOnMobile: true,\n sortValue: (s) => s.submittedAt ?? s.createdAt,\n render: (s) => (\n <div className=\"whitespace-nowrap text-right\">\n <div className=\"text-text-primary\">{formatDate(s.submittedAt)}</div>\n {s.submittedAt && (\n <div className=\"text-xs text-text-secondary\">{formatClock(s.submittedAt)}</div>\n )}\n </div>\n ),\n },\n ];\n\n return (\n <PageLayout\n title=\"Grading\"\n description=\"Review and grade assessment submissions across every course in your workspace.\"\n titleGradient\n aurora\n actions={\n <Button variant=\"outline\" onClick={() => navigateTo('submissions/bulk')}>\n <Layers className=\"mr-1.5 h-4 w-4\" /> Bulk grade\n </Button>\n }\n >\n <div className=\"flex flex-col gap-6\">\n {/* KPI row — scoped to the current page. */}\n <div className=\"grid grid-cols-2 gap-4 xl:grid-cols-4\">\n <MetricsCard\n icon={<ClipboardList className=\"h-5 w-5\" />}\n value={counts.total}\n label=\"Submissions (this page)\"\n featured\n />\n <MetricsCard\n icon={<Clock className=\"h-5 w-5\" />}\n value={counts.awaiting}\n label=\"Awaiting grade (this page)\"\n accent=\"warning\"\n />\n <MetricsCard\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n value={counts.graded}\n label=\"Graded (this page)\"\n accent=\"success\"\n />\n <MetricsCard\n icon={<Undo2 className=\"h-5 w-5\" />}\n value={counts.returned}\n label=\"Returned (this page)\"\n />\n </div>\n\n <Tabs items={STATUS_TABS} value={statusFilter} onChange={handleFilterChange} />\n\n {isLoading ? (\n <LoadingView label=\"Loading submissions…\" />\n ) : isError ? (\n <ErrorView\n title=\"Couldn't load submissions\"\n message=\"The grading queue failed to load.\"\n onRetry={() => refetch()}\n />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<GraduationCap className=\"h-10 w-10\" />}\n title={statusFilter === 'ALL' ? 'No submissions yet' : 'Nothing in this status'}\n description={\n statusFilter === 'ALL'\n ? 'When learners submit assessments, their attempts will appear here for grading.'\n : 'No submissions currently match this status filter.'\n }\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(s) => s.id}\n onRowClick={selectSubmission}\n defaultSort={{ key: 'submittedAt', dir: 'desc' }}\n />\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={rows.length}\n hasMore={data?.pageInfo?.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target, data?.pageInfo?.cursor)}\n />\n </div>\n )}\n </div>\n\n {/* Grading slide-over — right panel on desktop, bottom-sheet on mobile. */}\n <Sheet\n open={activeId != null}\n onOpenChange={(open) => {\n if (!open) closePanel();\n }}\n >\n <SheetContent\n side={isDesktop ? 'right' : 'bottom'}\n className={\n isDesktop\n ? 'flex w-full max-w-none flex-col gap-0 p-0 sm:max-w-xl'\n : 'flex max-h-[92vh] w-full flex-col gap-0 rounded-t-2xl p-0'\n }\n >\n {activeSubmission ? (\n // Keyed by submission id: remounting on id change gives the panel\n // fresh local (editable) state for free, with no synchronizing effect.\n <GradingPanelBody\n key={activeSubmission.id}\n submission={activeSubmission}\n currentUser={currentUser}\n meName={meName}\n isMe={isMe}\n learnerLabel={learnerLabel}\n gradeSubmission={gradeSubmission}\n onClose={closePanel}\n />\n ) : activeId ? (\n <div className=\"flex min-h-[300px] items-center justify-center p-5\">\n <LoadingView label=\"Loading submission…\" />\n </div>\n ) : null}\n </SheetContent>\n </Sheet>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"SubmissionsPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/submissions/SubmissionsPage.tsx"],"sourcesContent":["/**\n * SubmissionsPage — the workspace-wide grading queue.\n * @module buildmyiq/pages/submissions\n *\n * The educator surface for assessment submissions. Reads every submission in the\n * workspace via `useBuiSubmissions` (no `assessmentId` → all assessments),\n * filterable by status, and paginated by cursor. Clicking a row opens a grading\n * slide-over — a right-hand panel on desktop, a bottom-sheet on mobile — that\n * shows the learner's answers read-only (rendered safely from the `answers` JSON\n * blob) under a Submission / Details tab pair, plus a grading form\n * (`useBuiGradeSubmission`: score, passed, feedback). Already-graded rows show\n * their result read-only with a \"Re-grade\" affordance.\n *\n * Every number is derived from live `bui*` data; the KPI row is explicitly scoped\n * to the current page (\"this page\") so it never overstates workspace totals.\n * Learner identity is shown honestly: the signed-in educator's own attempts get\n * their real name + \"You\"; everyone else is a deterministic initials avatar keyed\n * off the opaque learner id — never a fabricated name or stock photo.\n *\n * The open submission's assessment is resolved (`buiAssessment`) so the panel can\n * name what is being graded instead of showing an opaque id, and — when that\n * assessment is made up ENTIRELY of objective questions — offer the deterministic\n * `buiAutoScoreSubmission` pass. The id remains the honest fallback whenever the\n * assessment read has not landed.\n */\n\nimport { useEffect, useMemo, useState } from 'react';\nimport type { FC, ReactNode } from 'react';\nimport { useParams } from 'react-router';\nimport {\n ClipboardList,\n Clock,\n CheckCircle2,\n Undo2,\n GraduationCap,\n BookOpen,\n Sparkles,\n Layers,\n Wand2,\n} from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Switch,\n Sheet,\n SheetContent,\n SheetHeader,\n SheetTitle,\n SheetDescription,\n SheetFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { useMediaQuery } from '@burdenoff/fe-libs/shared/hooks';\nimport {\n PageLayout,\n MetricsCard,\n StatusBadge,\n DataTable,\n Tabs,\n Pagination,\n EmptyState,\n LoadingView,\n ErrorView,\n Avatar,\n type Column,\n type TabItem,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiSubmissions,\n useBuiSubmission,\n useBuiAssessment,\n useBuiAutoScoreSubmission,\n useBuiGradeSubmission,\n useBuiSuggestGrade,\n useBuiReviewGradeSuggestion,\n useCursorPager,\n} from '../../hooks';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { formatDate, formatDateTime, formatEnumLabel } from '../../utils/formatters';\nimport type {\n BuiSubmission,\n BuiSubmissionStatus,\n BuiGradeSuggestion,\n BuiQuestionType,\n} from '../../types';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\nimport { AnswersView } from './AnswersView';\nimport { isLowConfidence } from './aiGrading';\n\nconst PAGE_SIZE = 24;\n\ntype StatusFilter = BuiSubmissionStatus | 'ALL';\ntype PanelTab = 'submission' | 'details';\n\nconst STATUS_TABS: TabItem<StatusFilter>[] = [\n { value: 'ALL', label: 'All' },\n { value: 'SUBMITTED', label: 'Submitted' },\n { value: 'GRADING', label: 'Grading' },\n { value: 'GRADED', label: 'Graded' },\n { value: 'RETURNED', label: 'Returned' },\n];\n\nconst PANEL_TABS: TabItem<PanelTab>[] = [\n { value: 'submission', label: 'Submission' },\n { value: 'details', label: 'Details' },\n];\n\n/** Truncate an opaque id for compact display, keeping the full value in `title`. */\nconst shortId = (id: string): string => (id.length > 10 ? `${id.slice(0, 8)}…` : id);\n\n/**\n * Question types `buiAutoScoreSubmission` can grade deterministically against an\n * answer key. The svc refuses the whole submission if ANY question falls outside\n * this set, so the action is only offered when every question is in it.\n */\nconst OBJECTIVE_QUESTION_TYPES: ReadonlySet<BuiQuestionType> = new Set<BuiQuestionType>([\n 'SINGLE_CHOICE',\n 'MULTIPLE_CHOICE',\n 'TRUE_FALSE',\n]);\n\n/** Time-only rendering (e.g. \"10:24 AM\") reusing the locale-aware date formatter. */\nconst formatClock = (date: string | null | undefined): string =>\n formatDate(date, {\n day: undefined,\n month: undefined,\n year: undefined,\n hour: 'numeric',\n minute: '2-digit',\n });\n\n/** A labelled read-only field for the submission meta / graded-result panels. */\nconst MetaField: FC<{ label: string; children: ReactNode }> = ({ label, children }) => (\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">{label}</p>\n <div className=\"mt-0.5 text-sm text-text-primary\">{children}</div>\n </div>\n);\n\n/**\n * AI-assisted grading (BOFF-4429). Calls `buiSuggestGrade`, which proposes a\n * score + per-question feedback for the submission's open-ended questions\n * (deterministic scoring for objective ones folded in) but NEVER writes the\n * submission — this panel only offers to PREFILL the grade form below with\n * the suggestion; the instructor still reviews and hits \"Grade submission\"\n * to commit it. Fails honestly (visible error, no silent fallback) when the\n * backend has no LLM provider configured or the assessment has no\n * open-ended questions to assist with.\n */\nconst AiGradeSuggestionPanel: FC<{\n submissionId: string;\n onUseSuggestion: (proposedScore: number, feedback: string) => void;\n}> = ({ submissionId, onUseSuggestion }) => {\n const suggestGrade = useBuiSuggestGrade();\n const reviewSuggestion = useBuiReviewGradeSuggestion();\n const [suggestion, setSuggestion] = useState<BuiGradeSuggestion | null>(null);\n const [dismissed, setDismissed] = useState(false);\n\n const handleSuggest = async () => {\n setDismissed(false);\n try {\n const result = await suggestGrade.mutateAsync(submissionId);\n setSuggestion(result);\n } catch {\n // error surfaced below via suggestGrade.isError / suggestGrade.error\n }\n };\n\n const handleUse = () => {\n if (!suggestion) return;\n const feedback = suggestion.perQuestionFeedback\n .map((f, i) => `Q${i + 1}: ${f.feedback}`)\n .join('\\n');\n onUseSuggestion(Math.round(suggestion.proposedScore), feedback);\n reviewSuggestion.mutate({ id: suggestion.id, outcome: 'ACCEPTED' });\n setDismissed(true);\n };\n\n const handleDismiss = () => {\n if (suggestion) reviewSuggestion.mutate({ id: suggestion.id, outcome: 'REJECTED' });\n setSuggestion(null);\n setDismissed(true);\n };\n\n if (dismissed && !suggestion) {\n return null;\n }\n\n if (!suggestion) {\n return (\n <div className=\"mb-4 rounded-xl border border-dashed border-border-seam bg-bg-sunken/40 p-3\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={handleSuggest}\n disabled={suggestGrade.isPending}\n >\n <Sparkles className=\"mr-1.5 h-4 w-4\" />\n {suggestGrade.isPending ? 'Asking the AI grader…' : 'Suggest with AI'}\n </Button>\n {suggestGrade.isError && (\n <p className=\"mt-2 text-xs text-status-error-text\">\n {suggestGrade.error instanceof Error\n ? suggestGrade.error.message\n : 'The AI grading assistant is unavailable right now.'}\n </p>\n )}\n <p className=\"mt-1.5 text-xs text-text-tertiary\">\n Proposes a score + feedback for open-ended answers. You always review before it counts.\n </p>\n </div>\n );\n }\n\n return (\n <div className=\"mb-4 rounded-xl border border-[var(--color-accent-teal)]/30 bg-[color-mix(in_srgb,var(--color-accent-teal)_8%,transparent)] p-3.5\">\n <div className=\"mb-2 flex items-center gap-2\">\n <Sparkles className=\"h-4 w-4 shrink-0 text-[var(--color-accent-teal)]\" />\n <p className=\"text-sm font-semibold text-text-primary\">\n AI-suggested score:{' '}\n <span className=\"tabular-nums\">{suggestion.proposedScore.toFixed(1)}</span>\n </p>\n <span\n className={`ml-auto text-xs ${\n isLowConfidence(suggestion.confidence)\n ? 'font-semibold text-status-warning-text'\n : 'text-text-tertiary'\n }`}\n >\n {Math.round(suggestion.confidence * 100)}% confidence\n </span>\n </div>\n {isLowConfidence(suggestion.confidence) && (\n <p className=\"mb-2 rounded-lg border border-status-warning-border bg-status-warning-bg-subtle p-2 text-xs text-status-warning-text\">\n Low AI confidence — review this suggestion especially carefully before using it.\n </p>\n )}\n {suggestion.perQuestionFeedback.length > 0 && (\n <ul className=\"mb-3 flex flex-col gap-1.5\">\n {suggestion.perQuestionFeedback.map((f, i) => (\n <li key={f.questionId} className=\"text-xs text-text-secondary\">\n <span className=\"font-medium text-text-primary\">Q{i + 1}</span>\n {f.suggestedPoints != null && (\n <span className=\"tabular-nums\"> ({f.suggestedPoints} pts)</span>\n )}\n : {f.feedback}\n {f.criterionScores && f.criterionScores.length > 0 && (\n <ul className=\"mt-1 flex flex-col gap-0.5 border-l border-border-seam/70 pl-3\">\n {f.criterionScores.map((c) => (\n <li key={c.criterionId} className=\"text-text-tertiary\">\n <span className=\"font-medium text-text-secondary\">{c.title}</span>{' '}\n <span className=\"tabular-nums\">\n {c.suggestedPoints}/{c.maxPoints}\n </span>\n {c.comment ? ` — ${c.comment}` : ''}\n </li>\n ))}\n </ul>\n )}\n </li>\n ))}\n </ul>\n )}\n <p className=\"mb-3 text-xs text-text-tertiary\">\n This is a suggestion, not a grade — nothing is saved until you review it and click\n &ldquo;Grade submission&rdquo; below.\n </p>\n <div className=\"flex gap-2\">\n <Button type=\"button\" size=\"sm\" onClick={handleUse}>\n Use this suggestion\n </Button>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={handleDismiss}>\n Dismiss\n </Button>\n </div>\n </div>\n );\n};\n\n/**\n * Deterministic auto-scoring for an entirely-objective assessment\n * (`buiAutoScoreSubmission`): the svc compares the learner's answers to each\n * question's answer key — no model and no human judgement. Unlike the AI\n * suggestion panel above, this DOES write the grade, so it is a deliberate,\n * clearly-labelled action rather than a prefill.\n */\nconst AutoScorePanel: FC<{ submissionId: string; questionCount: number }> = ({\n submissionId,\n questionCount,\n}) => {\n const tr = useTr();\n const autoScore = useBuiAutoScoreSubmission();\n\n return (\n <div className=\"mb-4 rounded-xl border border-border-seam bg-bg-sunken/40 p-3\">\n <BuiPermissionButton resource=\"buildmyiq-submission\" action=\"update\" deniedBehavior=\"hide\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => autoScore.mutate(submissionId)}\n disabled={autoScore.isPending}\n >\n <Wand2 className=\"mr-1.5 h-4 w-4\" />\n {autoScore.isPending\n ? tr('buildmyiq.grading.autoScore.pending', 'Scoring…')\n : tr('buildmyiq.grading.autoScore.action', 'Auto-score')}\n </Button>\n </BuiPermissionButton>\n {autoScore.isError && (\n <p className=\"mt-2 text-xs text-status-error-text\">\n {autoScore.error instanceof Error\n ? autoScore.error.message\n : tr('buildmyiq.grading.autoScore.error', 'Auto-scoring failed.')}\n </p>\n )}\n <p className=\"mt-1.5 text-xs text-text-tertiary\">\n {tr(\n 'buildmyiq.grading.autoScore.help',\n 'All {{count}} questions are objective, so this attempt can be graded against the answer key. This saves the grade immediately.',\n { count: questionCount }\n )}\n </p>\n </div>\n );\n};\n\ninterface GradingPanelBodyProps {\n submission: BuiSubmission;\n currentUser: ReturnType<typeof useBuildmyiq>['currentUser'];\n meName: string | undefined;\n isMe: (learnerId: string) => boolean;\n learnerLabel: (learnerId: string) => string;\n gradeSubmission: ReturnType<typeof useBuiGradeSubmission>;\n onClose: () => void;\n}\n\n/**\n * The grading slide-over's content, for a single fixed `submission`. Rendered\n * with `key={submission.id}` by the parent so switching to a different\n * submission remounts this component — every local (editable) field below\n * initializes fresh from `submission` with no synchronizing effect needed.\n */\nconst GradingPanelBody: FC<GradingPanelBodyProps> = ({\n submission,\n currentUser,\n meName,\n isMe,\n learnerLabel,\n gradeSubmission,\n onClose,\n}) => {\n const tr = useTr();\n // Names WHAT is being graded. Falls back to the opaque id (never a guess)\n // while this read is in flight or if the assessment is not readable.\n const assessmentQuery = useBuiAssessment(submission.assessmentId);\n const assessment = assessmentQuery.data ?? null;\n const assessmentLabel = assessment?.title ?? shortId(submission.assessmentId);\n const questions = assessment?.questions ?? [];\n // Only offer the deterministic pass when we KNOW every question is objective.\n const canAutoScore =\n questions.length > 0 && questions.every((q) => OBJECTIVE_QUESTION_TYPES.has(q.type));\n\n const [panelTab, setPanelTab] = useState<PanelTab>('submission');\n const [editMode, setEditMode] = useState(false);\n const [scoreInput, setScoreInput] = useState(\n submission.score != null ? String(submission.score) : ''\n );\n const [passed, setPassed] = useState(submission.passed ?? false);\n const [feedback, setFeedback] = useState(submission.feedback ?? '');\n const [formError, setFormError] = useState<string | null>(null);\n\n const gradingAllowed = submission.status === 'SUBMITTED' || submission.status === 'GRADING';\n const alreadyGraded = submission.status === 'GRADED' || submission.status === 'RETURNED';\n const notSubmitted = submission.status === 'IN_PROGRESS';\n const showGradeForm = gradingAllowed || editMode;\n\n const handleGrade = async () => {\n const trimmed = scoreInput.trim();\n const scoreNum = Number(trimmed);\n if (trimmed === '' || Number.isNaN(scoreNum)) {\n setFormError('Enter a numeric score.');\n return;\n }\n if (scoreNum < 0) {\n setFormError('Score cannot be negative.');\n return;\n }\n try {\n await gradeSubmission.mutateAsync({\n id: submission.id,\n input: { score: scoreNum, passed, feedback: feedback.trim() || undefined },\n });\n onClose();\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to save grade.');\n }\n };\n\n return (\n <>\n <SheetHeader className=\"border-b border-border-seam p-5 pr-12\">\n <div className=\"flex items-center gap-3\">\n <Avatar\n size=\"md\"\n name={isMe(submission.learnerId) ? meName : undefined}\n seed={submission.learnerId}\n src={isMe(submission.learnerId) ? currentUser?.avatar : undefined}\n />\n <div className=\"min-w-0\">\n <SheetTitle className=\"truncate text-lg\">\n {learnerLabel(submission.learnerId)}\n </SheetTitle>\n <SheetDescription className=\"truncate text-xs\" title={submission.assessmentId}>\n {assessmentLabel}\n {assessment ? ` · ${formatEnumLabel(assessment.type)}` : null}\n </SheetDescription>\n </div>\n </div>\n <p className=\"text-xs text-text-secondary\">\n Attempt #{submission.attempt} · Submitted {formatDateTime(submission.submittedAt)}\n </p>\n <div className=\"pt-1\">\n <StatusBadge status={submission.status} />\n </div>\n </SheetHeader>\n\n <div className=\"flex-1 overflow-y-auto px-5 py-4\">\n <Tabs items={PANEL_TABS} value={panelTab} onChange={setPanelTab} className=\"mb-4\" />\n\n {panelTab === 'submission' ? (\n <div>\n <p className=\"mb-2 text-sm font-semibold text-text-primary\">\n Answers <span className=\"font-normal text-text-secondary\">(read-only)</span>\n </p>\n <AnswersView answers={submission.answers} questions={assessment?.questions} />\n </div>\n ) : (\n <div className=\"grid grid-cols-2 gap-4 rounded-xl bg-bg-sunken/60 p-4\">\n <MetaField label=\"Status\">\n <StatusBadge status={submission.status} />\n </MetaField>\n <MetaField label=\"Attempt\">\n <span className=\"tabular-nums\">#{submission.attempt}</span>\n </MetaField>\n <MetaField label=\"Submitted\">{formatDateTime(submission.submittedAt)}</MetaField>\n <MetaField label=\"Assessment\">\n <span className=\"block truncate\" title={submission.assessmentId}>\n {assessmentLabel}\n </span>\n {assessment && (\n <span className=\"text-xs text-text-secondary\">\n {tr('buildmyiq.grading.passMark', 'Pass ≥ {{score}}%', {\n score: assessment.passingScore,\n })}\n </span>\n )}\n </MetaField>\n {submission.enrollmentId && (\n <MetaField label=\"Enrollment\">\n <span className=\"font-mono text-xs\" title={submission.enrollmentId}>\n {shortId(submission.enrollmentId)}\n </span>\n </MetaField>\n )}\n {submission.gradedAt && (\n <MetaField label=\"Graded\">{formatDateTime(submission.gradedAt)}</MetaField>\n )}\n </div>\n )}\n\n {/* Grade / result — persistent below the tab content. */}\n <div className=\"mt-6 border-t border-border-seam pt-5\">\n <p className=\"mb-3 text-sm font-semibold text-text-primary\">Grade</p>\n\n {notSubmitted && (\n <p className=\"rounded-lg bg-bg-sunken/60 px-4 py-3 text-sm text-text-secondary\">\n This attempt is still in progress — there is nothing to grade until the learner\n submits.\n </p>\n )}\n\n {alreadyGraded && !editMode && (\n <div className=\"rounded-xl border border-border-seam p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"grid grid-cols-2 gap-4\">\n <MetaField label=\"Score\">\n <span className=\"tabular-nums font-semibold\">{submission.score ?? '—'}</span>\n </MetaField>\n <MetaField label=\"Result\">\n {submission.passed == null ? (\n <span className=\"text-text-secondary\">—</span>\n ) : (\n <StatusBadge status={submission.passed ? 'GRADED' : 'RETURNED'} />\n )}\n </MetaField>\n </div>\n <Button variant=\"outline\" size=\"sm\" onClick={() => setEditMode(true)}>\n Re-grade\n </Button>\n </div>\n <div className=\"mt-3\">\n <MetaField label=\"Feedback\">\n {submission.feedback ? (\n <span className=\"whitespace-pre-wrap\">{submission.feedback}</span>\n ) : (\n <span className=\"text-text-secondary\">No feedback provided.</span>\n )}\n </MetaField>\n </div>\n </div>\n )}\n\n {showGradeForm && (\n <div className=\"grid gap-4\">\n {canAutoScore ? (\n <AutoScorePanel submissionId={submission.id} questionCount={questions.length} />\n ) : (\n <AiGradeSuggestionPanel\n submissionId={submission.id}\n onUseSuggestion={(proposedScore, aiFeedback) => {\n setScoreInput(String(proposedScore));\n setFeedback(aiFeedback);\n }}\n />\n )}\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"grade-score\">Score</Label>\n <Input\n id=\"grade-score\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={scoreInput}\n onChange={(e) => setScoreInput(e.target.value)}\n placeholder=\"e.g. 85\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"grade-passed\">Result</Label>\n <div className=\"flex h-10 items-center gap-3\">\n <Switch id=\"grade-passed\" checked={passed} onCheckedChange={setPassed} />\n <span className=\"text-sm text-text-secondary\">\n {passed ? 'Passed' : 'Not passed'}\n </span>\n </div>\n </div>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"grade-feedback\">Feedback</Label>\n <Textarea\n id=\"grade-feedback\"\n value={feedback}\n onChange={(e) => setFeedback(e.target.value)}\n placeholder=\"Notes, corrections or encouragement for the learner (optional).\"\n rows={4}\n />\n </div>\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n )}\n </div>\n </div>\n\n <SheetFooter className=\"flex-row justify-end gap-3 border-t border-border-seam p-4\">\n <Button variant=\"outline\" onClick={onClose}>\n {showGradeForm ? 'Cancel' : 'Close'}\n </Button>\n {showGradeForm && (\n <BuiPermissionButton\n resource=\"buildmyiq-submission\"\n action=\"update\"\n deniedBehavior=\"hide\"\n >\n <Button onClick={handleGrade} disabled={gradeSubmission.isPending}>\n {gradeSubmission.isPending\n ? 'Saving…'\n : alreadyGraded\n ? 'Update grade'\n : 'Grade submission'}\n </Button>\n </BuiPermissionButton>\n )}\n </SheetFooter>\n </>\n );\n};\n\nexport const SubmissionsPage: FC = () => {\n const { currentUser, navigateTo } = useBuildmyiq();\n const { submissionId } = useParams<{ submissionId?: string }>();\n const isDesktop = useMediaQuery('(min-width: 768px)');\n const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL');\n const pager = useCursorPager();\n\n const { data, isLoading, isError, refetch } = useBuiSubmissions({\n status: statusFilter === 'ALL' ? undefined : statusFilter,\n cursor: pager.cursor,\n limit: PAGE_SIZE,\n });\n const gradeSubmission = useBuiGradeSubmission();\n\n // Deep-link support: /buildmyiq/submissions/:submissionId opens the grading\n // panel directly, independent of which page of the list is currently loaded.\n // The URL is the single source of truth for \"which submission is open\" —\n // there is no separate `selected` state to keep in sync with it.\n const deepLink = useBuiSubmission(submissionId);\n\n const rows = useMemo<BuiSubmission[]>(() => data?.items ?? [], [data]);\n\n const activeId = submissionId ?? null;\n const activeSubmission = useMemo<BuiSubmission | null>(() => {\n if (!activeId) return null;\n return (\n rows.find((r) => r.id === activeId) ?? (deepLink.data?.id === activeId ? deepLink.data : null)\n );\n }, [activeId, rows, deepLink.data]);\n\n // The signed-in educator's own display name (used only for their own rows).\n const meName = useMemo<string | undefined>(() => {\n if (!currentUser) return undefined;\n const full = [currentUser.firstName, currentUser.lastName].filter(Boolean).join(' ').trim();\n return currentUser.name?.trim() || full || currentUser.username || currentUser.email;\n }, [currentUser]);\n\n // Counts are scoped to the loaded page — labelled \"this page\" to stay honest.\n const counts = useMemo(() => {\n const acc = { total: rows.length, awaiting: 0, graded: 0, returned: 0 };\n for (const s of rows) {\n if (s.status === 'SUBMITTED' || s.status === 'GRADING') acc.awaiting += 1;\n else if (s.status === 'GRADED') acc.graded += 1;\n else if (s.status === 'RETURNED') acc.returned += 1;\n }\n return acc;\n }, [rows]);\n\n const isMe = (learnerId: string): boolean => learnerId === currentUser?.id;\n const learnerLabel = (learnerId: string): string =>\n isMe(learnerId) ? 'You' : `Learner ${shortId(learnerId)}`;\n\n /** Row click: push the deep-linkable URL; the panel opens from the route param. */\n const selectSubmission = (s: BuiSubmission) => navigateTo(`submissions/${s.id}`);\n const closePanel = () => navigateTo('submissions');\n\n // If the id in the URL doesn't resolve to a real submission in this\n // workspace (wrong tenant, typo, or one that was later deleted), fall back\n // to the plain list once the lookup has definitively failed. This effect\n // only notifies the router — it never sets this component's own state, so\n // it can't cascade into an extra render loop.\n useEffect(() => {\n if (!activeId || activeSubmission) return;\n if (!deepLink.isLoading && (deepLink.isError || deepLink.data === null)) {\n navigateTo('submissions');\n }\n }, [activeId, activeSubmission, deepLink.isLoading, deepLink.isError, deepLink.data, navigateTo]);\n\n const handleFilterChange = (value: StatusFilter) => {\n setStatusFilter(value);\n pager.reset();\n };\n\n const columns: Column<BuiSubmission>[] = [\n {\n key: 'learner',\n header: 'Learner',\n sortValue: (s) => (isMe(s.learnerId) ? '' : s.learnerId),\n render: (s) => (\n <div className=\"flex items-center gap-3\">\n <Avatar\n size=\"sm\"\n name={isMe(s.learnerId) ? meName : undefined}\n seed={s.learnerId}\n src={isMe(s.learnerId) ? currentUser?.avatar : undefined}\n />\n <div className=\"min-w-0\">\n <span className=\"block truncate font-medium text-text-primary\" title={s.learnerId}>\n {isMe(s.learnerId) ? 'You' : shortId(s.learnerId)}\n </span>\n {isMe(s.learnerId) && meName && (\n <span className=\"block truncate text-xs text-text-secondary\">{meName}</span>\n )}\n </div>\n </div>\n ),\n },\n {\n key: 'assessment',\n header: 'Assessment',\n hideOnMobile: true,\n render: (s) => (\n <span className=\"inline-flex items-center gap-2 text-text-secondary\">\n <BookOpen className=\"h-4 w-4 shrink-0 opacity-70\" />\n <span className=\"font-mono text-xs\" title={s.assessmentId}>\n {shortId(s.assessmentId)}\n </span>\n </span>\n ),\n },\n {\n key: 'attempt',\n header: 'Attempt',\n align: 'right',\n hideOnMobile: true,\n sortValue: (s) => s.attempt,\n render: (s) => <span className=\"tabular-nums text-text-secondary\">#{s.attempt}</span>,\n },\n {\n key: 'status',\n header: 'Status',\n sortValue: (s) => s.status,\n render: (s) => <StatusBadge status={s.status} />,\n },\n {\n key: 'score',\n header: 'Score',\n align: 'right',\n sortValue: (s) => s.score ?? -1,\n render: (s) =>\n s.score == null ? (\n <span className=\"text-text-secondary\">—</span>\n ) : (\n <span className=\"tabular-nums font-semibold text-text-primary\">{s.score}</span>\n ),\n },\n {\n key: 'submittedAt',\n header: 'Submitted',\n align: 'right',\n hideOnMobile: true,\n sortValue: (s) => s.submittedAt ?? s.createdAt,\n render: (s) => (\n <div className=\"whitespace-nowrap text-right\">\n <div className=\"text-text-primary\">{formatDate(s.submittedAt)}</div>\n {s.submittedAt && (\n <div className=\"text-xs text-text-secondary\">{formatClock(s.submittedAt)}</div>\n )}\n </div>\n ),\n },\n ];\n\n return (\n <PageLayout\n title=\"Grading\"\n description=\"Review and grade assessment submissions across every course in your workspace.\"\n titleGradient\n aurora\n actions={\n <Button variant=\"outline\" onClick={() => navigateTo('submissions/bulk')}>\n <Layers className=\"mr-1.5 h-4 w-4\" /> Bulk grade\n </Button>\n }\n >\n <div className=\"flex flex-col gap-6\">\n {/* KPI row — scoped to the current page. */}\n <div className=\"grid grid-cols-2 gap-4 xl:grid-cols-4\">\n <MetricsCard\n icon={<ClipboardList className=\"h-5 w-5\" />}\n value={counts.total}\n label=\"Submissions (this page)\"\n featured\n />\n <MetricsCard\n icon={<Clock className=\"h-5 w-5\" />}\n value={counts.awaiting}\n label=\"Awaiting grade (this page)\"\n accent=\"warning\"\n />\n <MetricsCard\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n value={counts.graded}\n label=\"Graded (this page)\"\n accent=\"success\"\n />\n <MetricsCard\n icon={<Undo2 className=\"h-5 w-5\" />}\n value={counts.returned}\n label=\"Returned (this page)\"\n />\n </div>\n\n <Tabs items={STATUS_TABS} value={statusFilter} onChange={handleFilterChange} />\n\n {isLoading ? (\n <LoadingView label=\"Loading submissions…\" />\n ) : isError ? (\n <ErrorView\n title=\"Couldn't load submissions\"\n message=\"The grading queue failed to load.\"\n onRetry={() => refetch()}\n />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<GraduationCap className=\"h-10 w-10\" />}\n title={statusFilter === 'ALL' ? 'No submissions yet' : 'Nothing in this status'}\n description={\n statusFilter === 'ALL'\n ? 'When learners submit assessments, their attempts will appear here for grading.'\n : 'No submissions currently match this status filter.'\n }\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(s) => s.id}\n onRowClick={selectSubmission}\n defaultSort={{ key: 'submittedAt', dir: 'desc' }}\n />\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={rows.length}\n hasMore={data?.pageInfo?.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target, data?.pageInfo?.cursor)}\n />\n </div>\n )}\n </div>\n\n {/* Grading slide-over — right panel on desktop, bottom-sheet on mobile. */}\n <Sheet\n open={activeId != null}\n onOpenChange={(open) => {\n if (!open) closePanel();\n }}\n >\n <SheetContent\n side={isDesktop ? 'right' : 'bottom'}\n className={\n isDesktop\n ? 'flex w-full max-w-none flex-col gap-0 p-0 sm:max-w-xl'\n : 'flex max-h-[92vh] w-full flex-col gap-0 rounded-t-2xl p-0'\n }\n >\n {activeSubmission ? (\n // Keyed by submission id: remounting on id change gives the panel\n // fresh local (editable) state for free, with no synchronizing effect.\n <GradingPanelBody\n key={activeSubmission.id}\n submission={activeSubmission}\n currentUser={currentUser}\n meName={meName}\n isMe={isMe}\n learnerLabel={learnerLabel}\n gradeSubmission={gradeSubmission}\n onClose={closePanel}\n />\n ) : activeId ? (\n <div className=\"flex min-h-[300px] items-center justify-center p-5\">\n <LoadingView label=\"Loading submission…\" />\n </div>\n ) : null}\n </SheetContent>\n </Sheet>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-buildmyiq",
3
- "version": "2026.1025.2",
3
+ "version": "2026.1025.3",
4
4
  "description": "Burdenoff BuildMyIQ Microfrontend — education, learning & development",
5
5
  "type": "module",
6
6
  "files": [