Feat: microlearning implementation

This commit is contained in:
RaymondVerhoef
2026-05-25 18:32:45 +02:00
parent 18e98380d9
commit 042dfb2d92
4 changed files with 386 additions and 59 deletions

View File

@@ -1,83 +1,123 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import { Loader, BookOpen, Target, Layers, MessageCircle } from 'lucide-react';
import { useMicroLearnings } from '../../hooks/useMicroLearnings';
import MicroLearningContainer from './MicroLearningContainer';
import Button from '../ui/Button';
import Card from '../ui/Card';
const TYPE_LABELS = {
'concept_explainer': 'Concept Explainer',
'scenario_quiz': 'Scenario Quiz',
'flashcard_set': 'Flashcard Set',
'reflection_prompt': 'Reflection Prompt'
};
const TYPE_DESCRIPTIONS = {
'concept_explainer': 'Read a structured explanation to understand the concept.',
'scenario_quiz': 'Apply your knowledge in a realistic workplace scenario.',
'flashcard_set': 'Test your recall with a set of quick flashcards.',
'reflection_prompt': 'Connect the topic to your own professional experience.'
};
const TYPES = [
{
key: 'concept_explainer',
label: 'Concept Explainer',
description: 'Read a structured explanation to understand the concept.',
icon: BookOpen,
},
{
key: 'scenario_quiz',
label: 'Scenario Quiz',
description: 'Apply your knowledge in a realistic workplace scenario.',
icon: Target,
},
{
key: 'flashcard_set',
label: 'Flashcard Set',
description: 'Test your recall with a set of quick flashcards.',
icon: Layers,
},
{
key: 'reflection_prompt',
label: 'Reflection Prompt',
description: 'Connect the topic to your own professional experience.',
icon: MessageCircle,
},
];
export default function MicroLearningSelector({ topicId, sessionWeek, onTopicCompleted }) {
const { getMicroLearningsByTopic } = useMicroLearnings();
const [availableMLs, setAvailableMLs] = useState([]);
const { getOrGenerate } = useMicroLearnings();
const [selectedML, setSelectedML] = useState(null);
const [loading, setLoading] = useState(true);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchMLs = async () => {
setLoading(true);
const data = await getMicroLearningsByTopic(topicId);
setAvailableMLs(data);
const handleSelection = async (type) => {
setLoading(true);
setError(null);
try {
const record = await getOrGenerate(topicId, type);
setSelectedML(record);
} catch (err) {
console.error('[MicroLearningSelector] Generation failed:', err);
setError(err.message || 'Failed to generate content. Please try again.');
} finally {
setLoading(false);
};
if (topicId) {
fetchMLs();
}
}, [topicId]);
const handleSelection = (ml) => {
setSelectedML(ml);
};
if (loading) return <div className="text-slate-500 text-center py-8">Loading learning formats...</div>;
if (availableMLs.length === 0) {
return <div className="text-slate-500 text-center py-8">No micro learnings available for this topic yet.</div>;
// Loading state while AI generates
if (loading) {
return (
<Card className="w-full text-center py-16">
<Loader size={48} className="mx-auto text-teal animate-spin mb-4" />
<p className="font-medium text-lg">AI is generating your learning module</p>
<p className="text-fg-muted text-sm mt-2">This may take 1030 seconds.</p>
</Card>
);
}
// Error state
if (error) {
return (
<Card className="w-full border border-red-200 bg-red-50 text-red-900 p-6">
<p className="font-bold mb-1">Generation failed</p>
<p className="text-sm mb-4">{error}</p>
<button
onClick={() => setError(null)}
className="text-sm font-medium text-red-700 hover:text-red-900 underline"
>
Back to selection
</button>
</Card>
);
}
// Render selected micro learning
if (selectedML) {
return (
<div className="space-y-4">
<button
onClick={() => setSelectedML(null)}
<button
onClick={() => setSelectedML(null)}
className="text-fg-muted hover:text-teal mb-4 text-sm font-medium"
>
Back to selection
</button>
<MicroLearningContainer
microLearning={selectedML}
sessionWeek={sessionWeek}
onCompletedSuccessfully={onTopicCompleted}
<MicroLearningContainer
microLearning={selectedML}
sessionWeek={sessionWeek}
onCompletedSuccessfully={onTopicCompleted}
/>
</div>
);
}
// Type selection menu — always shows all 4 types
return (
<Card className="w-full">
<div className="mb-6 pb-4 border-b border-bg-warm">
<h2 className="text-xl font-bold">Choose a Learning Format</h2>
<p className="text-sm text-fg-muted mt-1">Select how you want to engage with this topic.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{availableMLs.map((ml) => (
<div
key={ml.id}
className="cursor-pointer border-2 border-bg-warm rounded-[var(--r-md)] p-6 hover:border-teal hover:bg-teal/5 transition-all"
onClick={() => handleSelection(ml)}
{TYPES.map(({ key, label, description, icon: Icon }) => (
<div
key={key}
className="cursor-pointer border-2 border-bg-warm rounded-[var(--r-md)] p-6 hover:border-teal hover:bg-teal/5 transition-all group"
onClick={() => handleSelection(key)}
>
<h3 className="font-bold text-lg mb-2 text-teal">{TYPE_LABELS[ml.type] || ml.type}</h3>
<p className="text-sm text-fg-muted">{TYPE_DESCRIPTIONS[ml.type]}</p>
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-full bg-teal/10 flex items-center justify-center flex-shrink-0 group-hover:bg-teal/20 transition-colors">
<Icon size={20} className="text-teal" />
</div>
<h3 className="font-bold text-lg text-teal">{label}</h3>
</div>
<p className="text-sm text-fg-muted">{description}</p>
</div>
))}
</div>