import React from "react"; import { BsClock, BsXCircle } from "react-icons/bs"; import clsx from "clsx"; import { Stat, User } from "@/interfaces/user"; import { Grading, Module, Step } from "@/interfaces"; import ai_usage from "@/utils/ai.detection"; import { calculateBandScore, getGradingLabel } from "@/utils/score"; import moment from "moment"; import { Assignment } from "@/interfaces/results"; import { uuidv4 } from "@firebase/util"; import { useRouter } from "next/router"; import { uniqBy } from "lodash"; import { sortByModule } from "@/utils/moduleUtils"; import { getExamById } from "@/utils/exams"; import ModuleBadge from "../ModuleBadge"; import useExamStore from "@/stores/exam"; import { findBy } from "@/utils"; const formatTimestamp = (timestamp: string | number) => { const time = typeof timestamp === "string" ? parseInt(timestamp) : timestamp; const date = moment(time); const formatter = "YYYY/MM/DD - HH:mm"; return date.format(formatter); }; const aggregateScoresByModule = (stats: Stat[]): { module: Module; total: number; missing: number; correct: number }[] => { const scores: { [key in Module]: { total: number; missing: number; correct: number }; } = { reading: { total: 0, correct: 0, missing: 0, }, listening: { total: 0, correct: 0, missing: 0, }, writing: { total: 0, correct: 0, missing: 0, }, speaking: { total: 0, correct: 0, missing: 0, }, level: { total: 0, correct: 0, missing: 0, }, }; stats.filter(x => !x.isPractice).forEach((x) => { scores[x.module!] = { total: scores[x.module!].total + x.score.total, correct: scores[x.module!].correct + x.score.correct, missing: scores[x.module!].missing + x.score.missing, }; }); return Object.keys(scores) .filter((x) => scores[x as Module].total > 0) .map((x) => ({ module: x as Module, ...scores[x as Module] })); }; interface StatsGridItemProps { width?: string | undefined; height?: string | undefined; gradingSystems: Grading[] examNumber?: number | undefined; stats: Stat[]; timestamp: string | number; user: User; assignments: Assignment[]; users: User[]; training?: boolean; selectedTrainingExams?: string[]; maxTrainingExams?: number; setSelectedTrainingExams?: React.Dispatch>; renderPdfIcon?: (session: string, color: string, textColor: string) => React.ReactNode; } const StatsGridItem: React.FC = ({ stats, timestamp, user, assignments, gradingSystems, users, training, selectedTrainingExams, setSelectedTrainingExams, renderPdfIcon, width = undefined, height = undefined, examNumber = undefined, maxTrainingExams = undefined, }) => { const router = useRouter(); const dispatch = useExamStore((s) => s.dispatch); const correct = stats.reduce((accumulator, current) => accumulator + current.score.correct, 0); const total = stats.reduce((accumulator, current) => accumulator + current.score.total, 0); const aggregatedScores = aggregateScoresByModule(stats).filter((x) => x.total > 0); const assignmentID = stats.reduce((_, current) => current.assignment as any, ""); const assignment = assignments.find((a) => a.id === assignmentID); const isDisabled = stats.some((x) => x.isDisabled); const aiUsage = Math.round(ai_usage(stats) * 100); const aggregatedLevels = aggregatedScores.map((x) => ({ module: x.module, level: calculateBandScore(x.correct, x.total, x.module, user.focus), })); const textColor = clsx( correct / total >= 0.7 && "text-mti-purple", correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red", correct / total < 0.3 && "text-mti-rose", ); const { timeSpent, inactivity, session } = stats[0]; const selectExam = () => { if ( training && !isDisabled && typeof maxTrainingExams !== "undefined" && typeof setSelectedTrainingExams !== "undefined" && typeof timestamp == "string" ) { setSelectedTrainingExams((prevExams) => { const uniqueExams = [...new Set(stats.map((stat) => `${stat.module}-${stat.date}`))]; const indexes = uniqueExams.map((exam) => prevExams.indexOf(exam)).filter((index) => index !== -1); if (indexes.length > 0) { const newExams = [...prevExams]; indexes .sort((a, b) => b - a) .forEach((index) => { newExams.splice(index, 1); }); return newExams; } else { if (prevExams.length + uniqueExams.length <= maxTrainingExams) { return [...prevExams, ...uniqueExams]; } else { return prevExams; } } }); } else { const examPromises = uniqBy(stats, "exam").map((stat) => { return getExamById(stat.module, stat.exam); }); if (isDisabled) return; Promise.all(examPromises).then((exams) => { if (exams.every((x) => !!x)) { dispatch({ type: 'INIT_SOLUTIONS', payload: { exams: exams.map((x) => x!).sort(sortByModule), modules: exams .map((x) => x!) .sort(sortByModule) .map((x) => x!.module), stats, timeSpent, inactivity } }); router.push("/exam"); } }); } }; const shouldRenderPDFIcon = () => { if (assignment) return assignment.released; return true; }; const levelAverage = () => aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length const renderLevelScore = () => { const defaultLevelScore = levelAverage().toFixed(1) if (!stats.every(s => s.module === "level")) return defaultLevelScore if (gradingSystems.length === 0) return defaultLevelScore const score = { correct: stats.reduce((acc, curr) => acc + curr.score.correct, 0), total: stats.reduce((acc, curr) => acc + curr.score.total, 0) } const level: number = calculateBandScore(score.correct, score.total, "level", user.focus); if (!!assignment) { const gradingSystem = findBy(gradingSystems, 'entity', assignment.entity) if (!gradingSystem) return defaultLevelScore return getGradingLabel(level, gradingSystem.steps) } return getGradingLabel(level, gradingSystems[0].steps) } const content = ( <>
{formatTimestamp(timestamp)}
{!!timeSpent && ( {Math.floor(timeSpent / 60)} minutes )} {!!inactivity && ( {Math.floor(inactivity / 60)} minutes )}
{((!!assignment && (assignment.released || assignment.released === undefined)) || !assignment) && ( Level{' '} {renderLevelScore()} )} {shouldRenderPDFIcon() && renderPdfIcon && renderPdfIcon(session, textColor, textColor)}
{examNumber === undefined ? ( <> {aiUsage >= 50 && user.type !== "student" && (
= 80, })}> AI Usage
)} ) : (
{examNumber}
)}
{aggregatedLevels.map(({ module, level }) => )}
{assignment && ( Assignment: {assignment.name}, Teacher: {users.find((u) => u.id === assignment.assigner)?.name} )}
); return ( <>
= 0.7 && "hover:border-mti-purple", correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red", correct / total < 0.3 && "hover:border-mti-rose", typeof selectedTrainingExams !== "undefined" && typeof timestamp === "string" && selectedTrainingExams.some((exam) => exam.includes(timestamp)) && "border-2 border-slate-600", )} onClick={() => { if (!!assignment && !assignment.released) return; if (examNumber === undefined) return selectExam(); return; }} style={{ ...(width !== undefined && { width }), ...(height !== undefined && { height }), }} data-tip={isDisabled ? "This exam is still being evaluated..." : "This exam is still locked by its assigner..."} role="button"> {content}
= 0.7 && "hover:border-mti-purple", correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red", correct / total < 0.3 && "hover:border-mti-rose", )} data-tip="Your screen size is too small to view previous exams." style={{ ...(width !== undefined && { width }), ...(height !== undefined && { height }), }} role="button"> {content}
); }; export default StatsGridItem;