feat: add download instructions functionality and UI integration in Workspace component
Test / backend (push) Successful in 13s
Test / frontend (push) Successful in 26s

This commit is contained in:
LaurensBartelds committed 2026-05-02 18:25:03 +02:00
1 parent a40bfa7838
commit a78fb8efc3
6 files changed
+357 -321

No files matched your search

+1 -321
View File
@@ -4,6 +4,7 @@ import type { Exam, HistoryEntry } from '../types'
import { validateExam, parseExam } from '../utils/validation'
import { loadState } from '../utils/storage'
import { formatDate, formatTime } from '../utils/formatting'
import { downloadInstructions } from '../utils/instructions'
interface Props {
onLoadExam: (exam: Exam, saveToWorkspace?: boolean) => void
@@ -53,327 +54,6 @@ 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 (
+15
View File
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import { useAuth } from '../contexts/AuthContext'
import * as examApi from '../api/examApi'
import * as resultApi from '../api/resultApi'
import { downloadInstructions } from '../utils/instructions'
import type { Exam } from '../types'
import MyExams from './MyExams'
import MyResults from './MyResults'
@@ -96,6 +97,20 @@ export default function Workspace({ onLoadExam }: Props) {
</button>
))}
</nav>
<div className="workspace-sidebar-footer">
<button
type="button"
className="btn btn-ghost"
onClick={downloadInstructions}
title={t('workspace.downloadInstructions')}
>
<span style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 18, height: 18, border: '1.5px solid currentColor', borderRadius: 5, fontSize: 12, lineHeight: 1, flexShrink: 0 }}>
{t('landing.downloadInstructionsIcon')}
</span>
{t('workspace.downloadInstructions')}
</button>
</div>
</aside>
<main className="workspace-main">
+1
View File
@@ -198,6 +198,7 @@
"importing": "Importing…",
"viewAllResults": "View all results",
"recentResults": "Recent Results",
"downloadInstructions": "Download Instructions",
"viewAll": "View all",
"noAttempts": "No attempts yet",
"noAttemptsDesc": "Take an exam to see your results here.",
+1
View File
@@ -197,6 +197,7 @@
"importing": "Importeren…",
"viewAllResults": "Alle resultaten bekijken",
"recentResults": "Recente Resultaten",
"downloadInstructions": "Download Instructies",
"viewAll": "Alles bekijken",
"noAttempts": "Nog geen pogingen",
"noAttemptsDesc": "Maak een examen om hier je resultaten te zien.",
+19
View File
@@ -2350,6 +2350,25 @@ select {
top: 57px;
height: calc(100vh - 57px);
overflow-y: auto;
display: flex;
flex-direction: column;
}
.workspace-sidebar-footer {
margin-top: auto;
padding-top: 16px;
}
.workspace-sidebar-footer .btn {
width: 100%;
justify-content: center;
font-size: 12.5px;
color: var(--ink-3);
}
.workspace-sidebar-footer .btn:hover {
color: var(--accent);
background: var(--accent-weak);
}
.workspace-profile {
+320
View File
@@ -0,0 +1,320 @@
export function 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)
}