feat: implement learning module page with AI-generated content and feedback workflow
This commit is contained in:
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user