import { anthropicApi } from './api'; import * as db from './db'; import { getCurriculumTopic } from './curriculumService'; const CONTENT_GENERATION_SYSTEM = `You are an expert learning content writer for Respellion, an internal IT company. 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_ARTICLE = `{ "article": { "title": "Article title", "intro": "Short intro of 1-2 sentences", "sections": [ { "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." } ] }`; const CONTENT_SCHEMA_INFOGRAPHIC = `{ "infographic": { "headline": "A short, punchy headline summarizing the topic (max 8 words)", "tagline": "A subtitle of max 15 words", "stats": [ { "value": "Number or %", "label": "Short description", "icon": "📊" } ], "steps": [ { "number": 1, "title": "Step title", "description": "One-sentence description.", "icon": "🔑" } ], "quote": "An inspiring or insightful quote about the topic.", "colorTheme": "teal" } }`; const CONTENT_SCHEMA_ALL = `{ "article": ${CONTENT_SCHEMA_ARTICLE.replace(/^\{|\}$/g, '').trim()}, "slides": ${CONTENT_SCHEMA_SLIDES.replace(/^\{|\}$/g, '').trim()}, "infographic": ${CONTENT_SCHEMA_INFOGRAPHIC.replace(/^\{|\}$/g, '').trim()} }`; /** * Get the assigned topic for a given week. * Curriculum-first: checks the curriculum collection for the current year. * Falls back to hash-based assignment if no curriculum is configured. */ export async function getAssignedTopic(userId, weekNumber) { // Try curriculum first try { const { topic } = await getCurriculumTopic(weekNumber); if (topic && topic.learning_relevance !== 'exclude') return topic; } catch (e) { console.warn('[Learn] Curriculum lookup failed, falling back to hash:', e.message); } // Fallback: hash-based assignment (backwards compatible) const allTopics = await db.getTopics(); // Filter out 'fact' type topics and 'exclude' relevance topics const topics = allTopics.filter(t => t.type !== 'fact' && t.learning_relevance !== 'exclude'); if (!topics || topics.length === 0) return null; const str = `${userId}:${weekNumber}`; let hash = 0; for (let i = 0; i < str.length; i++) { hash = (hash << 5) - hash + str.charCodeAt(i); hash |= 0; } const index = Math.abs(hash) % topics.length; return topics[index]; } export async function getCachedContent(topicId) { return db.getContent(topicId); } export async function getAllGeneratedContent() { const topics = await db.getTopics(); const results = await Promise.all( topics.map(async topic => { const content = await db.getContent(topic.id); return { topic, content, hasContent: !!content }; }) ); return results.filter(item => item.hasContent); } export async function generateLearningContent(topic, force = false, selectedType = 'article') { let cached = null; if (!force) { cached = await db.getContent(topic.id); if (cached) { if (cached[selectedType]) { console.log(`[Learn] Cache hit for topic: ${topic.id} (${selectedType})`); return cached; } } } 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 === '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: ${schema} ${instructions}`; const responseText = await anthropicApi.generateContent(CONTENT_GENERATION_SYSTEM, prompt); let newContent; try { const jsonMatch = responseText.match(/\{[\s\S]*\}/); newContent = JSON.parse(jsonMatch ? jsonMatch[0] : responseText); } catch (e) { throw new Error('AI could not generate valid learning content. Please try again.', { cause: e }); } const mergedContent = { ...(cached || {}), ...newContent }; await db.setContent(topic.id, mergedContent); return mergedContent; } export async function refineLearningContent(topic, refinementInstruction) { const existing = await db.getContent(topic.id); const prompt = `You have previously generated the following learning module for the topic "${topic.label}": ${JSON.stringify(existing, null, 2)} The admin has requested the following refinement: "${refinementInstruction}" Apply the refinement and return the complete updated JSON object using the same structure. Return ONLY valid JSON.`; const responseText = await anthropicApi.generateContent(CONTENT_GENERATION_SYSTEM, prompt); let content; try { const jsonMatch = responseText.match(/\{[\s\S]*\}/); content = JSON.parse(jsonMatch ? jsonMatch[0] : responseText); } catch (e) { throw new Error('AI could not process the refinement. Please try a different instruction.', { cause: e }); } await db.setContent(topic.id, content); return content; } export async function deleteCachedContent(topicId) { return db.deleteContent(topicId); } export async function generateCustomTopic(label) { const prompt = `A user wants to learn about "${label}". Create a short description (2-3 sentences) and categorize it. Return ONLY a JSON object with this structure: { "label": "Polished topic title", "type": "concept", // one of: concept, role, process "description": "Short description" }`; const responseText = await anthropicApi.generateContent( "You are a knowledge graph AI categorizing topics.", prompt ); let newTopic; try { const jsonMatch = responseText.match(/\{[\s\S]*\}/); newTopic = JSON.parse(jsonMatch ? jsonMatch[0] : responseText); newTopic.id = 'custom_' + Date.now().toString(36); } catch (e) { throw new Error('Could not process custom topic. Please try again.', { cause: e }); } await db.upsertTopic(newTopic); return newTopic; }