diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..2809745 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,41 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "test": "vitest", + "test:run": "vitest run" + }, + "dependencies": { + "dompurify": "^3.4.1", + "katex": "^0.16.45", + "react": "^19.2.5", + "react-dom": "^19.2.5" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@eslint/js": "^9.39.4", + "@rolldown/plugin-babel": "^0.2.3", + "@types/babel__core": "^7.20.5", + "@types/dompurify": "^3.0.5", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "@vitest/coverage-v8": "^4.1.5", + "babel-plugin-react-compiler": "^1.0.0", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.9", + "vitest": "^4.1.5" + } +} diff --git a/frontend/public/samples/calculus-101.json b/frontend/public/samples/calculus-101.json new file mode 100644 index 0000000..08774e5 --- /dev/null +++ b/frontend/public/samples/calculus-101.json @@ -0,0 +1,110 @@ +{ + "title": "Calculus I — Midterm Practice", + "subtitle": "Limits, derivatives, and applications", + "author": "Dept. of Mathematics", + "timeLimitMinutes": 45, + "feedbackMode": "end", + "passingScore": 70, + "categories": ["Limits", "Derivatives", "Applications"], + "questions": [ + { + "id": "q1", + "type": "multiple-choice", + "category": "Limits", + "points": 2, + "prompt": "Evaluate $\\displaystyle\\lim_{x \\to 0} \\frac{\\sin(3x)}{x}$.", + "choices": [ + { "id": "a", "text": "$0$" }, + { "id": "b", "text": "$1$" }, + { "id": "c", "text": "$3$" }, + { "id": "d", "text": "$\\infty$" } + ], + "answer": "c", + "explanation": "Using the standard limit $\\lim_{u\\to 0}\\frac{\\sin u}{u}=1$ with $u=3x$ gives $3\\cdot 1 = 3$." + }, + { + "id": "q2", + "type": "multiple-select", + "category": "Derivatives", + "points": 3, + "prompt": "Which of the following functions are differentiable at $x=0$? Select all that apply.", + "choices": [ + { "id": "a", "text": "$f(x) = |x|$" }, + { "id": "b", "text": "$f(x) = x^2 \\sin(1/x)$ with $f(0)=0$" }, + { "id": "c", "text": "$f(x) = \\sqrt{x}$" }, + { "id": "d", "text": "$f(x) = e^x$" } + ], + "answer": ["b", "d"], + "explanation": "$|x|$ has a corner, $\\sqrt{x}$ is not defined for $x<0$. Both $x^2\\sin(1/x)$ (extended) and $e^x$ are differentiable at 0." + }, + { + "id": "q3", + "type": "math-input", + "category": "Derivatives", + "points": 3, + "prompt": "Compute $\\dfrac{d}{dx}\\left[x^2 \\ln x\\right]$. Enter your answer in LaTeX.", + "answer": "2x\\ln x + x", + "acceptedForms": ["2x\\ln(x) + x", "x(2\\ln x + 1)", "x + 2x\\ln x"], + "explanation": "By the product rule: $2x\\ln x + x^2 \\cdot \\frac{1}{x} = 2x\\ln x + x$." + }, + { + "id": "q4", + "type": "short-answer", + "category": "Limits", + "points": 2, + "prompt": "What is the value of $\\displaystyle\\lim_{x \\to \\infty}\\left(1 + \\frac{1}{x}\\right)^x$? Give the standard symbol.", + "answer": "e", + "acceptedForms": ["E", "e", " e "], + "caseSensitive": false, + "explanation": "This is the definition of Euler's number, $e \\approx 2.71828$." + }, + { + "id": "q5", + "type": "order", + "category": "Applications", + "points": 3, + "prompt": "Order these steps to find the absolute maximum of $f$ on $[a,b]$ (first → last):", + "items": [ + { "id": "s1", "text": "Confirm $f$ is continuous on $[a,b]$" }, + { "id": "s2", "text": "Find critical points in $(a,b)$" }, + { "id": "s3", "text": "Evaluate $f$ at critical points and endpoints" }, + { "id": "s4", "text": "Compare values; the largest is the absolute max" } + ], + "answer": ["s1", "s2", "s3", "s4"], + "explanation": "The Extreme Value Theorem requires continuity on a closed interval; then candidates are critical points and endpoints." + }, + { + "id": "q6", + "type": "match", + "category": "Derivatives", + "points": 4, + "prompt": "Match each function to its derivative.", + "left": [ + { "id": "l1", "text": "$\\sin x$" }, + { "id": "l2", "text": "$\\tan x$" }, + { "id": "l3", "text": "$\\ln x$" }, + { "id": "l4", "text": "$e^{2x}$" } + ], + "right": [ + { "id": "r1", "text": "$\\dfrac{1}{x}$" }, + { "id": "r2", "text": "$\\cos x$" }, + { "id": "r3", "text": "$2e^{2x}$" }, + { "id": "r4", "text": "$\\sec^2 x$" } + ], + "answer": { "l1": "r2", "l2": "r4", "l3": "r1", "l4": "r3" } + }, + { + "id": "q7", + "type": "essay", + "category": "Applications", + "points": 5, + "prompt": "Explain, in your own words, the Mean Value Theorem and give a geometric interpretation.", + "rubric": [ + "States hypotheses (continuous on $[a,b]$, differentiable on $(a,b)$)", + "States conclusion: $\\exists c \\in (a,b)$ with $f'(c) = \\frac{f(b)-f(a)}{b-a}$", + "Explains geometric meaning (tangent parallel to secant)", + "Gives or references an example" + ] + } + ] +} diff --git a/frontend/public/samples/mixed-demo.json b/frontend/public/samples/mixed-demo.json new file mode 100644 index 0000000..1e4ae7f --- /dev/null +++ b/frontend/public/samples/mixed-demo.json @@ -0,0 +1,80 @@ +{ + "title": "Mixed Demo — Every Question Type", + "subtitle": "A tour of every renderer in the platform", + "timeLimitMinutes": null, + "feedbackMode": "end", + "categories": ["Demo"], + "questions": [ + { + "id": "q1", + "type": "multiple-choice", + "category": "Demo", + "points": 1, + "prompt": "Which of the following is the capital of Japan?", + "image": { + "src": "https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?w=800", + "alt": "City skyline", + "caption": "A city skyline — can you identify it?" + }, + "choices": [ + { "id": "a", "text": "Kyoto" }, + { "id": "b", "text": "Osaka" }, + { "id": "c", "text": "Tokyo" }, + { "id": "d", "text": "Hiroshima" } + ], + "answer": "c", + "explanation": "Tokyo has been Japan's capital since 1868." + }, + { + "id": "q2", + "type": "multiple-choice", + "category": "Demo", + "points": 2, + "prompt": "What does the following function return when called with `fib(6)`?", + "code": { + "language": "javascript", + "content": "function fib(n) {\n if (n < 2) return n;\n return fib(n - 1) + fib(n - 2);\n}" + }, + "choices": [ + { "id": "a", "text": "5" }, + { "id": "b", "text": "8" }, + { "id": "c", "text": "13" }, + { "id": "d", "text": "6" } + ], + "answer": "b", + "explanation": "The Fibonacci sequence is 0, 1, 1, 2, 3, 5, 8 — so fib(6) = 8." + }, + { + "id": "q3", + "type": "short-answer", + "category": "Demo", + "points": 1, + "prompt": "In the data table below, which country had the highest GDP growth?", + "table": { + "headers": ["Country", "GDP Growth (%)", "Year"], + "rows": [ + ["Vietnam", "7.2", "2024"], + ["India", "6.8", "2024"], + ["Indonesia", "5.1", "2024"], + ["Philippines", "5.9", "2024"] + ] + }, + "answer": "Vietnam", + "caseSensitive": false + }, + { + "id": "q4", + "type": "essay", + "category": "Demo", + "points": 5, + "prompt": "Describe a time you solved a difficult problem. What approach did you take?", + "minWords": 50, + "rubric": [ + "Specific situation (not generic)", + "Clear problem statement", + "Described methodology", + "Outcome or reflection" + ] + } + ] +} diff --git a/frontend/public/samples/spanish-a2.json b/frontend/public/samples/spanish-a2.json new file mode 100644 index 0000000..cb9dd14 --- /dev/null +++ b/frontend/public/samples/spanish-a2.json @@ -0,0 +1,103 @@ +{ + "title": "Spanish A2 — Vocabulary & Grammar", + "subtitle": "Elementary level practice set", + "timeLimitMinutes": 20, + "feedbackMode": "immediate", + "categories": ["Vocabulary", "Grammar", "Listening"], + "questions": [ + { + "id": "q1", + "type": "multiple-choice", + "category": "Grammar", + "points": 1, + "prompt": "Choose the correct form: _____ a la biblioteca todos los días.", + "choices": [ + { "id": "a", "text": "Voy" }, + { "id": "b", "text": "Va" }, + { "id": "c", "text": "Vamos" }, + { "id": "d", "text": "Van" } + ], + "answer": "a", + "explanation": "First person singular of 'ir' (to go) is 'voy'." + }, + { + "id": "q2", + "type": "translation", + "category": "Vocabulary", + "points": 2, + "sourceLanguage": "English", + "targetLanguage": "Spanish", + "prompt": "Translate: \"I would like a coffee with milk, please.\"", + "answer": "Me gustaría un café con leche, por favor.", + "acceptedForms": [ + "Quisiera un café con leche, por favor", + "Me gustaria un cafe con leche por favor", + "Quiero un café con leche, por favor" + ], + "caseSensitive": false, + "ignorePunctuation": true, + "ignoreAccents": true + }, + { + "id": "q3", + "type": "match", + "category": "Vocabulary", + "points": 4, + "prompt": "Match each Spanish word to its English translation.", + "left": [ + { "id": "l1", "text": "perro" }, + { "id": "l2", "text": "árbol" }, + { "id": "l3", "text": "lluvia" }, + { "id": "l4", "text": "manzana" } + ], + "right": [ + { "id": "r1", "text": "rain" }, + { "id": "r2", "text": "apple" }, + { "id": "r3", "text": "dog" }, + { "id": "r4", "text": "tree" } + ], + "answer": { "l1": "r3", "l2": "r4", "l3": "r1", "l4": "r2" } + }, + { + "id": "q4", + "type": "short-answer", + "category": "Grammar", + "points": 2, + "prompt": "Conjugate 'comer' in the first person plural, present tense.", + "answer": "comemos", + "caseSensitive": false, + "explanation": "Regular -er verb: com + emos = comemos ('we eat')." + }, + { + "id": "q5", + "type": "multiple-select", + "category": "Grammar", + "points": 2, + "prompt": "Which of the following are feminine nouns? Select all that apply.", + "choices": [ + { "id": "a", "text": "la mesa" }, + { "id": "b", "text": "el libro" }, + { "id": "c", "text": "la casa" }, + { "id": "d", "text": "el perro" }, + { "id": "e", "text": "la ciudad" } + ], + "answer": ["a", "c", "e"] + }, + { + "id": "q6", + "type": "order", + "category": "Grammar", + "points": 3, + "prompt": "Arrange these words to form a grammatically correct sentence:", + "items": [ + { "id": "w1", "text": "Mi" }, + { "id": "w2", "text": "hermana" }, + { "id": "w3", "text": "estudia" }, + { "id": "w4", "text": "en" }, + { "id": "w5", "text": "la universidad" } + ], + "answer": ["w1", "w2", "w3", "w4", "w5"], + "explanation": "Subject + verb + prepositional phrase: 'My sister studies at the university.'" + } + ] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..ca9c11d --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,15 @@ +/// +import { defineConfig } from 'vite' +import react, { reactCompilerPreset } from '@vitejs/plugin-react' +import babel from '@rolldown/plugin-babel' + +export default defineConfig({ + plugins: [ + react(), + babel({ presets: [reactCompilerPreset()] }) + ], + test: { + environment: 'node', + globals: true, + }, +})