diff --git a/frontend/src/components/Landing.tsx b/frontend/src/components/Landing.tsx index ea9c10f..2295775 100644 --- a/frontend/src/components/Landing.tsx +++ b/frontend/src/components/Landing.tsx @@ -50,6 +50,327 @@ export default function Landing({ onLoadExam, history, onResume, onClearHistory, } } + const downloadInstructions = () => { + const content = `BRAINWAVE EXAM JSON — FORMAT GUIDE +==================================== +Give this document to your preferred LLM (ChatGPT, Claude, Gemini, etc.) along with your exam content, +and ask it to produce a valid JSON file you can upload to BrainWave. + +──────────────────────────────────── +TOP-LEVEL STRUCTURE +──────────────────────────────────── +{ + "title": "string (REQUIRED) — The name of the exam", + "subtitle": "string (optional) — A short description shown under the title", + "author": "string (optional) — Who created the exam", + "timeLimitMinutes": number | null (optional) — Time limit in minutes, or null/omit for untimed, + "feedbackMode": "end" | "immediate" (optional, default: "end") + - "end": students see all feedback after submitting the full exam + - "immediate": feedback is shown after each question is answered, + "passingScore": number (optional) — Passing percentage, e.g. 70 means 70%, + "categories": ["string", ...] (optional) — Category labels for organizing questions, + "questions": [ ... ] (REQUIRED) — Array of question objects (see below) +} + +──────────────────────────────────── +QUESTION OBJECT (common fields) +──────────────────────────────────── +Every question MUST have these fields: + "id": "string" — Unique identifier, e.g. "q1", "q2", etc. + "type": "string" — One of the supported types listed below + "prompt": "string" — The question text (supports LaTeX with $...$ or $$...$$) + +Optional fields available on ALL question types: + "category": "string" — Which category this question belongs to + "points": number — Point value (default: 1) + "explanation": "string" — Explanation shown after grading (supports LaTeX) + "image": { "src": "URL", "alt": "string", "caption": "string" } — Attach an image + "audio": { "src": "URL" } — Attach an audio clip + "code": { "language": "string", "content": "string" } — Show a code block + "table": { "headers": ["Col1", "Col2"], "rows": [["a","b"], ["c","d"]] } — Show a data table + "svg": "string" — Raw SVG markup to display + +──────────────────────────────────── +QUESTION TYPES +──────────────────────────────────── + +1. MULTIPLE CHOICE ("multiple-choice") + Student picks ONE correct answer from a list. + + Required fields: + "choices": [{ "id": "a", "text": "Option A" }, { "id": "b", "text": "Option B" }, ...] + "answer": "a" ← the id of the correct choice + + Example: + { + "id": "q1", + "type": "multiple-choice", + "prompt": "What is the capital of France?", + "choices": [ + { "id": "a", "text": "London" }, + { "id": "b", "text": "Paris" }, + { "id": "c", "text": "Berlin" } + ], + "answer": "b" + } + +2. MULTIPLE SELECT ("multiple-select") + Student selects ALL correct answers (checkboxes). + + Required fields: + "choices": [{ "id": "a", "text": "Option A" }, ...] + "answer": ["a", "c"] ← array of correct choice ids + + Example: + { + "id": "q2", + "type": "multiple-select", + "prompt": "Which are prime numbers?", + "choices": [ + { "id": "a", "text": "2" }, + { "id": "b", "text": "4" }, + { "id": "c", "text": "7" }, + { "id": "d", "text": "9" } + ], + "answer": ["a", "c"] + } + +3. SHORT ANSWER ("short-answer") + Student types a short text answer. + + Required fields: + "answer": "string" — The correct answer + + Optional fields: + "acceptedForms": ["alt1", "alt2"] — Other accepted answers + "caseSensitive": true|false (default: false) + "ignorePunctuation": true|false (default: false) + "ignoreAccents": true|false (default: false) + + Example: + { + "id": "q3", + "type": "short-answer", + "prompt": "What element has the chemical symbol 'O'?", + "answer": "Oxygen", + "caseSensitive": false + } + +4. TRANSLATION ("translation") + Student translates text between languages. + + Required fields: + "answer": "string" — The correct translation + "sourceLanguage": "string" — e.g. "English" + "targetLanguage": "string" — e.g. "Spanish" + + Optional fields: + "acceptedForms": ["alt translation 1", "alt translation 2"] + "caseSensitive": true|false + "ignorePunctuation": true|false + "ignoreAccents": true|false + + Example: + { + "id": "q4", + "type": "translation", + "sourceLanguage": "English", + "targetLanguage": "Spanish", + "prompt": "Translate: \\"Good morning, how are you?\\"", + "answer": "Buenos días, ¿cómo estás?", + "acceptedForms": ["Buenos dias, como estas"], + "caseSensitive": false, + "ignorePunctuation": true, + "ignoreAccents": true + } + +5. MATH INPUT ("math-input") + Student enters a LaTeX math expression. + + Required fields: + "answer": "string" — Correct LaTeX expression + + Optional fields: + "acceptedForms": ["alt LaTeX 1", "alt LaTeX 2"] + + Example: + { + "id": "q5", + "type": "math-input", + "prompt": "Compute the derivative of $x^3$.", + "answer": "3x^2", + "acceptedForms": ["3x^{2}", "3 x^2"] + } + +6. ESSAY ("essay") + Student writes a long-form response. Graded manually (always marked "ungraded"). + + Optional fields: + "minWords": number — Minimum word count required + "rubric": ["criterion 1", "criterion 2"] — Grading rubric shown during review + + Example: + { + "id": "q6", + "type": "essay", + "prompt": "Discuss the causes of World War I.", + "points": 10, + "minWords": 100, + "rubric": [ + "Mentions alliance systems", + "Discusses assassination of Archduke Franz Ferdinand", + "Addresses nationalism and imperialism", + "Well-structured argument" + ] + } + +7. MATCH ("match") + Student matches items from a left column to items in a right column. + + Required fields: + "left": [{ "id": "l1", "text": "Item A" }, { "id": "l2", "text": "Item B" }, ...] + "right": [{ "id": "r1", "text": "Match 1" }, { "id": "r2", "text": "Match 2" }, ...] + "answer": { "l1": "r2", "l2": "r1" } ← maps each left id to its correct right id + + Example: + { + "id": "q7", + "type": "match", + "prompt": "Match the country to its capital.", + "left": [ + { "id": "l1", "text": "France" }, + { "id": "l2", "text": "Japan" }, + { "id": "l3", "text": "Brazil" } + ], + "right": [ + { "id": "r1", "text": "Brasília" }, + { "id": "r2", "text": "Tokyo" }, + { "id": "r3", "text": "Paris" } + ], + "answer": { "l1": "r3", "l2": "r2", "l3": "r1" } + } + +8. ORDER ("order") + Student arranges items in the correct sequence (drag & drop). + + Required fields: + "items": [{ "id": "s1", "text": "Step 1" }, { "id": "s2", "text": "Step 2" }, ...] + "answer": ["s1", "s2", ...] ← array of item ids in the correct order + + Example: + { + "id": "q8", + "type": "order", + "prompt": "Put these historical events in chronological order.", + "items": [ + { "id": "e1", "text": "Moon landing (1969)" }, + { "id": "e2", "text": "Fall of the Berlin Wall (1989)" }, + { "id": "e3", "text": "World Wide Web invented (1991)" }, + { "id": "e4", "text": "First iPhone released (2007)" } + ], + "answer": ["e1", "e2", "e3", "e4"] + } + +──────────────────────────────────── +LATEX SUPPORT +──────────────────────────────────── +Use LaTeX math in prompts, choices, and explanations: + - Inline math: $x^2 + y^2 = z^2$ + - Display math: $$\\\\int_0^1 x^2 \\\\, dx = \\\\frac{1}{3}$$ + +Remember to double-escape backslashes in JSON strings: + "prompt": "Evaluate $\\\\frac{d}{dx} x^2$" + +──────────────────────────────────── +MEDIA ATTACHMENTS +──────────────────────────────────── +You can attach images, audio, code blocks, tables, and SVGs to any question. +Use publicly accessible URLs for image and audio sources. + +Image: "image": { "src": "https://example.com/photo.jpg", "alt": "Description", "caption": "Optional caption" } +Audio: "audio": { "src": "https://example.com/clip.mp3" } +Code: "code": { "language": "python", "content": "def hello():\\n print('hi')" } +Table: "table": { "headers": ["Name", "Value"], "rows": [["A", "1"], ["B", "2"]] } + +──────────────────────────────────── +COMPLETE MINI EXAMPLE +──────────────────────────────────── +{ + "title": "Biology 101 — Cell Structure Quiz", + "subtitle": "Chapters 3-4 review", + "author": "Prof. Smith", + "timeLimitMinutes": 15, + "feedbackMode": "end", + "passingScore": 70, + "categories": ["Cells", "Organelles"], + "questions": [ + { + "id": "q1", + "type": "multiple-choice", + "category": "Cells", + "points": 2, + "prompt": "Which organelle is known as the 'powerhouse of the cell'?", + "choices": [ + { "id": "a", "text": "Nucleus" }, + { "id": "b", "text": "Mitochondria" }, + { "id": "c", "text": "Ribosome" }, + { "id": "d", "text": "Golgi apparatus" } + ], + "answer": "b", + "explanation": "Mitochondria produce ATP through cellular respiration." + }, + { + "id": "q2", + "type": "short-answer", + "category": "Organelles", + "points": 1, + "prompt": "What is the gel-like fluid inside a cell called?", + "answer": "cytoplasm", + "acceptedForms": ["Cytoplasm", "cytosol"], + "caseSensitive": false + }, + { + "id": "q3", + "type": "order", + "category": "Cells", + "points": 3, + "prompt": "Order these from smallest to largest:", + "items": [ + { "id": "a", "text": "Atom" }, + { "id": "b", "text": "Molecule" }, + { "id": "c", "text": "Organelle" }, + { "id": "d", "text": "Cell" } + ], + "answer": ["a", "b", "c", "d"] + } + ] +} + +──────────────────────────────────── +TIPS FOR LLM PROMPTING +──────────────────────────────────── +When asking your LLM to create an exam, include: + 1. This document (paste it in full) + 2. The subject matter or source material + 3. How many questions you want + 4. Which question types to use + 5. Desired difficulty level + 6. Whether to include a time limit + +Example prompt: "Using the attached format guide, create a 10-question exam on +World War II. Use a mix of multiple-choice, short-answer, and ordering questions. +Set a 20-minute time limit. Include explanations for each answer." +` + + const blob = new Blob([content], { type: 'text/plain' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'brainwave-exam-format-guide.txt' + a.click() + URL.revokeObjectURL(url) + } + const resumable = loadState() return ( @@ -86,6 +407,16 @@ export default function Landing({ onLoadExam, history, onResume, onClearHistory, /> + + {err && (
{err} diff --git a/frontend/src/index.css b/frontend/src/index.css index 550055f..a488f0a 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -275,6 +275,36 @@ input, textarea, select { font-family: inherit; font-size: inherit; color: inher font-size: 13.5px; } +.download-instructions-btn { + display: flex; + align-items: center; + gap: 8px; + margin-top: 12px; + width: 100%; + justify-content: center; + font-size: 12.5px; + color: var(--ink-3); + padding: 10px 14px; + border-radius: var(--r-md); + transition: all 150ms ease; +} +.download-instructions-btn:hover { + color: var(--accent); + background: var(--accent-weak); +} +.download-instructions-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border: 1.5px solid currentColor; + border-radius: 5px; + font-size: 13px; + line-height: 1; + flex-shrink: 0; +} + .samples h4 { font-size: 11px; font-family: var(--font-mono);