Boost Azure Demo
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
// Path: Frontend/src/components/assignment/student/assignment-review.data.ts
|
||||
|
||||
import { apiFetchJson } from "../../../lib/api";
|
||||
import type { ApiAssignment, ApiAssignmentStudentQuestionDetail, ApiClassroom, ApiListResponse, ApiUser } from "../../../lib/api-types";
|
||||
import { getAssignmentWorkHref } from "../../../lib/routes";
|
||||
import type { AssignmentPageData } from "../shared/assignment-types";
|
||||
|
||||
const formatDateLabel = (value: string | null) => {
|
||||
if (!value) return "No due date";
|
||||
|
||||
return new Intl.DateTimeFormat("en-GB", {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}).format(new Date(value));
|
||||
};
|
||||
|
||||
const extractTopic = (assignment: ApiAssignment, questions: ApiAssignmentStudentQuestionDetail[]) => {
|
||||
const fromInstructions = assignment.instructions?.match(/^Topic:\s*(.+)$/i)?.[1]?.trim();
|
||||
if (fromInstructions) return fromInstructions;
|
||||
|
||||
return questions[0]?.subject ?? "Assignment";
|
||||
};
|
||||
|
||||
const deriveStudentStatus = (questions: ApiAssignmentStudentQuestionDetail[], assignmentStatus: ApiAssignment["status"]) => {
|
||||
const total = questions.length;
|
||||
const answered = questions.filter((question) => question.answer_id).length;
|
||||
const reviewed = questions.filter((question) => question.answer_status === "reviewed").length;
|
||||
|
||||
if (answered === 0) return "NOT_STARTED" as const;
|
||||
if (reviewed === total || assignmentStatus === "closed") return "SUBMITTED" as const;
|
||||
return "IN_PROGRESS" as const;
|
||||
};
|
||||
|
||||
const deriveStatusLabel = (status: AssignmentPageData["status"]) => {
|
||||
switch (status) {
|
||||
case "SUBMITTED":
|
||||
return "Reviewed";
|
||||
case "IN_PROGRESS":
|
||||
return "In progress";
|
||||
default:
|
||||
return "Not started";
|
||||
}
|
||||
};
|
||||
|
||||
const deriveHeadline = (status: AssignmentPageData["status"], topic: string) => {
|
||||
switch (status) {
|
||||
case "SUBMITTED":
|
||||
return `Review how you did in ${topic}`;
|
||||
case "IN_PROGRESS":
|
||||
return "Keep going — you already have momentum here";
|
||||
default:
|
||||
return "Start this assignment with a steady first pass";
|
||||
}
|
||||
};
|
||||
|
||||
const deriveDescription = (status: AssignmentPageData["status"], answered: number, reviewed: number, total: number, topic: string) => {
|
||||
switch (status) {
|
||||
case "SUBMITTED":
|
||||
return `${reviewed} of ${total} questions have teacher-reviewed answers. Use this review view to see what landed well and what still needs another pass in ${topic}.`;
|
||||
case "IN_PROGRESS":
|
||||
return `You have touched ${answered} of ${total} questions so far. Finish the remaining questions while ${topic.toLowerCase()} still feels familiar.`;
|
||||
default:
|
||||
return `This assignment has ${total} questions. Start with a calm first pass, then come back here for one shared review summary across the assignment.`;
|
||||
}
|
||||
};
|
||||
|
||||
const derivePrimaryAction = (status: AssignmentPageData["status"]) => {
|
||||
switch (status) {
|
||||
case "SUBMITTED":
|
||||
return "Open workspace";
|
||||
case "IN_PROGRESS":
|
||||
return "Continue assignment";
|
||||
default:
|
||||
return "Start assignment";
|
||||
}
|
||||
};
|
||||
|
||||
const mapQuestionStatus = (question: ApiAssignmentStudentQuestionDetail) => {
|
||||
if (question.is_correct === true) {
|
||||
return {
|
||||
statusLabel: "Correct",
|
||||
statusTone: "success" as const,
|
||||
};
|
||||
}
|
||||
|
||||
if (question.answer_status === "reviewed") {
|
||||
return {
|
||||
statusLabel: "Reviewed",
|
||||
statusTone: "success" as const,
|
||||
};
|
||||
}
|
||||
|
||||
if (question.answer_status === "submitted") {
|
||||
return {
|
||||
statusLabel: "Submitted",
|
||||
statusTone: "warning" as const,
|
||||
};
|
||||
}
|
||||
|
||||
if (question.answer_status === "in_progress") {
|
||||
return {
|
||||
statusLabel: "In progress",
|
||||
statusTone: "warning" as const,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusLabel: "Not started",
|
||||
statusTone: "muted" as const,
|
||||
};
|
||||
};
|
||||
|
||||
export const getAssignmentReviewPageData = async (assignmentId: number, studentId: number): Promise<AssignmentPageData | null> => {
|
||||
if (!Number.isFinite(assignmentId) || assignmentId <= 0) return null;
|
||||
|
||||
try {
|
||||
const assignment = await apiFetchJson<ApiAssignment>(`/api/assignments/${assignmentId}`);
|
||||
|
||||
const [student, teacher, classrooms, questionDetails] = await Promise.all([
|
||||
apiFetchJson<ApiUser>(`/api/users/${studentId}`),
|
||||
apiFetchJson<ApiUser>(`/api/users/${assignment.teacher_id}`),
|
||||
apiFetchJson<ApiListResponse<ApiClassroom>>(`/api/teachers/${assignment.teacher_id}/classrooms`),
|
||||
apiFetchJson<ApiListResponse<ApiAssignmentStudentQuestionDetail>>(`/api/assignments/${assignmentId}/students/${studentId}/questions`),
|
||||
]);
|
||||
|
||||
const classroom = classrooms.data.find((entry) => entry.id === assignment.classroom_id);
|
||||
const questions = questionDetails.data;
|
||||
const topic = extractTopic(assignment, questions);
|
||||
const answeredCount = questions.filter((question) => question.answer_id).length;
|
||||
const reviewedCount = questions.filter((question) => question.answer_status === "reviewed").length;
|
||||
const assignmentAiFeedback = questions[0]?.assignment_ai_feedback?.trim() || "";
|
||||
const assignmentTeacherFeedback = questions[0]?.assignment_teacher_feedback?.trim() || "";
|
||||
const status = deriveStudentStatus(questions, assignment.status);
|
||||
const statusLabel = deriveStatusLabel(status);
|
||||
|
||||
return {
|
||||
id: assignment.id,
|
||||
title: assignment.title,
|
||||
topic,
|
||||
status,
|
||||
statusLabel,
|
||||
dueLabel: formatDateLabel(assignment.due_at),
|
||||
studentName: student.full_name,
|
||||
classroomName: classroom?.name ?? "Classroom",
|
||||
tutorName: teacher.full_name,
|
||||
headline: deriveHeadline(status, topic),
|
||||
description: deriveDescription(status, answeredCount, reviewedCount, questions.length, topic),
|
||||
primaryAction: derivePrimaryAction(status),
|
||||
primaryHref: getAssignmentWorkHref(assignment.id),
|
||||
stats: [
|
||||
{ label: "Status", value: statusLabel },
|
||||
{ label: "Due", value: formatDateLabel(assignment.due_at) },
|
||||
{ label: "Questions", value: `${questions.length}` },
|
||||
{ label: status === "SUBMITTED" ? "Reviewed" : "Answered", value: `${status === "SUBMITTED" ? reviewedCount : answeredCount}/${questions.length}` },
|
||||
],
|
||||
coachCard: {
|
||||
title: "Review notes",
|
||||
description: `Use this review view to compare your latest answers with the shared AI and teacher feedback for ${topic.toLowerCase()}.`,
|
||||
items: [`${answeredCount} questions attempted`, `${reviewedCount} questions reviewed`, `${Math.max(questions.length - answeredCount, 0)} questions still untouched`],
|
||||
},
|
||||
assignmentAiFeedback: assignmentAiFeedback || undefined,
|
||||
assignmentTeacherFeedback: assignmentTeacherFeedback || undefined,
|
||||
questions: questions.map((question) => {
|
||||
const questionStatus = mapQuestionStatus(question);
|
||||
|
||||
return {
|
||||
id: question.question_id,
|
||||
order: question.position,
|
||||
prompt: question.prompt,
|
||||
topic: question.subject,
|
||||
subTopic: null,
|
||||
difficulty: "Backend",
|
||||
marks: null,
|
||||
statusLabel: questionStatus.statusLabel,
|
||||
statusTone: questionStatus.statusTone,
|
||||
responseLabel: question.answer_id ? "Latest answer" : "Status",
|
||||
responseValue: question.answer_text?.trim() || "No attempt yet",
|
||||
feedback: "",
|
||||
workingSteps: question.working_steps?.trim() || "",
|
||||
solveModeLabel:
|
||||
question.solve_mode === "step_by_step"
|
||||
? "Step by step"
|
||||
: question.solve_mode === "solve_together"
|
||||
? "Solve together"
|
||||
: question.solve_mode === "handwritten"
|
||||
? "Handwritten"
|
||||
: question.solve_mode === "just_answer"
|
||||
? "Just answer"
|
||||
: undefined,
|
||||
showAnswerKey: Boolean(question.correct_answer && question.answer_id),
|
||||
correctAnswer: question.correct_answer?.trim() || null,
|
||||
isCorrect: typeof question.is_correct === "boolean" ? question.is_correct : null,
|
||||
};
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === "not_found") {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user