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) && (