feat: implement learning module page with AI-generated content and feedback workflow
All checks were successful
On Push to Main / test (push) Successful in 29s
On Push to Main / publish (push) Successful in 58s
On Push to Main / deploy-dev (push) Successful in 1m34s

This commit is contained in:
RaymondVerhoef
2026-05-17 17:53:09 +02:00
parent 5b37c04588
commit 5659ff6726
4 changed files with 6 additions and 78 deletions

View File

@@ -6,7 +6,7 @@
import React, { useState, useEffect, useRef } from 'react';
import {
ChevronLeft, ChevronRight, BookOpen, Presentation,
Mic, CheckCircle, Volume2, VolumeX, BarChart2
CheckCircle, BarChart2
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import Card from './Card';
@@ -16,12 +16,11 @@ import Button from './Button';
const MODES = [
{ key: 'article', icon: BookOpen, label: 'Article' },
{ key: 'slides', icon: Presentation, label: 'Slides' },
{ key: 'podcast', icon: Mic, label: 'Podcast' },
{ key: 'infographic', icon: BarChart2, label: 'Infographic' },
];
const LearningContentViewer = ({ content, topic, initialMode = 'article', onGenerate, isLoading }) => {
const populatedModes = MODES.filter(m => m.key === 'podcast' ? !!content?.podcastScript : !!content?.[m.key]);
const populatedModes = MODES.filter(m => !!content?.[m.key]);
const defaultMode = populatedModes.length > 0 ? populatedModes[0].key : initialMode;
const [activeMode, setActiveMode] = useState(defaultMode);
@@ -64,7 +63,7 @@ const LearningContentViewer = ({ content, topic, initialMode = 'article', onGene
transition={{ duration: 0.2 }}
>
{(() => {
const isPopulated = activeMode === 'podcast' ? !!content?.podcastScript : !!content?.[activeMode];
const isPopulated = !!content?.[activeMode];
if (!isPopulated) {
return (
<Card className="border border-bg-warm text-center py-16">
@@ -79,7 +78,6 @@ const LearningContentViewer = ({ content, topic, initialMode = 'article', onGene
}
if (activeMode === 'article') return <ArticleView content={content.article} />;
if (activeMode === 'slides') return <SlidesView slides={content.slides} />;
if (activeMode === 'podcast') return <PodcastView script={content.podcastScript} topicLabel={topic.label} />;
if (activeMode === 'infographic') return <InfographicView data={content.infographic} topicLabel={topic.label} />;
return null;
})()}
@@ -180,66 +178,6 @@ const SlidesView = ({ slides }) => {
);
};
/* ── Podcast Renderer ─────────────────────────────────────── */
const PodcastView = ({ script, topicLabel }) => {
const [isPlaying, setIsPlaying] = useState(false);
const utteranceRef = useRef(null);
const togglePlayback = () => {
if (!('speechSynthesis' in window)) {
alert('Text-to-speech is not supported by your browser.');
return;
}
if (isPlaying) {
window.speechSynthesis.cancel();
setIsPlaying(false);
} else {
const utterance = new SpeechSynthesisUtterance(script);
utterance.lang = 'en-US';
utterance.rate = 0.95;
utterance.onend = () => setIsPlaying(false);
utteranceRef.current = utterance;
window.speechSynthesis.speak(utterance);
setIsPlaying(true);
}
};
useEffect(() => () => window.speechSynthesis?.cancel(), []);
return (
<Card className="border border-bg-warm">
<div className="flex items-center gap-4 mb-6 pb-6 border-b border-bg-warm">
<div className="w-16 h-16 rounded-full bg-teal flex items-center justify-center flex-shrink-0">
<Mic size={28} className="text-white" />
</div>
<div>
<p className="text-xs text-fg-muted uppercase tracking-wider mb-1">Podcast Episode</p>
<h3 className="text-xl font-bold">{topicLabel}</h3>
</div>
<button
onClick={togglePlayback}
className={`ml-auto flex items-center gap-2 px-5 py-2.5 rounded-full font-medium transition-all ${
isPlaying ? 'bg-red-50 text-red-600 border border-red-200' : 'bg-teal text-white hover:bg-teal/90'
}`}
>
{isPlaying ? <><VolumeX size={18} /> Stop</> : <><Volume2 size={18} /> Play</>}
</button>
</div>
{isPlaying && (
<div className="flex items-center gap-2 mb-6 p-3 bg-teal/5 rounded-[var(--r-sm)] border border-teal/20">
<div className="flex gap-1 items-end h-6">
{[0.3, 0.7, 1, 0.6, 0.4, 0.8, 0.5].map((h, i) => (
<div key={i} className="w-1 bg-teal rounded-full animate-pulse" style={{ height: `${h * 24}px`, animationDelay: `${i * 0.1}s` }} />
))}
</div>
<span className="text-sm text-teal font-medium ml-2">Now playing...</span>
</div>
)}
<h4 className="text-sm font-semibold text-fg-muted uppercase tracking-wider mb-3">Script</h4>
<p className="leading-relaxed whitespace-pre-wrap text-fg">{script}</p>
</Card>
);
};
/* ── Infographic Renderer ─────────────────────────────────── */
const InfographicView = ({ data, topicLabel }) => {