diff --git a/docs/TODO.md b/docs/TODO.md
index 13e2cbd..512dc49 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -231,6 +231,13 @@ Captured so nothing is lost while the article writing runs.
- [ ] **High-yield / key-exam-info toggles** — mark spans and let the reader show
or hide them.
+## Loose ends
+
+- [ ] **QuestionEditModal is dead code** — nothing has imported it since Edit
+ moved to the full page. Its sibling CreateQuestionModal was removed when
+ its last two callers were replaced; this one was already unreferenced, so
+ it is left for a deliberate decision rather than swept up.
+
## Quiz runner
- [x] **Per-question notes** — done. The notes themselves were already built
diff --git a/frontend/src/components/QuestionEditors.jsx b/frontend/src/components/QuestionEditors.jsx
index d93dad9..7cd2c3e 100644
--- a/frontend/src/components/QuestionEditors.jsx
+++ b/frontend/src/components/QuestionEditors.jsx
@@ -200,209 +200,3 @@ export function QuestionEditModal({ question, categories, onSaved, onClose }) {
)
}
-
-export function CreateQuestionModal({ categories, onCreated, onClose }) {
- const [form, setForm] = useState({ question_text: '', question_type: 'mcq', options: ['', '', '', ''], correct_answer: '', correctIndex: -1, explanation: '', question_category_id: '', image_path: '' })
- const [saving, setSaving] = useState(false)
- const [error, setError] = useState('')
- const [imageTab, setImageTab] = useState('upload') // 'upload' | 'bank'
- const [imageBank, setImageBank] = useState([])
- const [uploadingImage, setUploadingImage] = useState(false)
-
- const setOption = (i, val) => {
- const updated = [...form.options]
- updated[i] = val
- setForm(f => ({ ...f, options: updated, correct_answer: f.correctIndex === i ? val : f.correct_answer }))
- }
-
- const setCorrectIndex = (i) => {
- setForm(f => ({ ...f, correctIndex: i, correct_answer: f.options[i] }))
- }
-
- const removeOption = (i) => {
- setForm(f => {
- const opts = f.options.filter((_, idx) => idx !== i)
- let ci = f.correctIndex
- if (ci === i) ci = -1
- else if (ci > i) ci--
- return { ...f, options: opts, correctIndex: ci, correct_answer: ci >= 0 ? opts[ci] : '' }
- })
- }
-
- const loadImageBank = async () => {
- try {
- const res = await api.get('/questions/images')
- setImageBank(res.data)
- } catch {}
- }
-
- const handleImageUpload = async (e) => {
- const file = e.target.files?.[0]
- if (!file) return
- setUploadingImage(true)
- try {
- const fd = new FormData()
- fd.append('file', file)
- const res = await api.post('/questions/upload-image', fd)
- setForm(f => ({ ...f, image_path: res.data.image_path }))
- } catch (err) {
- setError(apiError(err, 'Image upload failed'))
- } finally {
- setUploadingImage(false)
- }
- }
-
- const save = async () => {
- if (!form.question_text.trim()) return setError('Question text is required')
- const validOpts = form.options.filter(o => o.trim())
- if (validOpts.length < 2) return setError('At least 2 options are required')
- if (form.correctIndex < 0 || !form.correct_answer) return setError('Select the correct answer')
- setSaving(true); setError('')
- try {
- const payload = {
- question_text: form.question_text,
- question_type: form.question_type,
- options: validOpts,
- correct_answer: form.correct_answer,
- explanation: form.explanation || null,
- question_category_id: form.question_category_id ? parseInt(form.question_category_id) : null,
- image_path: form.image_path || null,
- }
- const res = await api.post('/questions/create', payload)
- onCreated(res.data)
- onClose()
- } catch (err) { setError(apiError(err, 'Failed to create question')) }
- finally { setSaving(false) }
- }
-
- return (
-
-
-
-
Create Question
- ✕
-
- {error &&
{error}
}
-
-
- Question Text
- setForm(f => ({ ...f, question_text: e.target.value }))} rows={4}
- placeholder="Loading editor..." style={{ width: '100%', padding: 12, border: '1px solid var(--border)', borderRadius: 8, background: 'var(--input-bg)', color: 'var(--text)', fontSize: '0.9rem', resize: 'vertical', boxSizing: 'border-box' }} />}>
- setForm(f => ({ ...f, question_text: v }))} height={180} placeholder="Write the question..." />
-
-
-
-
- Category
- setForm(f => ({ ...f, question_category_id: e.target.value }))}
- style={{ width: '100%', padding: '10px 14px', border: '1px solid var(--border)', borderRadius: 8, background: 'var(--input-bg)', color: 'var(--text)', fontSize: '0.9rem' }}>
- None
- {categories.map(c => {(c.breadcrumbs || []).map(b => b.name).join(' › ') || c.name} )}
-
-
-
- {/* Image */}
-
-
Image (optional)
- {form.image_path ? (
-
-
-
setForm(f => ({ ...f, image_path: '' }))}
- style={{ position: 'absolute', top: 4, right: 4, background: 'rgba(0,0,0,0.6)', color: '#fff', border: 'none', borderRadius: '50%', width: 24, height: 24, cursor: 'pointer', fontSize: '0.8rem', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>✕
-
- ) : (
-
-
- setImageTab('upload')} style={{ fontSize: '0.78rem' }}>Upload New
- { setImageTab('bank'); loadImageBank() }} style={{ fontSize: '0.78rem' }}>Image Bank
-
- {imageTab === 'upload' && (
-
-
- {uploadingImage && Uploading... }
-
- )}
- {imageTab === 'bank' && (
-
- {imageBank.length === 0 &&
No images in bank yet. Upload one first.
}
- {imageBank.map((img, i) => (
-
setForm(f => ({ ...f, image_path: img.image_path }))}
- style={{ cursor: 'pointer', border: '2px solid var(--border)', borderRadius: 8, overflow: 'hidden', aspectRatio: '1', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--bg)' }}
- onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--primary)'} onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border)'}>
-
e.target.style.display = 'none'} />
-
- ))}
-
- )}
-
- )}
-
-
-
-
- Options — click the letter to mark as correct
-
-
- {form.options.map((opt, i) => {
- const isCorrect = form.correctIndex === i
- return (
-
- setCorrectIndex(i)} title={isCorrect ? 'Correct answer' : 'Click to mark as correct'}
- style={{
- display: 'flex', alignItems: 'center', justifyContent: 'center', width: 32, height: 32,
- borderRadius: '50%', flexShrink: 0, fontSize: '0.8rem', fontWeight: 700, cursor: 'pointer',
- border: isCorrect ? '2px solid var(--correct-fg)' : '2px solid var(--border)',
- background: isCorrect ? 'var(--correct-fg)' : 'transparent',
- color: isCorrect ? 'white' : 'var(--text-muted)',
- transition: 'all 0.15s',
- }}>
- {LETTERS[i]}
-
- setOption(i, e.target.value)}
- placeholder={`Option ${LETTERS[i]}`}
- style={{
- flex: 1, padding: '10px 14px',
- border: `1.5px solid ${isCorrect ? 'var(--correct-bd)' : 'var(--border)'}`,
- borderRadius: 8, background: isCorrect ? 'var(--correct-bg)' : 'var(--input-bg)',
- color: 'var(--text)', fontSize: '0.9rem', fontFamily: 'inherit',
- }} />
- {form.options.length > 2 && (
- removeOption(i)} title="Remove option"
- style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)', fontSize: '1.1rem', padding: '4px 6px' }}
- onMouseEnter={e => e.currentTarget.style.color = '#ef4444'} onMouseLeave={e => e.currentTarget.style.color = 'var(--text-muted)'}>
- ✕
-
- )}
-
- )
- })}
-
- {form.options.length < 8 && (
-
setForm(f => ({ ...f, options: [...f.options, ''] }))}>+ Add option
- )}
-
-
-
- Explanation (optional)
- setForm(f => ({ ...f, explanation: e.target.value }))} rows={3}
- placeholder="Loading editor..." style={{ width: '100%', padding: 12, border: '1px solid var(--border)', borderRadius: 8, background: 'var(--input-bg)', color: 'var(--text)', fontSize: '0.9rem', resize: 'vertical', boxSizing: 'border-box' }} />}>
- setForm(f => ({ ...f, explanation: v }))} height={150} placeholder="Explain why this is the correct answer..." />
-
-
-
-
-
- {saving ? 'Creating...' : 'Create Question'}
-
- Cancel
-
-
-
- )
-}
diff --git a/frontend/src/pages/QuestionBankPage.jsx b/frontend/src/pages/QuestionBankPage.jsx
index f1964a0..6eac268 100644
--- a/frontend/src/pages/QuestionBankPage.jsx
+++ b/frontend/src/pages/QuestionBankPage.jsx
@@ -14,7 +14,6 @@ import { useDialog } from '../hooks/useDialog'
const TeachChat = lazy(() => import('../components/TeachChat'))
const RichEditor = lazy(() => import('../components/RichEditor'))
import QuestionReadingLinks from '../components/QuestionReadingLinks'
-import { CreateQuestionModal } from '../components/QuestionEditors'
const DIFFICULTY_LABEL = { '': 'Any', easy: 'Easy', medium: 'Medium', hard: 'Hard' }
@@ -37,8 +36,16 @@ function stripHtml(html) {
return html.replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\s+/g, ' ').trim()
}
-function QuestionStudyModal({ question, onClose, isFavorited, onToggleFavorite, collections = [] }) {
- const [answered, setAnswered] = useState(null)
+/**
+ * One question, shown whole.
+ *
+ * It used to make you answer before it would reveal anything, which is the
+ * right shape for practice and the wrong one here: in the bank the question is
+ * being inspected, not sat. Everything is shown at once — the correct option,
+ * the per-option reasoning, the explanation and the key points — because that
+ * is what looking at a question means.
+ */
+function QuestionPreviewModal({ question, onClose, isFavorited, onToggleFavorite, collections = [] }) {
return (
<>
{question.options.map((opt, i) => {
- const isSelected = answered === opt
- const hasAnswered = !!answered
const isCorrectOpt = opt === question.correct_answer
return (
!hasAnswered && setAnswered(opt)}
- style={{ cursor: hasAnswered ? 'default' : 'pointer' }}>
+ className={`option ${isCorrectOpt ? 'correct' : ''}`}>
{String.fromCharCode(65 + i)}
{opt}
- {hasAnswered && question.option_explanations?.[opt] && (
- {question.option_explanations[opt]}
+ {isCorrectOpt && ✓ Correct }
+ {question.option_explanations?.[opt] && (
+
+
+
)}
- {hasAnswered && isCorrectOpt && ✓ {isSelected ? 'Your answer' : 'Correct'} }
- {hasAnswered && isSelected && !isCorrectOpt && ✗ Wrong }
)
})}
)}
- {answered && (question.explanation || question.explanation_image_path) && (
+ {(question.explanation || question.explanation_image_path) && (
Explanation:
- {question.explanation &&
{question.explanation}
}
+ {question.explanation &&
}
{question.explanation_image_path && (
)}
- {answered && (question.key_points || []).length > 0 && (
+ {(question.key_points || []).length > 0 && (
Key points
@@ -145,12 +149,9 @@ function QuestionStudyModal({ question, onClose, isFavorited, onToggleFavorite,
}} style={{ padding: '4px 10px', border: '1px solid var(--border)', borderRadius: 6, fontSize: '0.8rem' }} />
- {answered && (
-
- setAnswered(null)}>Try again
- Close
-
- )}
+
+ Close
+
{/* AI Tutor — z-index above the modal */}
@@ -319,7 +320,6 @@ export default function QuestionBankPage() {
const [selectedTagIds, setSelectedTagIds] = useState([])
const [openFacet, setOpenFacet] = useState(null)
const [showTags, setShowTags] = useState(false)
- const [showCreateQuestion, setShowCreateQuestion] = useState(false)
const [showImport, setShowImport] = useState(false)
const [importFile, setImportFile] = useState(null)
const [importing, setImporting] = useState(false)
@@ -619,10 +619,9 @@ export default function QuestionBankPage() {
)
})()}
- {studyQuestion && setStudyQuestion(null)}
+ {studyQuestion && setStudyQuestion(null)}
isFavorited={favorites.includes(studyQuestion.id)} onToggleFavorite={toggleFavorite} />}
{showCreateQuiz && setShowCreateQuiz(false)} onCreated={() => {}} />}
- {showCreateQuestion && { setQuestions(prev => [q, ...prev]); setTotal(t => t + 1) }} onClose={() => setShowCreateQuestion(false)} />}
{/* Import CSV/Excel Modal */}
{showImport && (
@@ -678,7 +677,10 @@ export default function QuestionBankPage() {
Create Quiz ({selectedIds.size} selected)
)}
- setShowCreateQuestion(true)}>+ Question
+ {/* The full editor, not a cramped modal — the same page Edit
+ opens, so writing a question and fixing one are one screen. */}
+ + Question
{ setShowImport(true); setImportFile(null); setImportResult(null) }}>Import CSV/Excel
{qtiImporting ? 'Importing QTI...' : 'Import QTI'}
@@ -847,7 +849,7 @@ export default function QuestionBankPage() {
>
{favorites.includes(q.id) ? '⭐' : '☆'}
- setStudyQuestion(q)}>Study
+ setStudyQuestion(q)}>Preview
{(q.user_id === user?.id || isModerator) && (
{
diff --git a/frontend/src/pages/QuestionEditPage.jsx b/frontend/src/pages/QuestionEditPage.jsx
index e1e1962..3f36eb7 100644
--- a/frontend/src/pages/QuestionEditPage.jsx
+++ b/frontend/src/pages/QuestionEditPage.jsx
@@ -45,6 +45,9 @@ export default function QuestionEditPage({ mode = 'edit' }) {
const [picking, setPicking] = useState(null)
const stemRef = useRef(null)
const explanationRef = useRef(null)
+ // One ref per option, kept across renders so the toolbar can find the
+ // field it belongs to when options are added or reordered.
+ const optionRefs = useRef([])
// Back to wherever you opened this from — the bank with its filters, an
// article, the manager — rather than always to the bank you may not have used.
const { state } = useLocation()
@@ -241,7 +244,16 @@ export default function QuestionEditPage({ mode = 'edit' }) {