Skip to content

Commit 6ee3f57

Browse files
fix(mcp): make submission-grader tell the truth about what is graded (#567)
The per-question review list in the submission-grader widget misreported how much of a submission had actually been graded — the one thing a teacher opens it to find out. - An ungraded question (`points_earned: null`) rendered as `0/15 pts`, making "nobody has looked at this yet" indistinguishable from "the student got it wrong", and contradicting the `4/5 graded` summary a few hundred pixels above. It now renders as `—/15 pts` alongside a "Not graded" pill, and the `?? 0` coercion is gone. - The grade panel now states how many points sit in ungraded questions, so the `60 / 100` figure can no longer read as "the student lost 40 points" when 15 of those points have simply never been graded. - `is_overridden` arrived in the payload and was never rendered, so a teacher could not see which scores they had already corrected. Overridden rows now carry a "Teacher adjusted" pill. - AI confidence was plain grey text appended to a label, so 41% and 82% looked identical. Confidence below 60% now renders as an amber "review this" pill and tints the question's border, while high confidence stays quiet. The server side needed no change: `lms_get_submission` already excludes null scores from `total_points_earned` and `graded_count` (analytics.ts:382-385). The misleading part was the denominator spanning ungraded questions, which is a presentation concern and is handled in the widget. Verified against the `lms_demo_submission_grader` default fixture (ungraded Q5, overridden Q4, confidences 0.82 and 0.41) in both light and dark themes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Zt1kZKdcWAcsiJdBgnX3D
1 parent a6c88d3 commit 6ee3f57

1 file changed

Lines changed: 68 additions & 8 deletions

File tree

mcp-server/resources/submission-grader/widget.tsx

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,17 @@ function statusPill(status: string): { classes: string; label: string } {
9292
return map[status] ?? map.pending;
9393
}
9494

95+
/**
96+
* Below this, the AI is guessing rather than grading — the teacher should
97+
* re-read the answer instead of trusting the suggested score.
98+
*/
99+
const LOW_CONFIDENCE = 0.6;
100+
101+
/** `points_earned: null` means nobody has graded the question — never a zero. */
102+
function isUngraded(q: Props["questions"][number]): boolean {
103+
return q.points_earned == null;
104+
}
105+
95106
function formatDate(s: string): string {
96107
try {
97108
return new Date(s).toLocaleString(undefined, {
@@ -166,6 +177,15 @@ export default function SubmissionGrader() {
166177
const studentName = submission.student_name ?? submission.student_id.slice(0, 8);
167178
const saveDisabled = saving || (!dirty && status === "teacher_reviewed");
168179

180+
// The summary counts only graded questions into total_points_earned, but
181+
// total_points_possible spans the whole exam — so "60 / 100" can read as
182+
// "lost 40 points" when part of that gap is simply not graded yet.
183+
const ungradedQuestions = questions.filter(isUngraded);
184+
const ungradedPoints = ungradedQuestions.reduce(
185+
(total, q) => total + (q.points_possible ?? 0),
186+
0
187+
);
188+
169189
return (
170190
<McpUseProvider autoSize>
171191
<Brand />
@@ -216,6 +236,14 @@ export default function SubmissionGrader() {
216236
</div>
217237
</div>
218238

239+
{ungradedQuestions.length > 0 && (
240+
<div className="mt-2 text-[12.5px] font-medium text-amber-700 dark:text-amber-400">
241+
{ungradedPoints} pt{ungradedPoints === 1 ? "" : "s"} across{" "}
242+
{ungradedQuestions.length} question
243+
{ungradedQuestions.length === 1 ? "" : "s"} not graded yet
244+
</div>
245+
)}
246+
219247
<label className="mt-3.5 block">
220248
<span className="mb-1.5 block text-xs font-semibold text-zinc-500 dark:text-zinc-400">
221249
Feedback to student
@@ -255,6 +283,9 @@ export default function SubmissionGrader() {
255283
{/* Questions */}
256284
<div className="flex flex-col gap-3">
257285
{questions.map((q, i) => {
286+
const ungraded = isUngraded(q);
287+
const lowConfidence =
288+
q.ai_confidence != null && q.ai_confidence < LOW_CONFIDENCE;
258289
const correct = q.is_correct;
259290
const badge =
260291
correct === true
@@ -273,7 +304,11 @@ export default function SubmissionGrader() {
273304
return (
274305
<div
275306
key={q.question_id}
276-
className="rounded-xl border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900"
307+
className={`rounded-xl border bg-white p-4 dark:bg-zinc-900 ${
308+
lowConfidence
309+
? "border-amber-300 dark:border-amber-800"
310+
: "border-zinc-200 dark:border-zinc-800"
311+
}`}
277312
>
278313
<div className="mb-2 flex items-start justify-between gap-2.5">
279314
<div className="flex items-baseline gap-2">
@@ -284,10 +319,26 @@ export default function SubmissionGrader() {
284319
{q.type.replace(/_/g, " ")}
285320
</span>
286321
</div>
287-
<div className="flex shrink-0 items-center gap-2">
322+
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2">
288323
{q.points_possible != null && (
289-
<span className="text-xs font-semibold text-zinc-900 dark:text-zinc-100">
290-
{q.points_earned ?? 0}/{q.points_possible} pts
324+
<span
325+
className={`text-xs font-semibold ${
326+
ungraded
327+
? "text-zinc-400 dark:text-zinc-500"
328+
: "text-zinc-900 dark:text-zinc-100"
329+
}`}
330+
>
331+
{ungraded ? "—" : q.points_earned}/{q.points_possible} pts
332+
</span>
333+
)}
334+
{ungraded && (
335+
<span className="rounded-lg bg-zinc-100 px-2 py-0.5 text-[11px] font-semibold text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">
336+
Not graded
337+
</span>
338+
)}
339+
{q.is_overridden && (
340+
<span className="rounded-lg bg-[var(--brand-50)] px-2 py-0.5 text-[11px] font-semibold text-[var(--brand-600)] dark:bg-[var(--brand-950)] dark:text-[var(--brand-400)]">
341+
Teacher adjusted
291342
</span>
292343
)}
293344
{badge && (
@@ -342,11 +393,20 @@ export default function SubmissionGrader() {
342393
{/* AI feedback + confidence */}
343394
{q.ai_feedback && (
344395
<div className="text-[12.5px] leading-normal text-zinc-500 dark:text-zinc-400">
345-
<span className="font-semibold">
346-
AI feedback
396+
<div className="flex flex-wrap items-center gap-2">
397+
<span className="font-semibold">AI feedback</span>
347398
{q.ai_confidence != null &&
348-
` (${Math.round(q.ai_confidence * 100)}% confidence)`}
349-
</span>
399+
(lowConfidence ? (
400+
<span className="rounded-lg bg-amber-100 px-2 py-0.5 text-[11px] font-semibold text-amber-800 dark:bg-amber-950 dark:text-amber-300">
401+
{Math.round(q.ai_confidence * 100)}% confidence · review
402+
this
403+
</span>
404+
) : (
405+
<span className="text-[11px] font-medium">
406+
{Math.round(q.ai_confidence * 100)}% confidence
407+
</span>
408+
))}
409+
</div>
350410
<Markdown content={q.ai_feedback} dark={dark} fontSize={12.5} />
351411
</div>
352412
)}

0 commit comments

Comments
 (0)