diff --git a/backend/app/routers/study_tools.py b/backend/app/routers/study_tools.py
index 8afc849..bfbc108 100644
--- a/backend/app/routers/study_tools.py
+++ b/backend/app/routers/study_tools.py
@@ -59,7 +59,8 @@ def lab_values(include_drafts: bool = False, db: Session = Depends(get_db), user
if not include_drafts:
query = query.filter(LabReference.is_published.is_(True))
# ponytail: bounded personal-project table; add pagination before exceeding 500 entries.
- entries = query.order_by(LabReference.group, LabReference.name, LabReference.age_group).limit(500).all()
+ # Insertion order keeps each test's age rows in their logical sequence.
+ entries = query.order_by(LabReference.group, LabReference.name, LabReference.id).limit(500).all()
card_rows = db.query(LabReferenceCardLink.lab_reference_id, Flashcard.id, Flashcard.front, Flashcard.deck_id,
FlashcardDeck.title).join(
Flashcard, Flashcard.id == LabReferenceCardLink.flashcard_id,
diff --git a/backend/tests/test_study_tools.py b/backend/tests/test_study_tools.py
index d5367c6..9bcbd81 100644
--- a/backend/tests/test_study_tools.py
+++ b/backend/tests/test_study_tools.py
@@ -192,6 +192,11 @@ class StudyToolTests(unittest.TestCase):
self.assertEqual(created.status_code, 201, created.text)
entry_id = created.json()['id']
self.assertEqual(self.client.put(f'/study-tools/lab-values/{entry_id}/cards/999').status_code, 404)
+ second = self.client.post('/study-tools/lab-values', json={**payload, 'age_group': 'z', 'article_id': None, 'article_section_id': None})
+ self.assertEqual(second.status_code, 201, second.text)
+ # Same-name rows keep insertion (logical age) order, not alphabetical age order.
+ names_in_order = [r['name'] + ':' + r['age_group'] for r in self.client.get('/study-tools/lab-values').json() if r['name'] == 'Deep linked']
+ self.assertEqual(names_in_order, ['Deep linked:a', 'Deep linked:z'])
self.assertEqual(self.client.put(f'/study-tools/lab-values/{entry_id}/cards/{card.id}').json()['linked'], True)
self.assertEqual(self.client.put(f'/study-tools/lab-values/{entry_id}/cards/{card.id}').json()['linked'], False)
row = next(r for r in self.client.get('/study-tools/lab-values').json() if r['id'] == entry_id)
diff --git a/frontend/src/components/InProgressQuizzes.jsx b/frontend/src/components/InProgressQuizzes.jsx
index 72184a2..68e3943 100644
--- a/frontend/src/components/InProgressQuizzes.jsx
+++ b/frontend/src/components/InProgressQuizzes.jsx
@@ -16,12 +16,6 @@ export default function InProgressQuizzes() {
setInProgress(prev => prev.filter(a => a.attempt_id !== attemptId))
}
- const copyQuizCode = async (code) => {
- try {
- await navigator.clipboard.writeText(String(code))
- } catch {}
- }
-
if (inProgress.length === 0) return null
return (
@@ -40,15 +34,6 @@ export default function InProgressQuizzes() {
Started {new Date(a.started_at).toLocaleDateString()} · {a.total_questions} questions
-
- Quiz code
-
- {a.quiz_code || a.quiz_id}
-
-
-
diff --git a/frontend/src/pages/QuizPage.jsx b/frontend/src/pages/QuizPage.jsx
index e4a8016..68616ed 100644
--- a/frontend/src/pages/QuizPage.jsx
+++ b/frontend/src/pages/QuizPage.jsx
@@ -361,86 +361,7 @@ function CourseQuizStart({ quiz, onStart, onShareChanged }) {
)
}
-function ModeSelectScreen({ quiz, voices, onStart, onShareChanged }) {
- const [selectedVoice, setSelectedVoice] = useState(voices.find(v => v.is_default)?.id || voices[0]?.id || '')
- const [customTimer, setCustomTimer] = useState(quiz.time_limit_minutes || '')
- const [startError, setStartError] = useState('')
- const [startingMode, setStartingMode] = useState('')
-
- const handleStart = async (mode) => {
- if (mode === 'exam' && customTimer && (!Number.isInteger(Number(customTimer)) || Number(customTimer) < 1)) {
- setStartError('Enter a positive whole number of minutes, or leave the timer blank.')
- return
- }
- const timerMinutes = mode === 'exam' && customTimer ? Number(customTimer) : null
- setStartError('')
- setStartingMode(mode)
- try {
- await onStart(mode, selectedVoice, timerMinutes)
- } catch {
- setStartError('Could not start the quiz. Try again.')
- } finally {
- setStartingMode('')
- }
- }
-
- return (
-
-
-
📝
-
{quiz.title}
-
-
- {quiz.questions_count} questions
- {quiz.time_limit_minutes ? ` · ${quiz.time_limit_minutes} min limit` : ''}
-
-
Choose how to take this quiz:
-
- {[
- { mode: 'study', icon: '📖', label: 'Study Mode', desc: 'Answers & explanations shown as you go', color: '#22c55e', bg: '#f0fdf4' },
- { mode: 'exam', icon: '🎯', label: 'Exam Mode', desc: 'Answers hidden until submitted', color: '#3b82f6', bg: '#eff6ff' },
- ].map(({ mode, icon, label, desc, color, bg }) => (
-
- ))}
-
- {startError && (
-
- {startError}
-
- )}
-
-
-
-
setCustomTimer(e.target.value)}
- placeholder="No time limit"
- onClick={e => e.stopPropagation()}
- style={{ padding: '6px 10px', borderRadius: 8, border: '1px solid var(--border)', fontSize: '0.9rem', width: '100%', background: 'var(--input-bg)', color: 'var(--text)' }} />
-
Auto-submits when timer expires. Timer pauses if you leave and resumes when you come back.
-
-
- {voices.length > 0 && (
-
-
-
-
- )}
-
-
- )
-}
-
+// Mode selection prompt removed: general quizzes start in their own mode automatically.
function getQuizSessionId() {
const key = 'pedshub_quiz_session_id'
try {
@@ -681,7 +602,8 @@ export default function QuizPage() {
const def = voicesRes.data.find(v => v.is_default)
if (def) setSelectedVoice(def.id)
- // Check for saved progress and auto-resume
+ // Check for saved progress and auto-resume; otherwise start straight away
+ // in the quiz's own mode — no second mode prompt.
try {
const progressRes = await api.get('/attempts/progress', {
params: { quiz_id: id },
@@ -689,6 +611,10 @@ export default function QuizPage() {
})
if (progressRes.data) {
await resumeQuiz(progressRes.data, voicesRes.data)
+ return
+ }
+ if (!returnTo && quizRes.data) {
+ await startAttempt(quizRes.data.mode === 'timed' ? 'exam' : 'study', null, null)
}
} catch {
setResumeError('Could not restore your saved attempt. Retry resume before starting; your saved answers have not been replaced.')
@@ -700,8 +626,7 @@ export default function QuizPage() {
return () => clearInterval(timerRef.current)
}, [id, resumeRetry])
- const startQuiz = async (mode, voice, timerMinutes = null) => {
- if (hasStarted.current || resumeError || loading) return
+ const startAttempt = async (mode, voice, timerMinutes = null) => {
hasStarted.current = true
setSelectedVoice(voice)
setStarting(true)
@@ -754,6 +679,11 @@ export default function QuizPage() {
finally { setStarting(false) }
}
+ const startQuiz = async (mode, voice, timerMinutes = null) => {
+ if (hasStarted.current || resumeError || loading) return
+ return startAttempt(mode, voice, timerMinutes)
+ }
+
const timerStarted = timeLeft !== null
useEffect(() => {
if (!timerStarted) return
@@ -934,7 +864,10 @@ const timerStarted = timeLeft !== null
) : returnTo ? (
setQuiz(q => ({ ...q, share_token: token }))} />
) : (
- setQuiz(q => ({ ...q, share_token: token }))} />
+
)}
)
@@ -942,7 +875,6 @@ const timerStarted = timeLeft !== null
const answeredCount = Object.keys(answers).length
const totalCount = questions.length
const isLast = currentIdx === totalCount - 1
- const quizCode = quiz.quiz_code || quiz.id || id
const quizNavigation = (position = 'bottom') => (