diff --git a/src/components/admin/ContentManager.jsx b/src/components/admin/ContentManager.jsx index 9d7c8b6..02545cb 100644 --- a/src/components/admin/ContentManager.jsx +++ b/src/components/admin/ContentManager.jsx @@ -34,7 +34,7 @@ const ContentManager = () => { const handleRegenerate = async (topic) => { setTopicState(topic.id, { loading: 'regenerating', error: null, success: null }); try { - await generateLearningContent(topic, true); + await generateLearningContent(topic, true, 'all'); setTopicState(topic.id, { loading: null, success: 'Content regenerated.' }); refresh(); } catch (e) { diff --git a/src/components/ui/LearningContentViewer.jsx b/src/components/ui/LearningContentViewer.jsx index bf8b657..5a50585 100644 --- a/src/components/ui/LearningContentViewer.jsx +++ b/src/components/ui/LearningContentViewer.jsx @@ -20,8 +20,17 @@ const MODES = [ { key: 'infographic', icon: BarChart2, label: 'Infographic' }, ]; -const LearningContentViewer = ({ content, topic, initialMode = 'article' }) => { - const [activeMode, setActiveMode] = useState(initialMode); +const LearningContentViewer = ({ content, topic, initialMode = 'article', onGenerate, isLoading }) => { + const populatedModes = MODES.filter(m => m.key === 'podcast' ? !!content?.podcastScript : !!content?.[m.key]); + const defaultMode = populatedModes.length > 0 ? populatedModes[0].key : initialMode; + const [activeMode, setActiveMode] = useState(defaultMode); + + // If content populates while we are on an empty tab, keep the tab active + useEffect(() => { + if (content && populatedModes.length === 1 && !populatedModes.find(m => m.key === activeMode)) { + setActiveMode(populatedModes[0].key); + } + }, [content]); if (!content || !topic) return null; @@ -54,10 +63,26 @@ const LearningContentViewer = ({ content, topic, initialMode = 'article' }) => { exit={{ opacity: 0, y: -10 }} transition={{ duration: 0.2 }} > - {activeMode === 'article' && } - {activeMode === 'slides' && } - {activeMode === 'podcast' && } - {activeMode === 'infographic' && } + {(() => { + const isPopulated = activeMode === 'podcast' ? !!content?.podcastScript : !!content?.[activeMode]; + if (!isPopulated) { + return ( + +

This content has not been generated yet.

+ {onGenerate && ( + + )} +
+ ); + } + if (activeMode === 'article') return ; + if (activeMode === 'slides') return ; + if (activeMode === 'podcast') return ; + if (activeMode === 'infographic') return ; + return null; + })()} diff --git a/src/lib/learningService.js b/src/lib/learningService.js index b29a93c..0cc6725 100644 --- a/src/lib/learningService.js +++ b/src/lib/learningService.js @@ -6,7 +6,7 @@ You write training material for employees based on knowledge topics. Always write in clear, professional English. ALWAYS return valid JSON only — no markdown code blocks, no extra text.`; -const CONTENT_SCHEMA = `{ +const CONTENT_SCHEMA_ARTICLE = `{ "article": { "title": "Article title", "intro": "Short intro of 1-2 sentences", @@ -14,11 +14,20 @@ const CONTENT_SCHEMA = `{ { "heading": "Section title", "body": "Section text of at least 3 sentences." } ], "keyTakeaways": ["Takeaway 1", "Takeaway 2", "Takeaway 3"] - }, + } +}`; + +const CONTENT_SCHEMA_SLIDES = `{ "slides": [ { "title": "Slide title", "bullets": ["Point 1", "Point 2", "Point 3"], "speakerNote": "Speaker note for this slide." } - ], - "podcastScript": "A natural spoken script of approx. 300 words summarizing the topic as a podcast episode.", + ] +}`; + +const CONTENT_SCHEMA_PODCAST = `{ + "podcastScript": "A natural spoken script of approx. 300 words summarizing the topic as a podcast episode." +}`; + +const CONTENT_SCHEMA_INFOGRAPHIC = `{ "infographic": { "headline": "A short, punchy headline summarizing the topic (max 8 words)", "tagline": "A subtitle of max 15 words", @@ -33,6 +42,13 @@ const CONTENT_SCHEMA = `{ } }`; +const CONTENT_SCHEMA_ALL = `{ + "article": ${CONTENT_SCHEMA_ARTICLE.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()} +}`; + export async function getAssignedTopic(userId, weekNumber) { const topics = await db.getTopics(); if (!topics || topics.length === 0) return null; @@ -62,38 +78,64 @@ export async function getAllGeneratedContent() { return results.filter(item => item.hasContent); } -export async function generateLearningContent(topic, force = false) { +export async function generateLearningContent(topic, force = false, selectedType = 'article') { + let cached = null; if (!force) { - const cached = await db.getContent(topic.id); + cached = await db.getContent(topic.id); if (cached) { - console.log(`[Learn] Cache hit for topic: ${topic.id}`); - return cached; + if (selectedType === 'podcast' && cached.podcastScript) { + 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})`); + return cached; + } } } - const prompt = `Generate a complete learning module for the following topic: + let schema = ''; + let instructions = ''; + if (selectedType === 'all') { + schema = CONTENT_SCHEMA_ALL; + instructions = 'Provide at least 3 article sections, 4 slides, 3 stats, and 3-5 steps in the infographic.'; + } else if (selectedType === 'article') { + schema = CONTENT_SCHEMA_ARTICLE; + instructions = 'Provide at least 3 article sections.'; + } else if (selectedType === 'slides') { + schema = CONTENT_SCHEMA_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') { + schema = CONTENT_SCHEMA_INFOGRAPHIC; + instructions = 'Provide at least 3 stats, and 3-5 steps in the infographic.'; + } + + const prompt = `Generate a learning module piece for the following topic: Label: ${topic.label} Type: ${topic.type} Description: ${topic.description} Return ONLY a JSON object with the following structure: -${CONTENT_SCHEMA} +${schema} -Provide at least 3 article sections, 4 slides, 3 stats, and 3-5 steps in the infographic.`; +${instructions}`; const responseText = await anthropicApi.generateContent(CONTENT_GENERATION_SYSTEM, prompt); - let content; + let newContent; try { const jsonMatch = responseText.match(/\{[\s\S]*\}/); - content = JSON.parse(jsonMatch ? jsonMatch[0] : responseText); + newContent = JSON.parse(jsonMatch ? jsonMatch[0] : responseText); } catch (e) { throw new Error('AI could not generate valid learning content. Please try again.'); } - await db.setContent(topic.id, content); - return content; + const mergedContent = { ...(cached || {}), ...newContent }; + await db.setContent(topic.id, mergedContent); + return mergedContent; } export async function refineLearningContent(topic, refinementInstruction) { diff --git a/src/pages/Leren.jsx b/src/pages/Leren.jsx index 9b9731c..3fe3ad0 100644 --- a/src/pages/Leren.jsx +++ b/src/pages/Leren.jsx @@ -68,12 +68,12 @@ const Leren = () => { } }; - const loadContent = async () => { + const loadContent = async (selectedType = 'article') => { if (!activeTopic) return; setIsLoading(true); setError(null); try { - const generated = await generateLearningContent(activeTopic); + const generated = await generateLearningContent(activeTopic, false, selectedType); setContent(generated); } catch (e) { setError(e.message); @@ -220,8 +220,13 @@ const Leren = () => { {!content && !isLoading && ( -

Click the button to generate personalized AI learning content for this topic.

- +

Choose how you want to learn this topic.

+
+ + + + +
)} @@ -241,7 +246,7 @@ const Leren = () => { )} - {content && } + {content && } {content && (
diff --git a/src/store/AppContext.jsx b/src/store/AppContext.jsx index b43c5b3..0b08ccf 100644 --- a/src/store/AppContext.jsx +++ b/src/store/AppContext.jsx @@ -6,7 +6,7 @@ const AppContext = createContext(); const initialState = { currentUser: null, users: [], - weekNumber: 1, + weekNumber: getWeekNumber(new Date()), isLoading: true }; @@ -16,7 +16,7 @@ function appReducer(state, action) { return { ...state, users: action.payload.users, - weekNumber: action.payload.weekNumber, + weekNumber: action.payload.weekNumber || state.weekNumber, isLoading: false }; case 'LOGIN': @@ -30,6 +30,13 @@ function appReducer(state, action) { } } +function getWeekNumber(d) { + d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())); + d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay()||7)); + const yearStart = new Date(Date.UTC(d.getUTCFullYear(),0,1)); + return Math.ceil(( ( (d - yearStart) / 86400000) + 1)/7); +} + export function AppProvider({ children }) { const [state, dispatch] = useReducer(appReducer, initialState); @@ -47,7 +54,7 @@ export function AppProvider({ children }) { } } - const storedWeek = Number(await db.getSetting('admin:current_week', 1)); + const storedWeek = getWeekNumber(new Date()); const sessionUserId = sessionStorage.getItem('respellion_session'); if (sessionUserId) {