pdf-quiz-generator/frontend/src/components/QuestionPreview.jsx
Daniel 77361816d8
Some checks failed
Tests / backend (push) Failing after 4s
Tests / frontend (push) Failing after 26s
Tests / e2e (push) Failing after 37s
fix: the "Add to library" block in a question preview was three faults at once
It said library and meant collection. Its dropdown was always empty,
because the only page that renders this preview passes no collections —
so it read "Choose collection…" with nothing under it. And the "New
library…" input called setCollections, which does not exist in that
component: typing a name and pressing Enter created the collection,
added the question to it, and then threw a ReferenceError. Twelve lines,
none of which worked as written.

Removed rather than repaired. This preview is a moderator's editing
surface; putting a question aside is a learner's act and already lives
in the session and reading flows, where the collections are actually
loaded.

While there, the same word elsewhere: the Collections page called its
own contents "the question libraries you keep", which is the mislabel
the user spotted, on the page named after the other word. A library here
is the article library or an image library; a collection is what a
learner puts aside.

Found by the ped-ai session; the ReferenceError is mine to have missed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-13 15:35:14 +02:00

123 lines
6.7 KiB
JavaScript

import { Suspense } from 'react'
import lazyPage from '../utils/lazyPage'
import { Link } from 'react-router-dom'
import RichText from './RichText'
import QuestionReadingLinks from './QuestionReadingLinks'
import { uploadUrl } from '../utils/uploads'
const TeachChat = lazyPage(() => import('./TeachChat'))
/**
* One question, shown whole, without leaving the page you found it on.
*
* It is a preview, not practice: the correct option, the per-option reasoning,
* the explanation and the key points are all shown at once. Making someone
* answer first is the right shape for a quiz and the wrong one for a list of
* questions being inspected.
*
* Favourites and personal folders are optional and off by default — those
* belong to studying a question, not to looking through the bank.
*/
/* No "Add to library" here any more. It was three faults in one twelve-line
block: it said library and meant collection; its dropdown was always empty,
because the only page that renders this preview passes no collections; and
the "New library…" input called setCollections, which does not exist in this
component — so typing a name and pressing Enter created the collection,
added the question, and then threw a ReferenceError. Putting a question
aside belongs to the reading and session flows, where the collections are
actually loaded; this is a moderator's editing preview. */
export default function QuestionPreview({ question, onClose, isFavorited, onToggleFavorite }) {
return (
<>
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.55)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}
onClick={e => e.target === e.currentTarget && onClose()}>
<div style={{ background: 'var(--card-bg)', borderRadius: 'var(--card-radius)', padding: 24, maxWidth: 600, width: '100%', maxHeight: '90vh', overflowY: 'auto', boxShadow: '0 20px 60px rgba(0,0,0,0.3)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<span style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase' }}>
{question.quiz_title}{question.question_category_name ? ` · ${question.question_category_name}` : ''}
</span>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
{onToggleFavorite && (
<button onClick={() => onToggleFavorite(question.id)}
title={isFavorited ? 'Remove from favorites' : 'Add to favorites'}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '1.4rem', padding: 2, lineHeight: 1 }}>
{isFavorited ? '⭐' : '☆'}
</button>
)}
<button onClick={onClose} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)', fontSize: '1.2rem' }}></button>
</div>
</div>
{/* Markdown, not injected HTML: a stem is educator prose, and the
same renderer the quiz player uses keeps a lab table a table. */}
<div style={{ fontWeight: 600, fontSize: '0.95rem', marginBottom: 16 }}>
<RichText value={question.question_text} />
</div>
{question.image_path && (
<div style={{ margin: '0 0 14px' }}>
<img src={`/uploads/${question.image_path}`} alt="Question illustration"
style={{ maxWidth: '100%', maxHeight: 280, borderRadius: 8, border: '1px solid var(--border)' }}
onError={e => e.target.style.display = 'none'} />
</div>
)}
{question.options && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 16 }}>
{question.options.map((opt, i) => {
const isCorrectOpt = opt === question.correct_answer
return (
<div key={i}
className={`option ${isCorrectOpt ? 'correct' : ''}`}>
<span className="option-letter">{String.fromCharCode(65 + i)}</span>
<span style={{ flex: 1 }}>{opt}</span>
{isCorrectOpt && <span style={{ marginLeft: 'auto', fontSize: '0.8rem', fontWeight: 700, color: 'var(--correct-fg)' }}> Correct</span>}
{question.option_explanations?.[opt] && (
<span style={{ flexBasis: '100%', fontSize: '0.8rem', color: 'var(--text-muted)' }}>
<RichText value={question.option_explanations[opt]} />
</span>
)}
</div>
)
})}
</div>
)}
{(question.explanation || question.explanation_image_path) && (
<div className="explanation">
<strong>Explanation:</strong>
{question.explanation && <div style={{ marginTop: 8 }}><RichText value={question.explanation} /></div>}
{question.explanation_image_path && (
<div style={{ marginTop: 12 }}>
<img src={`/uploads/${question.explanation_image_path}`} alt="Explanation illustration"
style={{ maxWidth: '100%', maxHeight: 280, borderRadius: 8, border: '1px solid var(--border)' }}
onError={e => e.currentTarget.style.display = 'none'} />
</div>
)}
</div>
)}
{(question.key_points || []).length > 0 && (
<div className="explanation" style={{ marginTop: 12 }}>
<strong>Key points</strong>
<ul style={{ margin: '6px 0 0', paddingLeft: 18, fontSize: '0.85rem' }}>
{question.key_points.map((point, i) => (
<li key={i}>
{point.text}
{point.article_id && (
<Link to={`/articles/${point.article_id}${point.article_section_id ? `?section=${point.article_section_id}` : ''}`}
style={{ marginLeft: 8, color: 'var(--primary)', textDecoration: 'none', fontWeight: 600, fontSize: '0.8rem' }}>📖 Read more</Link>
)}
</li>
))}
</ul>
</div>
)}
<QuestionReadingLinks questionId={question.id} />
<div style={{ marginTop: 14, display: 'flex', gap: 8 }}>
<button className="btn btn-secondary btn-sm" onClick={onClose}>Close</button>
</div>
</div>
</div>
{/* AI Tutor — z-index above the modal */}
<Suspense fallback={null}>
<TeachChat question={question} elevated />
</Suspense>
</>
)
}