diff --git a/package-lock.json b/package-lock.json index 335cc30f..3e98eb63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,8 @@ "videojs-youtube": "^3.0.1", "web-vitals": "^4.2.4", "workbox-webpack-plugin": "^7.0.0", + "y-websocket": "^3.0.0", + "yjs": "^13.6.30", "zod": "^3.25.75", "zustand": "^5.0.10" }, @@ -3753,46 +3755,41 @@ "node_modules/@webassemblyjs/wasm-gen": { "version": "1.14.1", "license": "MIT", - "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/@webassemblyjs/wasm-opt": { "version": "1.14.1", "license": "MIT", - "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/@webassemblyjs/wasm-parser": { "version": "1.14.1", "license": "MIT", - "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/@webassemblyjs/wast-printer": { "version": "1.14.1", "license": "MIT", - "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/@wry/caches": { @@ -7310,6 +7307,16 @@ "version": "2.0.0", "license": "ISC" }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "dev": true, diff --git a/package.json b/package.json index b018f749..7e3e3cb5 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,8 @@ "videojs-youtube": "^3.0.1", "web-vitals": "^4.2.4", "workbox-webpack-plugin": "^7.0.0", + "y-websocket": "^3.0.0", + "yjs": "^13.6.30", "zod": "^3.25.75", "zustand": "^5.0.10" }, diff --git a/src/components/assessment/AdaptiveTesting.tsx b/src/components/assessment/AdaptiveTesting.tsx new file mode 100644 index 00000000..da7eda7e --- /dev/null +++ b/src/components/assessment/AdaptiveTesting.tsx @@ -0,0 +1,321 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { ArrowRight, ArrowLeft, Activity, Zap, CheckCircle2, AlertTriangle } from 'lucide-react'; +import { + AssessmentQuestion, + AssessmentQuestionType, + createQuestionTemplate, + AssessmentOption, + AssessmentTestCase, +} from './QuestionTypes'; + +const SAMPLE_QUESTIONS: AssessmentQuestion[] = [ + { + id: 'adaptive-1', + type: 'multiple-choice', + text: 'Which sentence describes an adaptive assessment?', + points: 5, + difficulty: 2, + explanation: 'Adaptive assessments adjust question difficulty based on learner performance.', + options: [ + { id: 'a', text: 'Same test for everyone', isCorrect: false }, + { id: 'b', text: 'Question difficulty adapts to answers', isCorrect: true }, + { id: 'c', text: 'Only one question is shown', isCorrect: false }, + ], + }, + { + id: 'adaptive-2', + type: 'true-false', + text: 'Adaptive testing can help identify a student’s zone of proximal development.', + points: 5, + difficulty: 3, + correctAnswer: 'true', + explanation: 'Adaptive questions focus on appropriate challenge levels.', + }, + { + id: 'adaptive-3', + type: 'code-challenge', + text: 'Write a function that returns the sum of two numbers.', + points: 10, + difficulty: 4, + language: 'javascript', + codeTemplate: 'function solution(a, b) { + return a + b; +}', + testCases: [ + { id: 't1', input: '1,2', expectedOutput: '3' }, + { id: 't2', input: '-1,4', expectedOutput: '3' }, + ], + explanation: 'The function should add numeric inputs and return the result.', + }, + { + id: 'adaptive-4', + type: 'essay', + text: 'Describe one advantage of adaptive testing for personalised learning.', + points: 8, + difficulty: 5, + wordLimit: 150, + explanation: 'Essay reflections provide evidence of mastery and strategy awareness.', + }, +]; + +const getCorrectAnswer = (question: AssessmentQuestion, answer: string) => { + if (question.type === 'multiple-choice') { + return question.options.some((option) => option.id === answer && option.isCorrect); + } + if (question.type === 'true-false') { + return question.correctAnswer === answer; + } + if (question.type === 'code-challenge') { + return question.testCases.every((testCase) => answer.includes(testCase.expectedOutput)); + } + if (question.type === 'essay') { + return answer.trim().length > 0; + } + return false; +}; + +const pickNextQuestion = ( + pool: AssessmentQuestion[], + previousQuestionId: string, + desiredDifficulty: number, +) => { + const remaining = pool.filter((item) => item.id !== previousQuestionId); + const sorted = remaining.sort( + (a, b) => + Math.abs(a.difficulty - desiredDifficulty) - Math.abs(b.difficulty - desiredDifficulty), + ); + return sorted[0] ?? null; +}; + +export function AdaptiveTesting() { + const [activeQuestion, setActiveQuestion] = useState(null); + const [history, setHistory] = useState<{ + question: AssessmentQuestion; + answer: string; + correct: boolean; + }[]>([]); + const [answerDraft, setAnswerDraft] = useState(''); + const [difficultyTarget, setDifficultyTarget] = useState(3); + const [isRunning, setIsRunning] = useState(false); + + const startAssessment = () => { + setHistory([]); + setDifficultyTarget(3); + setAnswerDraft(''); + setActiveQuestion(SAMPLE_QUESTIONS[0]); + setIsRunning(true); + }; + + const currentDifficultyLabel = (value: number) => + value <= 2 ? 'Easy' : value === 3 ? 'Medium' : 'Hard'; + + const submitAnswer = () => { + if (!activeQuestion) return; + const correct = getCorrectAnswer(activeQuestion, answerDraft); + const nextDifficulty = Math.max(1, Math.min(5, activeQuestion.difficulty + (correct ? 1 : -1))); + const nextQuestion = pickNextQuestion(SAMPLE_QUESTIONS, activeQuestion.id, nextDifficulty); + + setHistory((current) => [ + ...current, + { question: activeQuestion, answer: answerDraft, correct }, + ]); + setDifficultyTarget(nextDifficulty); + setAnswerDraft(''); + setActiveQuestion(nextQuestion); + if (!nextQuestion) { + setIsRunning(false); + } + }; + + const totalScore = useMemo( + () => history.reduce((sum, item) => sum + (item.correct ? item.question.points : 0), 0), + [history], + ); + + const maxPossible = useMemo( + () => history.reduce((sum, item) => sum + item.question.points, 0), + [history], + ); + + const progress = useMemo( + () => (history.length / SAMPLE_QUESTIONS.length) * 100, + [history.length], + ); + + return ( +
+
+
+

Adaptive Testing

+

An intelligent assessment mode that adjusts question difficulty in real time based on performance.

+
+ +
+ +
+
+
+
+
+
Current difficulty goal
+
{currentDifficultyLabel(difficultyTarget)}
+
+
+ Target level {difficultyTarget} +
+
+
+ + {!isRunning && history.length === 0 ? ( +
+ Click “Start adaptive session” to practice a dynamically curated quiz path. +
+ ) : activeQuestion ? ( +
+
+
+
Question {history.length + 1} of {SAMPLE_QUESTIONS.length}
+
{activeQuestion.text}
+
+
Difficulty {activeQuestion.difficulty}
+
+ +
+ {activeQuestion.type === 'multiple-choice' ? ( +
+ {activeQuestion.options.map((option) => ( + + ))} +
+ ) : activeQuestion.type === 'true-false' ? ( +
+ {(['true', 'false'] as const).map((value) => ( + + ))} +
+ ) : activeQuestion.type === 'code-challenge' ? ( +
+
{activeQuestion.codeTemplate}
+