import React from "react"; import {BsClock, BsXCircle} from "react-icons/bs"; import clsx from "clsx"; import {Stat, User} from "@/interfaces/user"; import {Module, Step} from "@/interfaces"; import ai_usage from "@/utils/ai.detection"; import {calculateBandScore} 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 {convertToUserSolutions} from "@/utils/stats"; import {getExamById} from "@/utils/exams"; import {Exam, UserSolution} from "@/interfaces/exam"; import ModuleBadge from "../ModuleBadge"; 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.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; examNumber?: number | undefined; stats: Stat[]; timestamp: string | number; user: User; assignments: Assignment[]; users: User[]; training?: boolean; gradingSystem?: Step[]; selectedTrainingExams?: string[]; maxTrainingExams?: number; setSelectedTrainingExams?: React.Dispatch>; setExams: (exams: Exam[]) => void; setShowSolutions: (show: boolean) => void; setUserSolutions: (solutions: UserSolution[]) => void; setSelectedModules: (modules: Module[]) => void; setInactivity: (inactivity: number) => void; setTimeSpent: (time: number) => void; renderPdfIcon: (session: string, color: string, textColor: string) => React.ReactNode; } const StatsGridItem: React.FC = ({ stats, timestamp, user, assignments, users, training, selectedTrainingExams, gradingSystem, setSelectedTrainingExams, setExams, setShowSolutions, setUserSolutions, setSelectedModules, setInactivity, setTimeSpent, renderPdfIcon, width = undefined, height = undefined, examNumber = undefined, maxTrainingExams = undefined, }) => { const router = useRouter(); 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)) { if (!!timeSpent) setTimeSpent(timeSpent); if (!!inactivity) setInactivity(inactivity); setUserSolutions(convertToUserSolutions(stats)); setShowSolutions(true); setExams(exams.map((x) => x!).sort(sortByModule)); setSelectedModules( exams .map((x) => x!) .sort(sortByModule) .map((x) => x!.module), ); router.push("/exercises"); } }); } }; const shouldRenderPDFIcon = () => { if (assignment) { return assignment.released; } return true; }; const content = ( <>
{formatTimestamp(timestamp)}
{!!timeSpent && ( {Math.floor(timeSpent / 60)} minutes )} {!!inactivity && ( {Math.floor(inactivity / 60)} minutes )}
{!!assignment && (assignment.released || assignment.released === undefined) && ( Level{" "} {( aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length ).toFixed(1)} )} {shouldRenderPDFIcon() && renderPdfIcon(session, textColor, textColor)}
{examNumber === undefined ? ( <> {aiUsage >= 50 && user.type !== "student" && (
= 80, })}> AI Usage
)} ) : (
{examNumber}
)}
{!!assignment && (assignment.released || assignment.released === undefined) && 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;