feat: theme-grouped pickers, theme-session content panel, theme/topic naming
All checks were successful
On Push to Main / test (push) Successful in 42s
On Push to Main / publish (push) Successful in 1m17s
On Push to Main / deploy-dev (push) Successful in 1m56s

- Admin Topic Quizzes (was Quizzes): topics are now grouped under
  collapsible theme headers with per-theme counts. Topics without a
  theme fall into a "No theme" bucket rendered last.
- /topic-test picker: eligible topics grouped by theme so learners can
  scan a curriculum theme and drill its topics on demand.
- Fix admin Theme Content panel (was Learning Content): it was empty
  because content moved to the theme_sessions collection in an earlier
  refactor while the panel still queried the legacy per-topic content
  table. Rewrites ContentManager to read db.getAllThemeSessions and
  render the emit_theme_session schema (title, intro, topic sections,
  connections, key takeaways). Adds db.getAllThemeSessions and
  db.deleteThemeSession.
- Disambiguate theme vs topic across the app: the weekly curriculum
  test is now "Theme Test" (nav, page heading, Dashboard card, button,
  explainer); the on-demand bank test stays "Topic Test". Admin nav
  also clarified to "Theme Content" / "Topic Quizzes".

No schema changes. 85/85 tests still pass, build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
RaymondVerhoef
2026-06-02 16:37:50 +02:00
parent 43a71e2110
commit 218f6e7d64
8 changed files with 336 additions and 205 deletions

View File

@@ -87,8 +87,8 @@ const Admin = () => {
const navItems = [
{ key: 'sources', icon: Database, label: 'Sources' },
{ key: 'content', icon: Layers, label: 'Content' },
{ key: 'tests', icon: CheckSquare, label: 'Quizzes' },
{ key: 'content', icon: Layers, label: 'Theme Content' },
{ key: 'tests', icon: CheckSquare, label: 'Topic Quizzes' },
{ key: 'curriculum', icon: CalendarDays, label: 'Curriculum' },
{ key: 'graph', icon: Network, label: 'Graph' },
{ key: 'team', icon: Users, label: 'Team' },
@@ -162,16 +162,16 @@ const Admin = () => {
{activeTab === 'content' && (
<div className="animate-in fade-in duration-300 max-w-4xl mx-auto">
<h1 className="text-3xl text-teal mb-2">Learning Content</h1>
<p className="text-fg-muted mb-8">Manage, refine, or regenerate AI-generated learning modules for each knowledge topic.</p>
<h1 className="text-3xl text-teal mb-2">Theme Content</h1>
<p className="text-fg-muted mb-8">Weekly theme sessions generated by the AI for the active curriculum. Each session covers all topics in that week's theme.</p>
<ContentManager />
</div>
)}
{activeTab === 'tests' && (
<div className="animate-in fade-in duration-300 max-w-4xl mx-auto">
<h1 className="text-3xl text-teal mb-2">Question Banks</h1>
<p className="text-fg-muted mb-8">Pre-generate and review quiz questions for each topic to eliminate loading times for users.</p>
<h1 className="text-3xl text-teal mb-2">Topic Question Banks</h1>
<p className="text-fg-muted mb-8">Pre-generate and review per-topic quiz questions, grouped by theme. These power both the weekly theme test and the on-demand Topic Test.</p>
<TestManager />
</div>
)}

View File

@@ -111,8 +111,8 @@ const Dashboard = () => {
};
const explainerSteps = [
{ icon: BookOpen, title: 'Learn', text: 'Each week you get one topic from the knowledge base. Work through the learning session at your own pace.' },
{ icon: CheckSquare, title: 'Test', text: 'Once your learning session is done, take the 5-question test to lock in what you learned and earn points.' },
{ icon: BookOpen, title: 'Learn', text: 'Each week the curriculum gives you a theme — a small set of related topics from the knowledge base. Work through the theme session at your own pace.' },
{ icon: CheckSquare, title: 'Theme Test', text: 'When your theme session is done, take the 5-question theme test to lock in what you learned and earn points. You can also take an on-demand Topic Test anytime to drill a single topic.' },
{ icon: Trophy, title: 'Climb', text: 'Points add up on the leaderboard. The curriculum runs in perpetual 26-week cycles, so there is always a next step.' },
{ icon: Sparkles, title: 'Ask R42', text: 'R42, your AI study buddy, is on every screen (bottom-right). Ask it anything about your topic or the platform.' },
];
@@ -159,7 +159,7 @@ const Dashboard = () => {
</div>
<p className="text-fg-muted text-sm">
A perpetual learning loop that keeps you current with the company knowledge base.
Use the navigation at the top (Home, Learn, Test, Leaderboard) to move around.
Use the navigation at the top (Home, Learn, Theme Test, Topic Test, Leaderboard) to move around.
</p>
</div>
</div>
@@ -255,21 +255,21 @@ const Dashboard = () => {
<Card className="flex flex-col border border-bg-warm" hoverable>
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-xl">Testing</h3>
<p className="text-fg-muted text-sm mt-1">Weekly test 5 questions</p>
<h3 className="text-xl">Theme Test</h3>
<p className="text-fg-muted text-sm mt-1">This week's theme — 5 questions</p>
</div>
{testResult ? <Tag variant="success">Score: {testResult.percentage}%</Tag> : <Tag variant="default">To Do</Tag>}
</div>
<div className="flex-1 flex items-center justify-center py-6 text-fg-subtle">
{!learnDone ? 'Complete your learning session first' : testResult ? 'Test completed for this week' : 'Ready to test your knowledge'}
{!learnDone ? 'Complete your theme session first' : testResult ? 'Theme test completed for this week' : 'Ready to test your knowledge'}
</div>
{learnDone && !testResult ? (
<Link to="/test" className="mt-auto">
<Button className="w-full">Start Test</Button>
<Button className="w-full">Start Theme Test</Button>
</Link>
) : (
<Link to="/test" className="mt-auto">
<Button variant="outline" className="w-full">{testResult ? 'View Results' : 'Start Test'}</Button>
<Button variant="outline" className="w-full">{testResult ? 'View Results' : 'Start Theme Test'}</Button>
</Link>
)}
</Card>

View File

@@ -180,10 +180,10 @@ const Testen = () => {
<div className="p-4 md:p-8 max-w-2xl mx-auto">
<div className="text-center py-12">
<CheckSquare size={64} className="mx-auto text-teal/30 mb-6" />
<h1 className="text-3xl md:text-4xl font-bold text-teal mb-4">Weekly Test</h1>
<h1 className="text-3xl md:text-4xl font-bold text-teal mb-4">Theme Test</h1>
<p className="text-fg-muted text-lg mb-2">Cycle {getCurriculumCycle(weekNumber)} · Week {getCurriculumWeek(weekNumber)} of 26</p>
<p className="text-fg-muted mb-8 max-w-md mx-auto">
Test your knowledge with 5 AI-generated questions. You have 5 minutes to complete the test. Good luck!
Test your knowledge of this week's theme with 5 AI-generated questions. You have 5 minutes to complete the test. Good luck!
</p>
{error && (

View File

@@ -1,8 +1,29 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import {
Loader, AlertCircle, Trophy, ArrowRight, ArrowLeft,
Clock, CheckCircle, XCircle, BookOpen,
Clock, CheckCircle, XCircle, BookOpen, Layers,
} from 'lucide-react';
// Pure helper: bucket the eligible topics by their `theme` field. Topics with
// no theme land in a single "No theme" group, rendered last. Each bucket is
// alphabetised by topic label for stable display.
function groupEligibleByTheme(topics) {
const buckets = new Map();
for (const t of topics) {
const key = t.theme || '__no_theme__';
if (!buckets.has(key)) buckets.set(key, { theme: t.theme || 'No theme', topics: [] });
buckets.get(key).topics.push(t);
}
const out = [];
let noTheme = null;
for (const [key, val] of buckets.entries()) {
val.topics.sort((a, b) => (a.label || '').localeCompare(b.label || ''));
if (key === '__no_theme__') noTheme = val;
else out.push(val);
}
out.sort((a, b) => a.theme.localeCompare(b.theme));
return noTheme ? [...out, noTheme] : out;
}
import { motion, AnimatePresence } from 'framer-motion';
import { Link } from 'react-router-dom';
import Card from '../components/ui/Card';
@@ -180,23 +201,35 @@ const TopicTest = () => {
<p className="text-sm">An admin needs to add at least {ON_DEMAND_TEST_SIZE} questions to a topic's bank before you can test on it.</p>
</Card>
) : (
<div className="space-y-3">
{topics.map(topic => (
<Card key={topic.id} className="border border-bg-warm flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<h3 className="font-semibold truncate">{topic.label}</h3>
{topic.theme && <Tag variant="accent" className="text-xs">{topic.theme}</Tag>}
<Tag variant="success" className="text-xs">{topic.bankSize} questions</Tag>
</div>
{topic.description && (
<p className="text-sm text-fg-muted truncate">{topic.description}</p>
)}
<div className="space-y-8">
{groupEligibleByTheme(topics).map(group => (
<section key={group.theme}>
<div className="flex items-center gap-2 mb-3">
<Layers size={16} className="text-teal/70" />
<h3 className="text-lg font-bold text-fg">{group.theme}</h3>
<span className="font-mono text-[11px] text-fg-muted ml-1">
{group.topics.length} {group.topics.length === 1 ? 'topic' : 'topics'}
</span>
</div>
<Button onClick={() => startTest(topic)} className="flex-shrink-0">
Start <ArrowRight size={16} className="ml-1.5" />
</Button>
</Card>
<div className="space-y-3">
{group.topics.map(topic => (
<Card key={topic.id} className="border border-bg-warm flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<h4 className="font-semibold truncate">{topic.label}</h4>
<Tag variant="success" className="text-xs">{topic.bankSize} questions</Tag>
</div>
{topic.description && (
<p className="text-sm text-fg-muted truncate">{topic.description}</p>
)}
</div>
<Button onClick={() => startTest(topic)} className="flex-shrink-0">
Start <ArrowRight size={16} className="ml-1.5" />
</Button>
</Card>
))}
</div>
</section>
))}
</div>
)}