From 4a28b5e0a0c41d49a623242fd89909e13f9de61d Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 11 Sep 2026 14:45:26 +0200 Subject: [PATCH] fix: new question opens the editor page; the bank previews rather than quizzes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things asked for. New question opened a cramped modal in both places it was offered — the question bank and the question manager — while Edit opened the full page. Both now go to /questions/new, carrying where they came from so the back link returns there. CreateQuestionModal had no callers left and is gone. The bank's "Study" action is "Preview", and shows the question whole: the correct option, the per-option reasoning, the explanation and the key points, all at once. Making someone answer first is the right shape for practice and the wrong one in the bank, where the question is being inspected rather than sat. Option reasoning and the explanation render as Markdown there now too, instead of raw text. Option explanations get the same formatting toolbar as the stem — an option's reasoning is prose as well, and often carries a list or table. Frontend 309/309. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN --- docs/TODO.md | 7 + frontend/src/components/QuestionEditors.jsx | 206 ------------------ frontend/src/pages/QuestionBankPage.jsx | 54 ++--- frontend/src/pages/QuestionEditPage.jsx | 14 +- .../src/pages/QuestionEntryPoints.test.jsx | 27 +++ frontend/src/pages/QuestionManagerPage.jsx | 11 +- 6 files changed, 77 insertions(+), 242 deletions(-) create mode 100644 frontend/src/pages/QuestionEntryPoints.test.jsx 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}
} - -
- - 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..." /> - -
- -
- - -
- - {/* Image */} -
- - {form.image_path ? ( -
- Question - -
- ) : ( -
-
- - -
- {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'} /> -
- ))} -
- )} -
- )} -
- -
- -
- {form.options.map((opt, i) => { - const isCorrect = form.correctIndex === i - return ( -
- - 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 && ( - - )} -
- ) - })} -
- {form.options.length < 8 && ( - - )} -
- -
- - 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..." /> - -
- -
- - -
-
-
- ) -} 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 && (
Explanation illustration )} - {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 && ( -
- - -
- )} +
+ +
{/* 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) )} - + {/* The full editor, not a cramped modal — the same page Edit + opens, so writing a question and fixing one are one screen. */} + + Question - + {(q.user_id === user?.id || isModerator) && (