feat: theme-grouped pickers, theme-session content panel, theme/topic naming
- 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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user