@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.
- package/dist/buildmyiq/pages/submissions/AnswersView.d.ts +13 -0
- package/dist/buildmyiq/pages/submissions/AnswersView.d.ts.map +1 -1
- package/dist/buildmyiq/pages/submissions/AnswersView.js +13 -9
- package/dist/buildmyiq/pages/submissions/AnswersView.js.map +1 -1
- package/dist/buildmyiq/pages/submissions/BulkGradingPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/submissions/BulkGradingPage.js +139 -138
- package/dist/buildmyiq/pages/submissions/BulkGradingPage.js.map +1 -1
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.js +4 -1
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -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,
|
|
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
|
|
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:
|
|
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",
|
|
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:
|
|
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(
|
|
32
|
+
children: r(a)
|
|
29
33
|
})]
|
|
30
|
-
},
|
|
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 }> = ({
|
|
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;
|
|
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 {
|
|
10
|
-
import { useCursorPager as
|
|
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
|
|
12
|
+
import { BuiPermissionButton as m } from "../../components/BuiPermissionButton.js";
|
|
13
13
|
import "../../components/index.js";
|
|
14
|
-
import { AnswersView as
|
|
15
|
-
import { FEEDBACK_TEMPLATES as
|
|
16
|
-
import { useCallback as
|
|
17
|
-
import { ArrowLeft as
|
|
18
|
-
import { Fragment as
|
|
19
|
-
import { Button as
|
|
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
|
|
22
|
-
let a =
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
+
B(e instanceof Error ? e.message : "AI grading assistant is unavailable.");
|
|
33
33
|
}
|
|
34
|
-
},
|
|
35
|
-
let t =
|
|
34
|
+
}, K = async () => {
|
|
35
|
+
let t = v.trim(), n = Number(t);
|
|
36
36
|
if (t === "" || Number.isNaN(n) || n < 0) {
|
|
37
|
-
|
|
37
|
+
B("Enter a valid non-negative score.");
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
|
-
|
|
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:
|
|
47
|
-
feedback:
|
|
46
|
+
passed: C,
|
|
47
|
+
feedback: E.trim() || void 0
|
|
48
48
|
}
|
|
49
|
-
}),
|
|
50
|
-
id:
|
|
49
|
+
}), U && d.mutate({
|
|
50
|
+
id: U.id,
|
|
51
51
|
outcome: "ACCEPTED"
|
|
52
|
-
}),
|
|
52
|
+
}), H(!0);
|
|
53
53
|
} catch (e) {
|
|
54
|
-
|
|
54
|
+
B(e instanceof Error ? e.message : "Failed to save grade.");
|
|
55
55
|
}
|
|
56
|
-
},
|
|
57
|
-
suggest:
|
|
58
|
-
save:
|
|
56
|
+
}, q = x({
|
|
57
|
+
suggest: G,
|
|
58
|
+
save: K
|
|
59
59
|
});
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
suggest:
|
|
63
|
-
save:
|
|
60
|
+
y(() => {
|
|
61
|
+
q.current = {
|
|
62
|
+
suggest: G,
|
|
63
|
+
save: K
|
|
64
64
|
};
|
|
65
|
-
}),
|
|
66
|
-
suggest: () => void
|
|
67
|
-
save: () => void
|
|
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
|
|
70
|
-
|
|
69
|
+
let J = (e) => {
|
|
70
|
+
k((t) => t.trim().length > 0 ? `${t}\n\n${e}` : e);
|
|
71
71
|
};
|
|
72
|
-
return
|
|
72
|
+
return V ? /* @__PURE__ */ j(N, {
|
|
73
73
|
treatment: "plain",
|
|
74
74
|
className: "flex items-center gap-3 p-4",
|
|
75
|
-
children: [/* @__PURE__ */
|
|
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 ${
|
|
79
|
+
t ? "your own attempt" : `learner ${z(e.learnerId)}`,
|
|
80
80
|
" — score",
|
|
81
81
|
" ",
|
|
82
|
-
/* @__PURE__ */
|
|
82
|
+
/* @__PURE__ */ A("span", {
|
|
83
83
|
className: "font-semibold text-text-primary tabular-nums",
|
|
84
|
-
children:
|
|
84
|
+
children: v
|
|
85
85
|
}),
|
|
86
86
|
"."
|
|
87
87
|
]
|
|
88
88
|
})]
|
|
89
|
-
}) : /* @__PURE__ */
|
|
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__ */
|
|
93
|
+
children: [/* @__PURE__ */ j("div", {
|
|
94
94
|
className: "flex items-center gap-3",
|
|
95
95
|
children: [
|
|
96
|
-
/* @__PURE__ */
|
|
96
|
+
/* @__PURE__ */ A(s, {
|
|
97
97
|
size: "sm",
|
|
98
98
|
seed: e.learnerId
|
|
99
99
|
}),
|
|
100
|
-
/* @__PURE__ */
|
|
100
|
+
/* @__PURE__ */ j("div", {
|
|
101
101
|
className: "min-w-0 flex-1",
|
|
102
|
-
children: [/* @__PURE__ */
|
|
102
|
+
children: [/* @__PURE__ */ A("p", {
|
|
103
103
|
className: "truncate text-sm font-medium text-text-primary",
|
|
104
|
-
children: t ? "You" : `Learner ${
|
|
105
|
-
}), /* @__PURE__ */
|
|
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
|
-
|
|
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__ */
|
|
117
|
+
/* @__PURE__ */ j(M, {
|
|
120
118
|
type: "button",
|
|
121
119
|
variant: "outline",
|
|
122
120
|
size: "sm",
|
|
123
|
-
onClick:
|
|
121
|
+
onClick: G,
|
|
124
122
|
disabled: o.isPending,
|
|
125
123
|
className: "shrink-0",
|
|
126
|
-
children: [/* @__PURE__ */
|
|
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__ */
|
|
127
|
+
}), /* @__PURE__ */ j("div", {
|
|
130
128
|
className: "grid gap-4 lg:grid-cols-2",
|
|
131
|
-
children: [/* @__PURE__ */
|
|
129
|
+
children: [/* @__PURE__ */ j("div", {
|
|
132
130
|
className: "min-w-0",
|
|
133
|
-
children: [/* @__PURE__ */
|
|
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__ */
|
|
134
|
+
}), /* @__PURE__ */ A("div", {
|
|
137
135
|
className: "max-h-72 overflow-y-auto pr-1",
|
|
138
|
-
children: /* @__PURE__ */
|
|
136
|
+
children: /* @__PURE__ */ A(h, {
|
|
137
|
+
answers: e.answers,
|
|
138
|
+
questions: p?.questions
|
|
139
|
+
})
|
|
139
140
|
})]
|
|
140
|
-
}), /* @__PURE__ */
|
|
141
|
+
}), /* @__PURE__ */ j("div", {
|
|
141
142
|
className: "grid content-start gap-3",
|
|
142
143
|
children: [
|
|
143
|
-
|
|
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__ */
|
|
147
|
+
/* @__PURE__ */ j("div", {
|
|
147
148
|
className: "flex items-center gap-2 text-xs",
|
|
148
|
-
children: [/* @__PURE__ */
|
|
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__ */
|
|
154
|
-
className: `tabular-nums ${
|
|
155
|
-
children: [Math.round(
|
|
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
|
-
|
|
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__ */
|
|
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
|
-
|
|
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:
|
|
167
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
186
|
+
/* @__PURE__ */ j("div", {
|
|
186
187
|
className: "grid gap-3 sm:grid-cols-[7rem_auto_1fr] sm:items-start",
|
|
187
188
|
children: [
|
|
188
|
-
/* @__PURE__ */
|
|
189
|
+
/* @__PURE__ */ j("div", {
|
|
189
190
|
className: "grid gap-1",
|
|
190
|
-
children: [/* @__PURE__ */
|
|
191
|
+
children: [/* @__PURE__ */ A(F, {
|
|
191
192
|
htmlFor: `score-${e.id}`,
|
|
192
193
|
className: "text-xs",
|
|
193
194
|
children: "Score"
|
|
194
|
-
}), /* @__PURE__ */
|
|
195
|
+
}), /* @__PURE__ */ A(P, {
|
|
195
196
|
id: `score-${e.id}`,
|
|
196
197
|
type: "number",
|
|
197
198
|
min: "0",
|
|
198
|
-
value:
|
|
199
|
-
onChange: (e) =>
|
|
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__ */
|
|
205
|
+
/* @__PURE__ */ j("div", {
|
|
205
206
|
className: "grid gap-1",
|
|
206
|
-
children: [/* @__PURE__ */
|
|
207
|
+
children: [/* @__PURE__ */ A(F, {
|
|
207
208
|
className: "text-xs",
|
|
208
209
|
children: "Result"
|
|
209
|
-
}), /* @__PURE__ */
|
|
210
|
+
}), /* @__PURE__ */ j("div", {
|
|
210
211
|
className: "flex h-9 items-center gap-2",
|
|
211
|
-
children: [/* @__PURE__ */
|
|
212
|
-
checked:
|
|
213
|
-
onCheckedChange:
|
|
214
|
-
}), /* @__PURE__ */
|
|
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:
|
|
217
|
+
children: C ? "Passed" : "Not passed"
|
|
217
218
|
})]
|
|
218
219
|
})]
|
|
219
220
|
}),
|
|
220
|
-
/* @__PURE__ */
|
|
221
|
+
/* @__PURE__ */ j("div", {
|
|
221
222
|
className: "grid gap-1",
|
|
222
223
|
children: [
|
|
223
|
-
/* @__PURE__ */
|
|
224
|
+
/* @__PURE__ */ A(F, {
|
|
224
225
|
htmlFor: `feedback-${e.id}`,
|
|
225
226
|
className: "text-xs",
|
|
226
227
|
children: "Feedback (optional)"
|
|
227
228
|
}),
|
|
228
|
-
/* @__PURE__ */
|
|
229
|
+
/* @__PURE__ */ A(L, {
|
|
229
230
|
id: `feedback-${e.id}`,
|
|
230
231
|
rows: 3,
|
|
231
|
-
value:
|
|
232
|
-
onChange: (e) =>
|
|
232
|
+
value: E,
|
|
233
|
+
onChange: (e) => k(e.target.value),
|
|
233
234
|
className: "text-sm"
|
|
234
235
|
}),
|
|
235
|
-
/* @__PURE__ */
|
|
236
|
-
"aria-label": `Insert feedback template for learner ${
|
|
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 =
|
|
241
|
-
t &&
|
|
241
|
+
let t = g.find((t) => t.text === e.target.value);
|
|
242
|
+
t && J(t.text);
|
|
242
243
|
},
|
|
243
|
-
children: [/* @__PURE__ */
|
|
244
|
+
children: [/* @__PURE__ */ A("option", {
|
|
244
245
|
value: "",
|
|
245
246
|
children: "Insert template…"
|
|
246
|
-
}),
|
|
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
|
-
|
|
256
|
+
R && /* @__PURE__ */ A("p", {
|
|
256
257
|
className: "text-xs text-status-error-text",
|
|
257
|
-
children:
|
|
258
|
+
children: R
|
|
258
259
|
}),
|
|
259
|
-
/* @__PURE__ */
|
|
260
|
+
/* @__PURE__ */ A("div", {
|
|
260
261
|
className: "flex justify-end",
|
|
261
|
-
children: /* @__PURE__ */
|
|
262
|
+
children: /* @__PURE__ */ A(m, {
|
|
262
263
|
resource: "buildmyiq-submission",
|
|
263
264
|
action: "update",
|
|
264
265
|
deniedBehavior: "hide",
|
|
265
|
-
children: /* @__PURE__ */
|
|
266
|
+
children: /* @__PURE__ */ A(M, {
|
|
266
267
|
size: "sm",
|
|
267
|
-
onClick:
|
|
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
|
-
},
|
|
278
|
-
let { currentUser: n, navigateTo: s } = e(), c =
|
|
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:
|
|
282
|
-
}), h =
|
|
283
|
-
t ?
|
|
284
|
-
}, []),
|
|
285
|
-
return
|
|
286
|
-
let e = h[
|
|
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
|
-
}, [
|
|
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
|
-
|
|
298
|
-
}, r = h[
|
|
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,
|
|
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__ */
|
|
306
|
+
actions: /* @__PURE__ */ j(M, {
|
|
306
307
|
variant: "ghost",
|
|
307
308
|
onClick: () => s("submissions"),
|
|
308
|
-
children: [/* @__PURE__ */
|
|
309
|
+
children: [/* @__PURE__ */ A(C, { className: "mr-1.5 h-4 w-4" }), " Back to grading queue"]
|
|
309
310
|
}),
|
|
310
|
-
children: /* @__PURE__ */
|
|
311
|
+
children: /* @__PURE__ */ A("div", {
|
|
311
312
|
className: "flex flex-col gap-4",
|
|
312
|
-
children:
|
|
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__ */
|
|
317
|
+
}) : h.length === 0 ? /* @__PURE__ */ A(r, {
|
|
317
318
|
illustration: "empty-data",
|
|
318
|
-
icon: /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
327
|
-
/* @__PURE__ */
|
|
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__ */
|
|
330
|
+
children: [/* @__PURE__ */ A(E, { className: "h-3.5 w-3.5 shrink-0" }), /* @__PURE__ */ j("span", { children: [
|
|
330
331
|
"Shortcuts: ",
|
|
331
|
-
/* @__PURE__ */
|
|
332
|
+
/* @__PURE__ */ A("kbd", {
|
|
332
333
|
className: "font-semibold",
|
|
333
334
|
children: "J"
|
|
334
335
|
}),
|
|
335
336
|
"/",
|
|
336
|
-
/* @__PURE__ */
|
|
337
|
+
/* @__PURE__ */ A("kbd", {
|
|
337
338
|
className: "font-semibold",
|
|
338
339
|
children: "↓"
|
|
339
340
|
}),
|
|
340
341
|
" next · ",
|
|
341
|
-
/* @__PURE__ */
|
|
342
|
+
/* @__PURE__ */ A("kbd", {
|
|
342
343
|
className: "font-semibold",
|
|
343
344
|
children: "K"
|
|
344
345
|
}),
|
|
345
346
|
"/",
|
|
346
|
-
/* @__PURE__ */
|
|
347
|
+
/* @__PURE__ */ A("kbd", {
|
|
347
348
|
className: "font-semibold",
|
|
348
349
|
children: "↑"
|
|
349
350
|
}),
|
|
350
351
|
" previous ·",
|
|
351
352
|
" ",
|
|
352
|
-
/* @__PURE__ */
|
|
353
|
+
/* @__PURE__ */ A("kbd", {
|
|
353
354
|
className: "font-semibold",
|
|
354
355
|
children: "S"
|
|
355
356
|
}),
|
|
356
357
|
" suggest AI ·",
|
|
357
358
|
" ",
|
|
358
|
-
/* @__PURE__ */
|
|
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__ */
|
|
366
|
+
/* @__PURE__ */ A("div", {
|
|
366
367
|
className: "flex flex-col gap-3",
|
|
367
|
-
children: h.map((e, t) => /* @__PURE__ */
|
|
368
|
+
children: h.map((e, t) => /* @__PURE__ */ A(B, {
|
|
368
369
|
submission: e,
|
|
369
370
|
isMe: e.learnerId === n?.id,
|
|
370
|
-
isFocused: t ===
|
|
371
|
+
isFocused: t === O,
|
|
371
372
|
registerActions: D
|
|
372
373
|
}, e.id))
|
|
373
374
|
}),
|
|
374
|
-
/* @__PURE__ */
|
|
375
|
+
/* @__PURE__ */ A(o, {
|
|
375
376
|
page: c.page,
|
|
376
|
-
pageSize:
|
|
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 {
|
|
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’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":";;;;;;;;;;;;;;;;;;;;AAwDA,IAAM,IAAY,IAGZ,KAAW,MAAwB,EAAG,SAAS,KAAK,GAAG,EAAG,MAAM,GAAG,CAAC,EAAE,KAAK,GAe3E,KAAuC,EAAE,eAAY,SAAM,cAAW,yBAAsB;CAChG,IAAM,IAAkB,EAAsB,GACxC,IAAe,EAAmB,GAClC,IAAmB,EAA4B,GAE/C,CAAC,GAAY,KAAiB,EAAS,EAAE,GACzC,CAAC,GAAQ,KAAa,EAAS,EAAK,GACpC,CAAC,GAAU,KAAe,EAAS,EAAE,GACrC,CAAC,GAAO,KAAY,EAAwB,IAAI,GAChD,CAAC,GAAO,KAAY,EAAS,EAAK,GAClC,CAAC,GAAY,KAAiB,EAU1B,IAAI,GAER,IAAgB,YAAY;EAChC,EAAS,IAAI;EACb,IAAI;GACF,IAAM,IAAS,MAAM,EAAa,YAAY,EAAW,EAAE;GAG3D,AAFA,EAAc,OAAO,KAAK,MAAM,EAAO,aAAa,CAAC,CAAC,GACtD,EAAY,EAAO,oBAAoB,KAAK,GAAG,MAAM,IAAI,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC,GAC3F,EAAc;IACZ,IAAI,EAAO;IACX,YAAY,EAAO;IACnB,iBAAiB,EAAO,oBAAoB,SAAS,MAAM,EAAE,mBAAmB,CAAC,CAAC;GACpF,CAAC;EACH,SAAS,GAAG;GACV,EAAS,aAAa,QAAQ,EAAE,UAAU,sCAAsC;EAClF;CACF,GAEM,IAAa,YAAY;EAC7B,IAAM,IAAU,EAAW,KAAK,GAC1B,IAAW,OAAO,CAAO;EAC/B,IAAI,MAAY,MAAM,OAAO,MAAM,CAAQ,KAAK,IAAW,GAAG;GAC5D,EAAS,mCAAmC;GAC5C;EACF;EACA,EAAS,IAAI;EACb,IAAI;GAMF,AALA,MAAM,EAAgB,YAAY;IAChC,IAAI,EAAW;IACf,OAAO;KAAE,OAAO;KAAU;KAAQ,UAAU,EAAS,KAAK,KAAK,KAAA;IAAU;GAC3E,CAAC,GACG,KAAY,EAAiB,OAAO;IAAE,IAAI,EAAW;IAAI,SAAS;GAAW,CAAC,GAClF,EAAS,EAAI;EACf,SAAS,GAAG;GACV,EAAS,aAAa,QAAQ,EAAE,UAAU,uBAAuB;EACnE;CACF,GAKM,IAAiB,EAAO;EAAE,SAAS;EAAe,MAAM;CAAW,CAAC;CAI1E,AAHA,QAAgB;EACd,EAAe,UAAU;GAAE,SAAS;GAAe,MAAM;EAAW;CACtE,CAAC,GACD,SACE,EAAgB,EAAW,IAAI;EAC7B,eAAe,KAAK,EAAe,QAAQ,QAAQ;EACnD,YAAY,KAAK,EAAe,QAAQ,KAAK;CAC/C,CAAC,SACY,EAAgB,EAAW,IAAI,IAAI,IAC/C,CAAC,EAAW,IAAI,CAAe,CAAC;CAEnC,IAAM,KAAwB,MAAyB;EACrD,GAAa,MAAU,EAAK,KAAK,EAAE,SAAS,IAAI,GAAG,EAAK,MAAM,MAAiB,CAAa;CAC9F;CAcA,OAZI,IAEA,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC,CACE,kBAAC,GAAD,EAAc,WAAU,4CAA6C,CAAA,GACrE,kBAAC,KAAD;GAAG,WAAU;aAAb;IAA2C;IACjC,IAAO,qBAAqB,WAAW,EAAQ,EAAW,SAAS;IAAI;IAAS;IACxF,kBAAC,QAAD;KAAM,WAAU;eAAgD;IAAiB,CAAA;IAAC;GACjF;IACM;MAKb,kBAAC,GAAD;EACE,WAAU;EACV,IAAI,kBAAkB,EAAW;EACjC,WAAW,6CACT,IAAY,2CAA2C;YAJ3D,CAOE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KAAQ,MAAK;KAAK,MAAM,EAAW;IAAY,CAAA;IAC/C,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBACV,IAAO,QAAQ,WAAW,EAAQ,EAAW,SAAS;KACtD,CAAA,GACH,kBAAC,KAAD;MAAG,WAAU;MAAsC,OAAO,EAAW;gBAArE;OAAmF;OACrE,EAAQ,EAAW,YAAY;OAAE;OAAa,EAAW;OAAQ;OAAG;OAC/E,EAAe,EAAW,WAAW;MACrC;OACA;;IACL,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,MAAK;KACL,SAAS;KACT,UAAU,EAAa;KACvB,WAAU;eANZ,CAQE,kBAAC,GAAD,EAAU,WAAU,qBAAsB,CAAA,GACzC,EAAa,YAAY,eAAe,SACnC;;GACL;MAGL,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eAAyE;IAEnF,CAAA,GACH,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD,EAAa,SAAS,EAAW,QAAU,CAAA;IACxC,CAAA,CACF;OAEL,kBAAC,OAAD;IAAK,WAAU;cAAf;KACG,KACC,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD,EAAU,WAAU,uDAAwD,CAAA,GAC5E,kBAAC,QAAD;SAAM,WAAU;mBAAhB;UAAsC;UACrB;UACf,kBAAC,QAAD;WACE,WAAW,gBACT,EAAgB,EAAW,UAAU,IACjC,2CACA;qBAJR,CAOG,KAAK,MAAM,EAAW,aAAa,GAAG,GAAE,GACrC;;SACF;UACH;;OACJ,EAAgB,EAAW,UAAU,KACpC,kBAAC,KAAD;QAAG,WAAU;kBAAb,CACE,kBAAC,GAAD,EAAe,WAAU,8BAA+B,CAAA,GAAC,gFAExD;;OAEJ,EAAW,gBAAgB,SAAS,KACnC,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAW,gBAAgB,KAAK,MAC/B,kBAAC,MAAD,EAAA,UAAA;SACE,kBAAC,QAAD;UAAM,WAAU;oBAAmC,EAAE;SAAY,CAAA;SAAE;SACnE,kBAAC,QAAD;UAAM,WAAU;oBAAhB;WACG,EAAE;WAAgB;WAAE,EAAE;UACnB;;SACL,EAAE,UAAU,MAAM,EAAE,YAAY;QAC/B,EAAA,GANK,EAAE,WAMP,CACL;OACC,CAAA;MAEH;;KAGP,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAS,SAAS,EAAW;SAAM,WAAU;mBAAU;QAEvD,CAAA,GACP,kBAAC,GAAD;SACE,IAAI,SAAS,EAAW;SACxB,MAAK;SACL,KAAI;SACJ,OAAO;SACP,WAAW,MAAM,EAAc,EAAE,OAAO,KAAK;SAC7C,aAAY;SACZ,WAAU;QACX,CAAA,CACE;;OACL,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,WAAU;mBAAU;QAAa,CAAA,GACxC,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,GAAD;UAAQ,SAAS;UAAQ,iBAAiB;SAAY,CAAA,GACtD,kBAAC,QAAD;UAAM,WAAU;oBACb,IAAS,WAAW;SACjB,CAAA,CACH;UACF;;OACL,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,GAAD;UAAO,SAAS,YAAY,EAAW;UAAM,WAAU;oBAAU;SAE1D,CAAA;SACP,kBAAC,GAAD;UACE,IAAI,YAAY,EAAW;UAC3B,MAAM;UACN,OAAO;UACP,WAAW,MAAM,EAAY,EAAE,OAAO,KAAK;UAC3C,WAAU;SACX,CAAA;SACD,kBAAC,UAAD;UACE,cAAY,wCAAwC,EAAQ,EAAW,SAAS;UAChF,WAAU;UACV,OAAM;UACN,WAAW,MAAM;WACf,IAAM,IAAW,EAAmB,MAAM,MAAM,EAAE,SAAS,EAAE,OAAO,KAAK;WACzE,AAAI,KAAU,EAAqB,EAAS,IAAI;UAClD;oBAPF,CASE,kBAAC,UAAD;WAAQ,OAAM;qBAAG;UAAwB,CAAA,GACxC,EAAmB,KAAK,MACvB,kBAAC,UAAD;WAAsB,OAAO,EAAE;qBAC5B,EAAE;UACG,GAFK,EAAE,KAEP,CACT,CACK;;QACL;;MACF;;KAEJ,KAAS,kBAAC,KAAD;MAAG,WAAU;gBAAkC;KAAS,CAAA;KAElE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OACE,UAAS;OACT,QAAO;OACP,gBAAe;iBAEf,kBAAC,GAAD;QAAQ,MAAK;QAAK,SAAS;QAAY,UAAU,EAAgB;kBAC9D,EAAgB,YAAY,YAAY;OACnC,CAAA;MACW,CAAA;KAClB,CAAA;IACF;KACF;IACI;;AAEf,GAEa,UAA4B;CACvC,IAAM,EAAE,gBAAa,kBAAe,EAAa,GAC3C,IAAQ,EAAe,GACvB,EAAE,SAAM,cAAW,YAAS,eAAY,EAAkB;EAC9D,QAAQ;EACR,QAAQ,EAAM;EACd,OAAO;CACT,CAAC,GAEK,IAAO,QAA+B,GAAM,SAAS,CAAC,GAAG,CAAC,CAAI,CAAC,GAC/D,CAAC,GAAc,KAAmB,EAAS,CAAC,GAC5C,IAAa,kBAAO,IAAI,IAAwB,CAAC,GAEjD,IAAkB,GAAa,GAAsB,MAA+B;EACxF,AAAI,IAAS,EAAW,QAAQ,IAAI,GAAc,CAAO,IACpD,EAAW,QAAQ,OAAO,CAAY;CAC7C,GAAG,CAAC,CAAC,GAGC,IAAmB,EAAK,WAAW,IAAI,IAAI,KAAK,IAAI,GAAc,EAAK,SAAS,CAAC;CAoDvF,OAjDA,QAAgB;EACd,IAAM,IAAU,EAAK;EAChB,KACL,SACG,eAAe,kBAAkB,EAAQ,IAAI,GAC5C,eAAe;GAAE,OAAO;GAAW,UAAU;EAAS,CAAC;CAC7D,GAAG,CAAC,GAAkB,CAAI,CAAC,GAI3B,QAAgB;EACd,IAAM,KAAa,MAAqB;GACtC,IAAI,EAAK,WAAW,GAAG;GACvB,IAAM,IAAS,EAAE;GAUjB,IARE,MACC,EAAO,YAAY,WAClB,EAAO,YAAY,cACnB,EAAO,YAAY,YACnB,EAAO,sBAIP,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ;GAExC,IAAM,KAAQ,MAAkB;IAC9B,GAAiB,MAAS,KAAK,IAAI,KAAK,IAAI,IAAO,GAAO,CAAC,GAAG,EAAK,SAAS,CAAC,CAAC;GAChF,GACM,IAAoB,EAAK,IACzB,IAAU,IAAoB,EAAW,QAAQ,IAAI,EAAkB,EAAE,IAAI,KAAA;GAEnF,AAAI,EAAE,QAAQ,OAAO,EAAE,QAAQ,eAC7B,EAAE,eAAe,GACjB,EAAK,CAAC,KACG,EAAE,QAAQ,OAAO,EAAE,QAAQ,aACpC,EAAE,eAAe,GACjB,EAAK,EAAE,KACE,EAAE,QAAQ,OAAO,KAC1B,EAAE,eAAe,GACjB,EAAQ,QAAQ,KACP,EAAE,QAAQ,WAAW,MAC9B,EAAE,eAAe,GACjB,EAAQ,KAAK;EAEjB;EAEA,OADA,OAAO,iBAAiB,WAAW,CAAS,SAC/B,OAAO,oBAAoB,WAAW,CAAS;CAC9D,GAAG,CAAC,GAAM,CAAgB,CAAC,GAGzB,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SACE,kBAAC,GAAD;GAAQ,SAAQ;GAAQ,eAAe,EAAW,aAAa;aAA/D,CACE,kBAAC,GAAD,EAAW,WAAU,iBAAkB,CAAA,GAAC,wBAClC;;YAGV,kBAAC,OAAD;GAAK,WAAU;aACZ,IACC,kBAAC,GAAD,EAAa,OAAM,sCAAuC,CAAA,IACxD,IACF,kBAAC,GAAD;IACE,OAAM;IACN,SAAQ;IACR,eAAe,EAAQ;GACxB,CAAA,IACC,EAAK,WAAW,IAClB,kBAAC,GAAD;IACE,cAAa;IACb,MAAM,kBAAC,GAAD,EAAe,WAAU,YAAa,CAAA;IAC5C,OAAM;IACN,aAAY;IACZ,QACE,kBAAC,GAAD;KAAQ,SAAQ;KAAU,eAAe,EAAW,aAAa;eAAG;IAE5D,CAAA;GAEX,CAAA,IAED,kBAAA,GAAA,EAAA,UAAA;IACE,kBAAC,KAAD;KAAG,WAAU;eAAb,CACE,kBAAC,GAAD,EAAU,WAAU,uBAAwB,CAAA,GAC5C,kBAAC,QAAD,EAAA,UAAA;MAAM;MACO,kBAAC,OAAD;OAAK,WAAU;iBAAgB;MAAM,CAAA;MAAC;MACjD,kBAAC,OAAD;OAAK,WAAU;iBAAgB;MAAM,CAAA;MAAC;MAAQ,kBAAC,OAAD;OAAK,WAAU;iBAAgB;MAAM,CAAA;MAAC;MACnF,kBAAC,OAAD;OAAK,WAAU;iBAAgB;MAAM,CAAA;MAAC;MAAY;MACnD,kBAAC,OAAD;OAAK,WAAU;iBAAgB;MAAM,CAAA;MAAC;MAAc;MACpD,kBAAC,OAAD;OAAK,WAAU;iBAAgB;MAAU,CAAA;MAAC;KAEtC,EAAA,CAAA,CACL;;IACH,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAK,KAAK,GAAG,MACZ,kBAAC,GAAD;MAEE,YAAY;MACZ,MAAM,EAAE,cAAc,GAAa;MACnC,WAAW,MAAM;MACA;KAClB,GALM,EAAE,EAKR,CACF;IACE,CAAA;IACL,kBAAC,GAAD;KACE,MAAM,EAAM;KACZ,UAAU;KACV,WAAW,EAAK;KAChB,SAAS,GAAM,UAAU,WAAW;KACpC,eAAe,MAAW,EAAM,aAAa,GAAQ,GAAM,UAAU,MAAM;IAC5E,CAAA;GACD,EAAA,CAAA;EAED,CAAA;CACK,CAAA;AAEhB"}
|
|
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’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, {
|
|
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 “Grade submission” 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 “Grade submission” 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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"}
|