Before Fine Tune

This commit is contained in:
MangoPig
2026-05-26 13:43:09 +01:00
parent 4f79137d89
commit f29aff25f5
35 changed files with 6953 additions and 142 deletions

View File

@@ -1,5 +1,5 @@
import { apiFetchJson } from "../../../lib/api";
import type { ApiAssignment, ApiClassroom, ApiListResponse, ApiReviewQueueItem, ApiReviewSummary, ApiStudent } from "../../../lib/api-types";
import type { ApiAssignment, ApiAssignmentStudentQuestionDetail, ApiClassroom, ApiListResponse, ApiReviewQueueItem, ApiReviewSummary, ApiStudent } from "../../../lib/api-types";
import {
getAssignmentReviewHref,
getDashboardTeacherClassroomHref,
@@ -10,6 +10,7 @@ import {
buildTeacherShell,
formatRelativeTime,
formatShortDate,
formatCombinedScoreLabel,
initialsFor,
queueStatusLabel,
queueStatusTone,
@@ -45,6 +46,43 @@ export const getTeacherClassroomDetailData = async (
const students = studentsResponse.data.slice().sort((left, right) => left.full_name.localeCompare(right.full_name));
const reviewSummaryByAssignment = new Map(reviewSummaryEntries);
const reviewQueueByAssignment = new Map(reviewQueueEntries);
const scoredAssignments = classroomAssignments.filter((assignment) => assignment.status !== "draft");
const studentScoreEntries = await Promise.all(
students.map(async (student) => {
const scoreRows = await Promise.all(
scoredAssignments.map(async (assignment) => {
const questions = (
await apiFetchJson<ApiListResponse<ApiAssignmentStudentQuestionDetail>>(`/api/assignments/${assignment.id}/students/${student.id}/questions`)
).data;
const overallScore = questions.find((question) => typeof question.overall_score === "number")?.overall_score;
return typeof overallScore === "number" ? overallScore : null;
}),
);
const numericScores = scoreRows.filter((score): score is number => typeof score === "number");
const combinedScore = numericScores.length > 0 ? numericScores.reduce((sum, value) => sum + value, 0) : null;
return [student.id, { combinedScore, scoredAssignments: numericScores.length }] as const;
}),
);
const scoreByStudentId = new Map(studentScoreEntries);
const endangeredRankByStudentId = new Map(
studentScoreEntries
.filter(([, entry]) => entry.combinedScore != null)
.sort((left, right) => {
const scoreDelta = (left[1].combinedScore ?? Number.POSITIVE_INFINITY) - (right[1].combinedScore ?? Number.POSITIVE_INFINITY);
if (scoreDelta !== 0) return scoreDelta;
const scoredAssignmentDelta = left[1].scoredAssignments - right[1].scoredAssignments;
if (scoredAssignmentDelta !== 0) return scoredAssignmentDelta;
const leftStudent = students.find((student) => student.id === left[0]);
const rightStudent = students.find((student) => student.id === right[0]);
return (leftStudent?.full_name ?? "").localeCompare(rightStudent?.full_name ?? "");
})
.slice(0, 3)
.map(([studentId], index) => [studentId, (index + 1) as 1 | 2 | 3]),
);
const selectedStudent =
(selectedStudentId != null ? students.find((student) => student.id === selectedStudentId) : null) ?? students[0] ?? null;
@@ -57,6 +95,7 @@ export const getTeacherClassroomDetailData = async (
const liveRedoCount = studentRedoRows.filter((entry) => entry.assignment.status !== "closed").length;
const closedRedoCount = studentRedoRows.filter((entry) => entry.assignment.status === "closed").length;
const submittedCount = studentRedoRows.filter((entry) => (entry.row?.submitted_questions ?? 0) > 0).length;
const scoring = scoreByStudentId.get(student.id) ?? { combinedScore: null, scoredAssignments: 0 };
return {
id: student.id,
@@ -65,9 +104,11 @@ export const getTeacherClassroomDetailData = async (
initials: initialsFor(student.full_name),
statusLabel: submittedCount > 0 ? "Needs review" : liveRedoCount > 0 ? "Redo active" : closedRedoCount > 0 ? "Redo history" : "No redo",
redoCountLabel: `${studentRedoRows.length} redo assignment${studentRedoRows.length === 1 ? "" : "s"}`,
combinedScoreLabel: formatCombinedScoreLabel(scoring.combinedScore, scoring.scoredAssignments),
note: studentNote(submittedCount, liveRedoCount, closedRedoCount),
href: getDashboardTeacherClassroomHref(classroomId, student.id),
selected: student.id === selectedStudent?.id,
endangeredRank: endangeredRankByStudentId.get(student.id) ?? null,
};
});

View File

@@ -105,3 +105,11 @@ export const studentNote = (submittedCount: number, activeRedoCount: number, clo
if (closedRedoCount > 0) return `${closedRedoCount} closed redo assignment${closedRedoCount === 1 ? "" : "s"} available for reference.`;
return "No individual redo assignments for this student yet.";
};
export const formatCombinedScoreLabel = (combinedScore: number | null, scoredAssignments: number) => {
if (combinedScore == null || scoredAssignments <= 0) {
return "No scored assignments yet";
}
return `Combined score ${combinedScore.toFixed(1)} across ${scoredAssignments} assignment${scoredAssignments === 1 ? "" : "s"}`;
};

View File

@@ -52,7 +52,16 @@ const DashboardTeacherClassroomDetail: Component<Props> = (props) => {
<div class={styles.studentList}>
<For each={props.data.students.items}>
{(student) => (
<A href={student.href} class={`${styles.studentCard} ${student.selected ? styles.studentCardSelected : ""}`.trim()}>
<A
href={student.href}
class={[
styles.studentCard,
student.selected ? styles.studentCardSelected : "",
student.endangeredRank != null ? styles[`studentCardEndangered${student.endangeredRank}`] : "",
]
.filter(Boolean)
.join(" ")}
>
<div class={styles.studentCardHeader}>
<div class={styles.studentAvatar}>{student.initials}</div>
<div>
@@ -63,7 +72,11 @@ const DashboardTeacherClassroomDetail: Component<Props> = (props) => {
<div class={styles.metaRow}>
<span>{student.statusLabel}</span>
<span>{student.redoCountLabel}</span>
<Show when={student.endangeredRank != null}>
<span>{student.endangeredRank === 1 ? "Most endangered" : student.endangeredRank === 2 ? "Second most endangered" : "Third most endangered"}</span>
</Show>
</div>
<p class={styles.studentScoreLabel}>{student.combinedScoreLabel}</p>
<p class={styles.studentNote}>{student.note}</p>
</A>
)}

View File

@@ -7,9 +7,11 @@ export type TeacherClassroomDetailStudentItem = {
initials: string;
statusLabel: string;
redoCountLabel: string;
combinedScoreLabel: string;
note: string;
href: string;
selected: boolean;
endangeredRank: 1 | 2 | 3 | null;
};
export type TeacherClassroomRedoAssignmentItem = {

View File

@@ -251,6 +251,21 @@
background: color-mix(in srgb, var(--surface-info) 12%, var(--surface-panel-strong) 88%);
}
.studentCardEndangered1 {
border-color: color-mix(in srgb, var(--danger) 36%, var(--border-soft) 64%);
background: color-mix(in srgb, var(--surface-danger) 22%, var(--surface-panel-strong) 78%);
}
.studentCardEndangered2 {
border-color: color-mix(in srgb, var(--warning) 30%, var(--border-soft) 70%);
background: color-mix(in srgb, var(--surface-warning) 18%, var(--surface-panel-strong) 82%);
}
.studentCardEndangered3 {
border-color: color-mix(in srgb, var(--info) 22%, var(--border-soft) 78%);
background: color-mix(in srgb, var(--surface-info) 12%, var(--surface-panel-strong) 88%);
}
.studentCardHeader {
display: flex;
align-items: center;
@@ -285,6 +300,12 @@
font-size: 0.95rem;
}
.studentScoreLabel {
font-size: 0.88rem;
font-weight: 700;
color: var(--text-subtle);
}
.selectedStudentCard {
display: grid;
gap: 0.45rem;