feat: implement learning module page with AI-generated content and feedback workflow
This commit is contained in:
@@ -26,7 +26,7 @@ const UploadZone = ({ onUploadComplete }) => {
|
|||||||
const [status, setStatus] = useState(null);
|
const [status, setStatus] = useState(null);
|
||||||
|
|
||||||
// GitHub sync state
|
// GitHub sync state
|
||||||
const [githubUrl, setGithubUrl] = useState('https://github.com/respellion/employee-handbook/tree/main/docs');
|
const [githubUrl, setGithubUrl] = useState('https://github.com/respellion/employee-handbook/tree/main/docs/knowledge-base');
|
||||||
const [isFetching, setIsFetching] = useState(false);
|
const [isFetching, setIsFetching] = useState(false);
|
||||||
const [fileList, setFileList] = useState(null);
|
const [fileList, setFileList] = useState(null);
|
||||||
const [syncProgress, setSyncProgress] = useState(null);
|
const [syncProgress, setSyncProgress] = useState(null);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
ChevronLeft, ChevronRight, BookOpen, Presentation,
|
ChevronLeft, ChevronRight, BookOpen, Presentation,
|
||||||
Mic, CheckCircle, Volume2, VolumeX, BarChart2
|
CheckCircle, BarChart2
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import Card from './Card';
|
import Card from './Card';
|
||||||
@@ -16,12 +16,11 @@ import Button from './Button';
|
|||||||
const MODES = [
|
const MODES = [
|
||||||
{ key: 'article', icon: BookOpen, label: 'Article' },
|
{ key: 'article', icon: BookOpen, label: 'Article' },
|
||||||
{ key: 'slides', icon: Presentation, label: 'Slides' },
|
{ key: 'slides', icon: Presentation, label: 'Slides' },
|
||||||
{ key: 'podcast', icon: Mic, label: 'Podcast' },
|
|
||||||
{ key: 'infographic', icon: BarChart2, label: 'Infographic' },
|
{ key: 'infographic', icon: BarChart2, label: 'Infographic' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const LearningContentViewer = ({ content, topic, initialMode = 'article', onGenerate, isLoading }) => {
|
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 defaultMode = populatedModes.length > 0 ? populatedModes[0].key : initialMode;
|
||||||
const [activeMode, setActiveMode] = useState(defaultMode);
|
const [activeMode, setActiveMode] = useState(defaultMode);
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ const LearningContentViewer = ({ content, topic, initialMode = 'article', onGene
|
|||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
>
|
>
|
||||||
{(() => {
|
{(() => {
|
||||||
const isPopulated = activeMode === 'podcast' ? !!content?.podcastScript : !!content?.[activeMode];
|
const isPopulated = !!content?.[activeMode];
|
||||||
if (!isPopulated) {
|
if (!isPopulated) {
|
||||||
return (
|
return (
|
||||||
<Card className="border border-bg-warm text-center py-16">
|
<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 === 'article') return <ArticleView content={content.article} />;
|
||||||
if (activeMode === 'slides') return <SlidesView slides={content.slides} />;
|
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} />;
|
if (activeMode === 'infographic') return <InfographicView data={content.infographic} topicLabel={topic.label} />;
|
||||||
return null;
|
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 ─────────────────────────────────── */
|
/* ── Infographic Renderer ─────────────────────────────────── */
|
||||||
const InfographicView = ({ data, topicLabel }) => {
|
const InfographicView = ({ data, topicLabel }) => {
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ const CONTENT_SCHEMA_SLIDES = `{
|
|||||||
]
|
]
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const CONTENT_SCHEMA_PODCAST = `{
|
|
||||||
"podcastScript": "A natural spoken script of approx. 300 words summarizing the topic as a podcast episode."
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const CONTENT_SCHEMA_INFOGRAPHIC = `{
|
const CONTENT_SCHEMA_INFOGRAPHIC = `{
|
||||||
"infographic": {
|
"infographic": {
|
||||||
@@ -45,7 +43,6 @@ const CONTENT_SCHEMA_INFOGRAPHIC = `{
|
|||||||
const CONTENT_SCHEMA_ALL = `{
|
const CONTENT_SCHEMA_ALL = `{
|
||||||
"article": ${CONTENT_SCHEMA_ARTICLE.replace(/^\{|\}$/g, '').trim()},
|
"article": ${CONTENT_SCHEMA_ARTICLE.replace(/^\{|\}$/g, '').trim()},
|
||||||
"slides": ${CONTENT_SCHEMA_SLIDES.replace(/^\{|\}$/g, '').trim()},
|
"slides": ${CONTENT_SCHEMA_SLIDES.replace(/^\{|\}$/g, '').trim()},
|
||||||
"podcastScript": "A natural spoken script of approx. 300 words summarizing the topic as a podcast episode.",
|
|
||||||
"infographic": ${CONTENT_SCHEMA_INFOGRAPHIC.replace(/^\{|\}$/g, '').trim()}
|
"infographic": ${CONTENT_SCHEMA_INFOGRAPHIC.replace(/^\{|\}$/g, '').trim()}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
@@ -83,10 +80,7 @@ export async function generateLearningContent(topic, force = false, selectedType
|
|||||||
if (!force) {
|
if (!force) {
|
||||||
cached = await db.getContent(topic.id);
|
cached = await db.getContent(topic.id);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
if (selectedType === 'podcast' && cached.podcastScript) {
|
if (cached[selectedType]) {
|
||||||
console.log(`[Learn] Cache hit for topic: ${topic.id} (podcast)`);
|
|
||||||
return cached;
|
|
||||||
} else if (selectedType !== 'podcast' && cached[selectedType]) {
|
|
||||||
console.log(`[Learn] Cache hit for topic: ${topic.id} (${selectedType})`);
|
console.log(`[Learn] Cache hit for topic: ${topic.id} (${selectedType})`);
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
@@ -104,9 +98,6 @@ export async function generateLearningContent(topic, force = false, selectedType
|
|||||||
} else if (selectedType === 'slides') {
|
} else if (selectedType === 'slides') {
|
||||||
schema = CONTENT_SCHEMA_SLIDES;
|
schema = CONTENT_SCHEMA_SLIDES;
|
||||||
instructions = 'Provide at least 4 slides.';
|
instructions = 'Provide at least 4 slides.';
|
||||||
} else if (selectedType === 'podcast') {
|
|
||||||
schema = CONTENT_SCHEMA_PODCAST;
|
|
||||||
instructions = 'Provide a natural spoken script.';
|
|
||||||
} else if (selectedType === 'infographic') {
|
} else if (selectedType === 'infographic') {
|
||||||
schema = CONTENT_SCHEMA_INFOGRAPHIC;
|
schema = CONTENT_SCHEMA_INFOGRAPHIC;
|
||||||
instructions = 'Provide at least 3 stats, and 3-5 steps in the infographic.';
|
instructions = 'Provide at least 3 stats, and 3-5 steps in the infographic.';
|
||||||
|
|||||||
@@ -224,7 +224,6 @@ const Leren = () => {
|
|||||||
<div className="flex flex-wrap justify-center gap-4">
|
<div className="flex flex-wrap justify-center gap-4">
|
||||||
<Button onClick={() => loadContent('article')}>Article</Button>
|
<Button onClick={() => loadContent('article')}>Article</Button>
|
||||||
<Button onClick={() => loadContent('slides')}>Slides</Button>
|
<Button onClick={() => loadContent('slides')}>Slides</Button>
|
||||||
<Button onClick={() => loadContent('podcast')}>Podcast</Button>
|
|
||||||
<Button onClick={() => loadContent('infographic')}>Infographic</Button>
|
<Button onClick={() => loadContent('infographic')}>Infographic</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user