feat: the topic behind the right answer, as a chip where the eye already is

Reading links existed and sat at the foot of the whole explanation block, under
everything else, as a headed list. A learner who has just been shown the right
answer is looking at the right answer — so the link now sits directly under it,
as a chip, the way a reference site does it. Outside the option's button rather
than inside it: a link nested in a button is neither, and the click would have
toggled the explanation instead of opening the article.

Also a real bug beside it. Key points were gated on `option_explanations`
being non-empty — the wrong field entirely — so a question with key points and
no per-option reasoning showed none of them. It happens not to bite today
because exactly one question in the bank has either, which is its own finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
This commit is contained in:
Daniel 2026-09-12 20:18:05 +02:00
parent 2d80df8d73
commit 0d41179b30
3 changed files with 52 additions and 3 deletions

View file

@ -9,7 +9,7 @@ import api from '../api/client'
* the article's own sections for it this used to ask for every linked * the article's own sections for it this used to ask for every linked
* article in full, prose and all, to print a title and a heading. * article in full, prose and all, to print a title and a heading.
*/ */
export default function QuestionReadingLinks({ questionId }) { export default function QuestionReadingLinks({ questionId, variant = 'list' }) {
const [links, setLinks] = useState(null) const [links, setLinks] = useState(null)
useEffect(() => { useEffect(() => {
if (!questionId) { setLinks([]); return } if (!questionId) { setLinks([]); return }
@ -17,6 +17,25 @@ export default function QuestionReadingLinks({ questionId }) {
.then(res => setLinks(res.data)).catch(() => setLinks([])) .then(res => setLinks(res.data)).catch(() => setLinks([]))
}, [questionId]) }, [questionId])
if (!links || links.length === 0) return null if (!links || links.length === 0) return null
// Under the right answer in the player, the same links are a row of chips
// one control, plainly a door to an article rather than a headed list,
// which at that point in the page reads as another section of explanation.
if (variant === 'chips') {
return (
<div className="question-reading-chips" data-testid="question-reading">
{links.map(link => (
<Link key={`${link.article_id}-${link.section_id || 'all'}`}
className="question-reading-chip"
to={link.section_id ? `/articles/${link.article_id}?section=${link.section_id}` : `/articles/${link.article_id}`}>
<span aria-hidden="true"></span>
{link.section_title || link.title}
</Link>
))}
</div>
)
}
return ( return (
<div className="question-reading" data-testid="question-reading"> <div className="question-reading" data-testid="question-reading">
<strong>Topic reading</strong> <strong>Topic reading</strong>

View file

@ -2104,6 +2104,14 @@ const timerStarted = timeLeft !== null
</span> </span>
)} )}
</button> </button>
{/* The topic behind the right answer, offered where the
learner is already looking. Outside the option's
button rather than inside it, because a link nested
in a button is neither: the click would toggle the
explanation instead of opening the article. */}
{showCorrect && (
<QuestionReadingLinks questionId={current.id} variant="chips" />
)}
{/* Outside the option, so ruling one out is never {/* Outside the option, so ruling one out is never
mistaken for choosing it. Gone once the question is mistaken for choosing it. Gone once the question is
marked there is nothing left to narrow down. */} marked there is nothing left to narrow down. */}
@ -2183,7 +2191,12 @@ const timerStarted = timeLeft !== null
)} )}
</div> </div>
)} )}
{current.option_explanations && Object.keys(current.option_explanations).length > 0 && ( {/* Gated on key points, not on option explanations. It asked
whether the *other* field was populated, so a question
with key points and no per-option reasoning showed none of
them which is most of the bank, once anybody writes
them. */}
{(current.key_points || []).length > 0 && (
<div className="explanation" style={{ marginTop: 16 }}> <div className="explanation" style={{ marginTop: 16 }}>
<strong>Key points</strong> <strong>Key points</strong>
<ul className="quiz-key-points"> <ul className="quiz-key-points">
@ -2198,7 +2211,6 @@ const timerStarted = timeLeft !== null
</ul> </ul>
</div> </div>
)} )}
<QuestionReadingLinks questionId={current.id} />
{current.question_type === 'fill_blank' && ( {current.question_type === 'fill_blank' && (
<div className="explanation" style={{ marginTop: 12, borderLeftColor: '#22c55e' }}> <div className="explanation" style={{ marginTop: 12, borderLeftColor: '#22c55e' }}>
<strong>Correct Answer:</strong> {current.correct_answer} <strong>Correct Answer:</strong> {current.correct_answer}

View file

@ -932,3 +932,21 @@ body:has(.quiz-player.is-exam-chrome) .site-footer { display: none; }
/* The clocks, at the foot of the rail with everything else about the session /* The clocks, at the foot of the rail with everything else about the session
rather than about the question in front of you. */ rather than about the question in front of you. */
.quiz-rail-foot { display: flex; align-items: center; } .quiz-rail-foot { display: flex; align-items: center; }
/* The topic behind the right answer, in the player. A chip rather than a
headed list: at that point on the page a heading reads as another section of
explanation, and what this is is a door. */
.question-reading-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 4px 42px; }
.question-reading-chip {
display: inline-flex; align-items: center; gap: 8px;
padding: 7px 13px; border: 1px solid var(--border); border-radius: 8px;
background: var(--card-bg); color: var(--text); text-decoration: none;
font-size: 0.85rem; font-weight: 600;
}
.question-reading-chip:hover { border-color: var(--primary); color: var(--primary); }
.question-reading-chip span { color: var(--text-muted); font-size: 0.9rem; }
.question-reading-chip:hover span { color: var(--primary); }
@media (max-width: 640px) {
.question-reading-chips { margin-left: 0; }
}