diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 50a6df4..6829d44 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,3 +1,106 @@
+import { useState, useEffect, useCallback } from 'react'
+import type { AppScreen, Exam, ExamState, HistoryEntry, Tweaks } from './types'
+import { gradeQuestion } from './utils/grading'
+import { loadState, saveState, clearState, loadHistory, appendHistory, clearHistory } from './utils/storage'
+import Landing from './components/Landing'
+import ExamRunner from './components/ExamRunner'
+import Results from './components/Results'
+import TweaksPanel from './components/TweaksPanel'
+
+const DEFAULT_TWEAKS: Tweaks = { theme: 'light', fontSize: 'medium', feedbackMode: 'auto' }
+
export default function App() {
- return
+ const [screen, setScreen] = useState('landing')
+ const [exam, setExam] = useState(null)
+ const [examState, setExamState] = useState(null)
+ const [history, setHistory] = useState(() => loadHistory())
+ const [tweaks, setTweaks] = useState(DEFAULT_TWEAKS)
+ const [tweaksOpen, setTweaksOpen] = useState(false)
+
+ useEffect(() => {
+ document.documentElement.dataset['theme'] = tweaks.theme
+ const sizeMap: Record = { small: '14px', medium: '15px', large: '17px' }
+ document.body.style.fontSize = sizeMap[tweaks.fontSize] ?? '15px'
+ }, [tweaks])
+
+ useEffect(() => {
+ if (screen === 'exam' && exam && examState) saveState({ exam, state: examState })
+ }, [exam, examState, screen])
+
+ const updateTweak = (key: K, value: Tweaks[K]) =>
+ setTweaks((t) => ({ ...t, [key]: value }))
+
+ const loadExam = (data: Exam) => {
+ const fm = tweaks.feedbackMode === 'auto' ? (data.feedbackMode ?? 'end') : tweaks.feedbackMode
+ setExam(data)
+ setExamState({ currentIdx: 0, answers: {}, flags: {}, revealed: {}, startedAt: Date.now(), feedbackMode: fm })
+ setScreen('exam')
+ }
+
+ const resume = () => {
+ const saved = loadState()
+ if (saved?.exam && saved?.state) { setExam(saved.exam); setExamState(saved.state); setScreen('exam') }
+ }
+
+ const submit = useCallback(() => {
+ if (!exam || !examState) return
+ const completedAt = Date.now()
+ const elapsedMs = completedAt - examState.startedAt
+ const graded = exam.questions.map((q) => gradeQuestion(q, examState.answers[q.id]))
+ const entry: HistoryEntry = {
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
+ title: exam.title,
+ completedAt,
+ elapsedMs,
+ gradedCount: graded.filter((r) => r.status !== 'ungraded').length,
+ correctCount: graded.filter((r) => r.correct).length,
+ totalPoints: exam.questions.reduce((a, q) => a + (q.points ?? 1), 0),
+ scorePoints: graded.reduce((a, r) => a + r.points, 0),
+ passingScore: exam.passingScore ?? 60,
+ exam,
+ state: { ...examState, completedAt },
+ }
+ appendHistory(entry)
+ setHistory(loadHistory())
+ clearState()
+ setExamState((s) => s ? { ...s, completedAt } : s)
+ setScreen('results')
+ }, [exam, examState])
+
+ const exit = () => { setScreen('landing'); setExam(null); setExamState(null) }
+ const restart = () => { if (exam) loadExam(exam) }
+ const reviewAttempt = (entry: HistoryEntry) => { setExam(entry.exam); setExamState(entry.state); setScreen('review') }
+ const clearAllHistory = () => { if (confirm('Clear all past attempts?')) { clearHistory(); setHistory([]) } }
+
+ return (
+
+
+
+
+ BrainWave
+ / practice
+
+
+ {screen === 'exam' && exam && (
+ <>
+ {exam.title}
+
+ >
+ )}
+
+
+
+
+ {screen === 'landing' && (
+
+ )}
+ {screen === 'exam' && exam && examState && (
+
+ )}
+ {(screen === 'results' || screen === 'review') && exam && examState && (
+
+ )}
+ {tweaksOpen &&
setTweaksOpen(false)} />}
+
+ )
}
diff --git a/frontend/src/components/TweaksPanel.tsx b/frontend/src/components/TweaksPanel.tsx
new file mode 100644
index 0000000..626cdfb
--- /dev/null
+++ b/frontend/src/components/TweaksPanel.tsx
@@ -0,0 +1,57 @@
+import type { Tweaks, ThemeMode, FontSize, FeedbackTweak } from '../types'
+
+interface Props {
+ tweaks: Tweaks
+ update: (key: K, value: Tweaks[K]) => void
+ onClose: () => void
+}
+
+function Seg({ tkey, options, tweaks, update }: {
+ tkey: keyof Tweaks
+ options: { value: string; label: string }[]
+ tweaks: Tweaks
+ update: Props['update']
+}) {
+ return (
+
+ {options.map((opt) => (
+
+ ))}
+
+ )
+}
+
+export default function TweaksPanel({ tweaks, update, onClose }: Props) {
+ return (
+
+ )
+}