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) {