Skip to main content
Glama
analyze-content-for-widgets-v5.3.0.js49.1 kB
#!/usr/bin/env node /** * Content Analysis for Widgets Tool v5.2.0 - FULLY OPERATIONAL * Intelligently maps Claude's natural content to optimal widget types * @version 5.2.0 (January 12, 2025) * @status FULLY OPERATIONAL - Intelligent content-to-widget mapping * @reference JIT workflow step 2 of 7 * @milestone v5.2.0 - Context-aware widget selection from natural content */ export class ContentWidgetAnalyzer { constructor() { this.processingStartTime = null; this.analysisLog = []; this.contentTypes = this.initializeContentTypes(); this.widgetMappings = this.initializeWidgetMappings(); } /** * Main content analysis entry point for JIT workflow * @param {Object} input - Contains claudeContent (natural educational content) * @returns {Object} Widget mapping recommendations with educational flow analysis */ async analyzeContent(input) { this.processingStartTime = new Date(); this.analysisLog = []; try { const { claudeContent } = input; // Validate input with enhanced error message if (!claudeContent || typeof claudeContent !== 'object') { throw new Error(`claudeContent is required and must be an object. Received: ${typeof claudeContent} Expected format: { title: "...", contentSections: [...], interactiveElements: [...] } For rich content analysis, include structured sections with type and content fields.`); } // Extract and normalize content const normalizedContent = this.normalizeContent(claudeContent); // DEBUG: Log content processing (controlled by environment) if (process.env.MCP_DEBUG === 'true' || process.env.NODE_ENV === 'development') { console.error('[CONTENT_ANALYZER] Processing content with', normalizedContent.structuredSections?.length || 0, 'structured sections'); console.error('[CONTENT_ANALYZER] Content sections detected:', claudeContent.contentSections?.length || 0); console.error('[CONTENT_ANALYZER] Interactive elements:', claudeContent.interactiveElements?.length || 0); console.error('[CONTENT_ANALYZER] Visual needs:', claudeContent.visualsNeeded?.length || 0); console.error('[CONTENT_ANALYZER] Full claudeContent keys:', Object.keys(claudeContent)); } // Analyze content structure const contentStructure = this.analyzeContentStructure(normalizedContent); // Generate widget recommendations const widgetRecommendations = this.generateWidgetRecommendations(contentStructure); // DEBUG: Log widget recommendations (controlled by environment) if (process.env.MCP_DEBUG === 'true' || process.env.NODE_ENV === 'development') { console.error('[CONTENT_ANALYZER] Generated', widgetRecommendations.length, 'widget recommendations'); const widgetTypes = widgetRecommendations.map(r => r.widgetType); console.error('[CONTENT_ANALYZER] Widget types:', [...new Set(widgetTypes)]); } // Analyze educational flow const educationalFlow = this.analyzeEducationalFlow(widgetRecommendations); // Create optimizations and recommendations const optimizations = this.generateOptimizations(educationalFlow, widgetRecommendations); // Create final widget selection const selectedWidgetTypes = this.createWidgetSelection(widgetRecommendations); // Create response const analysis = { success: true, data: { contentStructure: { originalContent: normalizedContent.flatText, metadata: normalizedContent.metadata, segments: contentStructure.segments }, widgetRecommendations: widgetRecommendations, educationalFlow: educationalFlow, optimizations: optimizations, selectedWidgetTypes: selectedWidgetTypes, mappingRationale: { totalWidgets: widgetRecommendations.length, widgetTypes: [...new Set(widgetRecommendations.map(w => w.widgetType))], averageConfidence: this.calculateAverageConfidence(widgetRecommendations), mappingStrategy: "Balanced educational approach with varied widget types" } }, debug: { timestamp: new Date().toISOString(), processingTime: Date.now() - this.processingStartTime.getTime(), contentLength: normalizedContent.flatText.length, segmentsAnalyzed: contentStructure.segments.length, widgetsRecommended: widgetRecommendations.length } }; return analysis; } catch (error) { console.error('[CONTENT_ANALYZER] Error:', error.message); return { success: false, error: { code: 'ANALYSIS_ERROR', message: error.message, timestamp: new Date().toISOString() } }; } } /** * Normalize Claude's natural content into analyzable format * ENHANCED: Handles both structured content sections AND unstructured markdown */ normalizeContent(claudeContent) { let flatText = ''; let metadata = {}; let structuredSections = []; // Extract title if (claudeContent.title) { flatText += claudeContent.title + '\n\n'; metadata.title = claudeContent.title; } // Extract subject and grade level if (claudeContent.subject) { metadata.subject = claudeContent.subject; } if (claudeContent.gradeLevel) { metadata.gradeLevel = claudeContent.gradeLevel; } // Extract learning objectives if (claudeContent.learningObjectives) { metadata.learningObjectives = claudeContent.learningObjectives; } // CRITICAL FIX: Handle different content formats if (claudeContent.content) { if (typeof claudeContent.content === 'string') { // Handle unstructured markdown content console.error('[CONTENT_ANALYZER] Processing unstructured markdown content'); const parsedSections = this.parseMarkdownContent(claudeContent.content); structuredSections = parsedSections; flatText += claudeContent.content; } else if (typeof claudeContent.content === 'object') { // Handle structured content objects (what Claude Desktop actually sends) console.error('[CONTENT_ANALYZER] Processing structured content object'); const parsedSections = this.parseStructuredContentObject(claudeContent.content); structuredSections = parsedSections; flatText += this.extractTextFromObject(claudeContent.content); } } // Process structured content sections (CRITICAL FIX) if (claudeContent.contentSections && Array.isArray(claudeContent.contentSections)) { claudeContent.contentSections.forEach((section, index) => { // Extract all text content from section let sectionText = ''; if (section.title) sectionText += section.title + '\n'; if (section.content) sectionText += section.content + '\n'; // Add vocabulary terms if present if (section.terms && Array.isArray(section.terms)) { section.terms.forEach(term => { sectionText += `${term.term}: ${term.definition}\n`; }); } // Add assessment questions if present if (section.questions && Array.isArray(section.questions)) { section.questions.forEach(q => { sectionText += `${q.question}\n`; if (q.options) { q.options.forEach(opt => sectionText += `- ${opt}\n`); } }); } flatText += sectionText.trim() + '\n\n'; // Preserve structured section data with enhanced type detection const enhancedType = this.detectEnhancedContentType(section); structuredSections.push({ id: `section_${index}`, type: enhancedType, title: section.title, content: section.content, originalType: section.type, visualDescription: section.visualDescription, terms: section.terms, // for vocabulary sections questions: section.questions, // for assessment sections fullText: sectionText.trim() }); }); } // Extract interactive elements with structure preservation if (claudeContent.interactiveElements) { claudeContent.interactiveElements.forEach(element => { flatText += element + '\n\n'; // Detect interactive element types const lowerElement = element.toLowerCase(); if (lowerElement.includes('quiz') || lowerElement.includes('questões')) { structuredSections.push({ id: `interactive_quiz`, type: 'assessment', content: element, originalType: 'assessment', isInteractive: true }); } else if (lowerElement.includes('flashcard') || lowerElement.includes('vocabulário')) { structuredSections.push({ id: `interactive_flashcards`, type: 'vocabulary', content: element, originalType: 'vocabulary', isInteractive: true }); } }); } // Extract visual needs if (claudeContent.visualsNeeded && Array.isArray(claudeContent.visualsNeeded)) { claudeContent.visualsNeeded.forEach((visual, index) => { structuredSections.push({ id: `visual_${index}`, type: 'diagram_needed', content: visual, originalType: 'diagram_needed', isVisual: true }); }); } // CRITICAL FIX: Handle 'sections' array format (what Claude Desktop sends when going directly to tools) if (claudeContent.sections && Array.isArray(claudeContent.sections)) { console.error('[CONTENT_ANALYZER] Processing sections array format'); claudeContent.sections.forEach((section, index) => { // Extract content based on section structure let sectionContent = ''; let sectionTitle = section.title || `Section ${index + 1}`; // Handle different content formats within sections if (typeof section.content === 'string') { sectionContent = section.content; } else if (typeof section.content === 'object') { // Handle nested content objects (like biomas example) sectionContent = this.extractTextFromObject(section.content); } // Add vocabulary terms if present if (section.terms && Array.isArray(section.terms)) { section.terms.forEach(term => { sectionContent += `\n${term.term}: ${term.definition}`; }); } // Add to flat text flatText += `${sectionTitle}\n${sectionContent}\n\n`; // Determine section type const sectionType = this.mapSectionTypeToContentType(section.type) || this.detectContentTypeFromText(sectionTitle, sectionContent); // Add to structured sections with explicit position for order preservation structuredSections.push({ id: `section_${index}`, type: sectionType, title: sectionTitle, content: sectionContent, originalType: section.type, fullText: `${sectionTitle}\n${sectionContent}`, terms: section.terms, position: index, // Explicit position for order preservation isFromSections: true // Mark as coming from sections array }); // Log conclusion sections to ensure they're not lost if (section.type === 'conclusion' || sectionTitle.toLowerCase().includes('conclusão')) { console.error(`[CONTENT_ANALYZER] Conclusion section detected at position ${index}: "${sectionTitle}"`); } }); } // Extract homework if present (CRITICAL FIX: Handle both string and object homework) if (claudeContent.homework) { if (typeof claudeContent.homework === 'string') { flatText += `\nTarefa de Casa: ${claudeContent.homework}\n`; } else if (typeof claudeContent.homework === 'object') { // Handle structured homework object const homeworkText = this.extractTextFromObject(claudeContent.homework); flatText += `\nTarefa de Casa: ${homeworkText}\n`; } } // Extract activities if present (CRITICAL FIX: Handle structured activities) if (claudeContent.activities) { if (typeof claudeContent.activities === 'string') { flatText += `\nAtividades: ${claudeContent.activities}\n`; } else if (typeof claudeContent.activities === 'object') { // Handle structured activities object const activitiesText = this.extractTextFromObject(claudeContent.activities); flatText += `\nAtividades: ${activitiesText}\n`; } } // Extract vocabulary if present (CRITICAL FIX: Handle structured vocabulary) if (claudeContent.vocabulary) { if (typeof claudeContent.vocabulary === 'string') { flatText += `\nVocabulário: ${claudeContent.vocabulary}\n`; } else if (typeof claudeContent.vocabulary === 'object') { // Handle structured vocabulary object const vocabularyText = this.extractTextFromObject(claudeContent.vocabulary); flatText += `\nVocabulário: ${vocabularyText}\n`; } } // Extract introduction if present (CRITICAL FIX: Handle structured introduction) if (claudeContent.introduction) { if (typeof claudeContent.introduction === 'string') { flatText += `\nIntrodução: ${claudeContent.introduction}\n`; } else if (typeof claudeContent.introduction === 'object') { // Handle structured introduction object const introductionText = this.extractTextFromObject(claudeContent.introduction); flatText += `\nIntrodução: ${introductionText}\n`; } } // Extract main sections if present (CRITICAL FIX: Handle structured main sections) if (claudeContent.mainSections && Array.isArray(claudeContent.mainSections)) { claudeContent.mainSections.forEach((section, index) => { if (typeof section === 'string') { flatText += `\nSeção ${index + 1}: ${section}\n`; } else if (typeof section === 'object') { // Handle structured section object const sectionText = this.extractTextFromObject(section); flatText += `\nSeção ${index + 1}: ${sectionText}\n`; } }); } // CRITICAL FIX: Handle any remaining structured content that wasn't processed above // This ensures comprehensive content extraction from the Animal Kingdoms test case structure Object.keys(claudeContent).forEach(key => { if (!['title', 'subject', 'gradeLevel', 'learningObjectives', 'content', 'contentSections', 'interactiveElements', 'visualsNeeded', 'sections', 'homework', 'activities', 'vocabulary', 'introduction', 'mainSections'].includes(key)) { const value = claudeContent[key]; if (typeof value === 'string' && value.length > 0) { flatText += `\n${key}: ${value}\n`; } else if (typeof value === 'object' && value !== null) { const extractedText = this.extractTextFromObject(value); if (extractedText.trim().length > 0) { flatText += `\n${key}: ${extractedText}\n`; } } } }); return { flatText: flatText.trim(), metadata: metadata, originalStructure: claudeContent, structuredSections: structuredSections // CRITICAL: Preserve structure }; } /** * Recursively extract text from nested content objects */ extractTextRecursively(obj) { let text = ''; if (typeof obj === 'string') { return obj + '\n\n'; } if (Array.isArray(obj)) { obj.forEach(item => { text += this.extractTextRecursively(item); }); return text; } if (typeof obj === 'object' && obj !== null) { Object.values(obj).forEach(value => { text += this.extractTextRecursively(value); }); } return text; } /** * Analyze content structure and create segments * FIXED: Now preserves type detection from parseMarkdownContent */ analyzeContentStructure(normalizedContent) { let segments = []; // Use structured sections if available (CRITICAL FIX) if (normalizedContent.structuredSections && normalizedContent.structuredSections.length > 0) { segments = normalizedContent.structuredSections.map((section, index) => ({ id: section.id || `segment_${index}`, content: section.content || section.title || '', // CRITICAL: Preserve the type detection from parseMarkdownContent type: section.type || this.mapStructuredTypeToContentType(section.originalType || section.type), position: index, length: (section.content || section.title || '').length, originalType: section.originalType, isInteractive: section.isInteractive, isVisual: section.isVisual, structuredData: section, // Preserve all structured data title: section.title // Preserve title for debugging })); console.error(`[CONTENT_ANALYZER] Content structure analysis - preserving types:`); segments.forEach((segment, i) => { console.error(` ${i+1}. "${segment.title}" → ${segment.type}`); }); } else { // Fallback to text-based analysis const sentences = normalizedContent.flatText .split(/\n\n+/) .filter(s => s.trim().length > 0) .map(s => s.trim()); segments = sentences.map((sentence, index) => ({ id: `segment_${index}`, content: sentence, type: this.classifyContentType(sentence), position: index, length: sentence.length })); } return { segments }; } /** * Parse structured content object into sections * CRITICAL: This handles when Claude Desktop sends structured objects */ parseStructuredContentObject(contentObj) { const sections = []; let sectionIndex = 0; // Extract introduction if (contentObj.introduction) { sections.push({ id: `parsed_section_${sectionIndex++}`, title: 'Introdução', content: contentObj.introduction, type: 'introduction', originalType: 'structured', fullText: contentObj.introduction }); } // Extract main sections if (contentObj.mainSections && Array.isArray(contentObj.mainSections)) { contentObj.mainSections.forEach((section, index) => { sections.push({ id: `parsed_section_${sectionIndex++}`, title: section.title || `Seção ${index + 1}`, content: section.content, type: 'explanatory', originalType: 'structured', fullText: `${section.title || ''}\n${section.content || ''}` }); }); } // Extract timeline if (contentObj.timeline && Array.isArray(contentObj.timeline)) { let timelineContent = ''; contentObj.timeline.forEach(item => { timelineContent += `${item.year}: ${item.event}\n`; }); sections.push({ id: `parsed_section_${sectionIndex++}`, title: 'Linha do Tempo', content: timelineContent, type: 'list', originalType: 'structured', fullText: `Linha do Tempo\n${timelineContent}` }); } // Extract vocabulary if (contentObj.vocabulary && Array.isArray(contentObj.vocabulary)) { let vocabContent = ''; contentObj.vocabulary.forEach(item => { vocabContent += `${item.term}: ${item.definition}\n`; }); sections.push({ id: `parsed_section_${sectionIndex++}`, title: 'Vocabulário', content: vocabContent, type: 'vocabulary', originalType: 'structured', fullText: `Vocabulário\n${vocabContent}`, terms: contentObj.vocabulary }); } // Extract objectives if (contentObj.objectives && Array.isArray(contentObj.objectives)) { const objectivesContent = contentObj.objectives.join('\n'); sections.push({ id: `parsed_section_${sectionIndex++}`, title: 'Objetivos de Aprendizagem', content: objectivesContent, type: 'list', originalType: 'structured', fullText: `Objetivos de Aprendizagem\n${objectivesContent}` }); } // Extract Luther principles (specific to this content) if (contentObj.lutherPrinciples && Array.isArray(contentObj.lutherPrinciples)) { let principlesContent = ''; contentObj.lutherPrinciples.forEach(item => { principlesContent += `${item.principle}: ${item.explanation}\n`; }); sections.push({ id: `parsed_section_${sectionIndex++}`, title: 'Princípios de Lutero', content: principlesContent, type: 'list', originalType: 'structured', fullText: `Princípios de Lutero\n${principlesContent}` }); } // Extract reflection questions if (contentObj.reflectionQuestions && Array.isArray(contentObj.reflectionQuestions)) { const questionsContent = contentObj.reflectionQuestions.join('\n'); sections.push({ id: `parsed_section_${sectionIndex++}`, title: 'Questões para Reflexão', content: questionsContent, type: 'assessment', originalType: 'structured', fullText: `Questões para Reflexão\n${questionsContent}` }); } // Extract consequences if (contentObj.consequences && typeof contentObj.consequences === 'object') { let consequencesContent = ''; Object.entries(contentObj.consequences).forEach(([key, value]) => { consequencesContent += `${key}: ${value}\n`; }); sections.push({ id: `parsed_section_${sectionIndex++}`, title: 'Consequências', content: consequencesContent, type: 'explanatory', originalType: 'structured', fullText: `Consequências\n${consequencesContent}` }); } // Extract other reformers if (contentObj.otherReformers && Array.isArray(contentObj.otherReformers)) { let reformersContent = ''; contentObj.otherReformers.forEach(reformer => { reformersContent += `${reformer.name}: ${reformer.description}\n`; }); sections.push({ id: `parsed_section_${sectionIndex++}`, title: 'Outros Reformadores', content: reformersContent, type: 'explanatory', originalType: 'structured', fullText: `Outros Reformadores\n${reformersContent}` }); } console.error(`[CONTENT_ANALYZER] Parsed ${sections.length} sections from structured object`); return sections; } /** * Enhanced text extraction from objects with educational content prioritization * Version: 2.0.0 - Fixes [object Object] serialization issues * @param {any} obj - Object to extract text from * @param {number} depth - Current recursion depth (prevents infinite loops) * @param {string} context - Context for debugging (parent key name) * @returns {string} Extracted text content */ extractTextFromObject(obj, depth = 0, context = 'root') { // Prevent infinite recursion if (depth > 10) { console.error(`[CONTENT_ANALYZER] Max recursion depth reached at context: ${context}`); return ''; } // Handle null/undefined if (obj === null || obj === undefined) { return ''; } // Handle strings - direct return if (typeof obj === 'string') { console.error(`[CONTENT_ANALYZER] String content extracted from ${context}: ${obj.length} chars`); return obj; } // Handle numbers/booleans - convert to string if (typeof obj === 'number' || typeof obj === 'boolean') { return String(obj); } // Handle arrays - process each item if (Array.isArray(obj)) { console.error(`[CONTENT_ANALYZER] Processing array at ${context} with ${obj.length} items`); let arrayText = ''; obj.forEach((item, index) => { const itemText = this.extractTextFromObject(item, depth + 1, `${context}[${index}]`); if (itemText.trim()) { arrayText += itemText + '\n'; } }); return arrayText; } // Handle objects - educational content prioritization if (typeof obj === 'object') { console.error(`[CONTENT_ANALYZER] Processing object at ${context} with keys: ${Object.keys(obj).join(', ')}`); let objectText = ''; // Educational content prioritization order const priorityKeys = [ 'content', 'explanation', 'description', 'text', 'body', 'title', 'name', 'question', 'answer', 'definition', 'term', 'concept', 'instructions', 'steps', 'procedure', 'examples', 'activities', 'exercises' ]; // Process high-priority educational content first priorityKeys.forEach(key => { if (obj.hasOwnProperty(key)) { const value = obj[key]; const extractedText = this.extractTextFromObject(value, depth + 1, `${context}.${key}`); if (extractedText.trim()) { objectText += extractedText + '\n'; } } }); // Process remaining keys (excluding already processed ones) Object.entries(obj).forEach(([key, value]) => { if (!priorityKeys.includes(key)) { const extractedText = this.extractTextFromObject(value, depth + 1, `${context}.${key}`); if (extractedText.trim()) { objectText += extractedText + '\n'; } } }); return objectText; } // Fallback for unexpected types console.error(`[CONTENT_ANALYZER] Unexpected type ${typeof obj} at ${context}, converting to string`); return String(obj); } /** * Parse unstructured markdown content into structured sections * CRITICAL: This handles what Claude Desktop actually sends */ parseMarkdownContent(markdownText) { const sections = []; const lines = markdownText.split('\n'); let currentSection = null; let sectionIndex = 0; let inQuizSection = false; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Skip empty lines unless we're in a quiz section (preserve question structure) if (!line && !inQuizSection) continue; // Special handling for quiz sections - keep collecting until we hit a new major section if (inQuizSection) { // Check if this is a new major section (not a question) if (this.isSectionHeader(line, lines[i + 1]) && !line.match(/^Questão \d+/) && !line.toLowerCase().includes('questão')) { // End quiz section and start new section inQuizSection = false; // Save quiz section if (currentSection && currentSection.content.trim()) { sections.push({ ...currentSection, id: `parsed_section_${sectionIndex++}`, type: this.detectContentTypeFromText(currentSection.title, currentSection.content), fullText: `${currentSection.title}\n${currentSection.content}`.trim() }); } // Start new section currentSection = { title: line, content: '', originalType: 'parsed' }; } else { // Continue adding to quiz section if (line) { currentSection.content += line + '\n'; } else { currentSection.content += '\n'; // Preserve spacing in quiz } } } else { // Normal section detection if (this.isSectionHeader(line, lines[i + 1])) { // Save previous section if (currentSection && currentSection.content.trim()) { sections.push({ ...currentSection, id: `parsed_section_${sectionIndex++}`, type: this.detectContentTypeFromText(currentSection.title, currentSection.content), fullText: `${currentSection.title}\n${currentSection.content}`.trim() }); } // Start new section currentSection = { title: line, content: '', originalType: 'parsed' }; // Check if this is a quiz section if (line.toLowerCase().includes('quiz') || line.toLowerCase().includes('avaliação')) { inQuizSection = true; } } else if (currentSection) { // Add line to current section currentSection.content += line + '\n'; } else { // No section started yet, create default section currentSection = { title: 'Introdução', content: line + '\n', originalType: 'parsed' }; } } } // Don't forget the last section if (currentSection && currentSection.content.trim()) { sections.push({ ...currentSection, id: `parsed_section_${sectionIndex++}`, type: this.detectContentTypeFromText(currentSection.title, currentSection.content), fullText: `${currentSection.title}\n${currentSection.content}`.trim() }); } console.error(`[CONTENT_ANALYZER] Parsed ${sections.length} sections from markdown`); sections.forEach((section, i) => { console.error(`[CONTENT_ANALYZER] Section ${i+1}: "${section.title}" (${section.type}) - ${section.content.length} chars`); }); return sections; } /** * Check if a line is a section header */ isSectionHeader(line, nextLine) { // Skip if line is too short or too long if (line.length < 3 || line.length > 120) return false; // Definitive headers with colons if (line.includes(':') && line.length < 100) return true; // Specific educational section patterns if (line.toLowerCase().includes('questão') || line.toLowerCase().includes('avaliação')) return true; if (line.toLowerCase().includes('vocabulário') || line.toLowerCase().includes('experiência')) return true; if (line.toLowerCase().includes('objetivo') || line.toLowerCase().includes('introdução')) return true; if (line.toLowerCase().includes('processo') || line.toLowerCase().includes('importância')) return true; // Question pattern (starts with "Questão") if (line.match(/^Questão \d+:/)) return true; // Capitalized educational headers if (line.match(/^[A-ZÇÁÉÍÓÚÂÊÔÀÈÌ]/)) { // Must be reasonable length and not a sentence if (line.length < 100 && !line.includes('.') && !line.includes(',')) { // Additional check: not starting with common sentence starters if (!line.toLowerCase().startsWith('a ') && !line.toLowerCase().startsWith('o ') && !line.toLowerCase().startsWith('para ')) { return true; } } } return false; } /** * Detect content type from title and content text */ detectContentTypeFromText(title, content) { const titleLower = (title || '').toLowerCase(); const contentLower = (content || '').toLowerCase(); const combined = titleLower + ' ' + contentLower; // Strong quiz/assessment detection if (titleLower.includes('quiz') || titleLower.includes('avaliação') || titleLower.includes('questão')) { return 'assessment'; } // Look for multiple choice patterns (a), b), c), d)) const hasMultipleChoice = /[a-d]\)/.test(contentLower); if (hasMultipleChoice && (combined.includes('questão') || titleLower.includes('questão'))) { return 'assessment'; } // Count questions - if multiple questions with a), b), c) format, it's a quiz const questionCount = (content.match(/questão \d+/gi) || []).length; if (questionCount >= 2 && hasMultipleChoice) { return 'assessment'; } // Strong vocabulary detection if (titleLower.includes('vocabulário')) { return 'vocabulary'; } // Check for definition patterns if (content.includes(':') && (combined.includes('definição') || combined.includes('significa'))) { // Count definitions (term: definition format) const definitionCount = (content.match(/\w+:/g) || []).length; if (definitionCount >= 2) { return 'vocabulary'; } } // Experiment detection if (combined.includes('experiência') || combined.includes('experimento') || combined.includes('material necessário')) { return 'explanatory'; } // List detection if (combined.includes('ingredientes') || combined.includes('fatores')) { return 'list'; } // Check for numbered lists const numberedItems = (content.match(/\d+\./g) || []).length; if (numberedItems >= 2) { return 'list'; } // Introduction detection if (titleLower.includes('introdução') || titleLower.includes('objetivo')) { return 'introduction'; } // Process detection if (titleLower.includes('processo') || titleLower.includes('passo')) { return 'explanatory'; } // Importance detection if (titleLower.includes('importância') || titleLower.includes('papel')) { return 'explanatory'; } return 'explanatory'; } /** * Detect enhanced content type from structured section data */ detectEnhancedContentType(section) { // Direct type mapping from structured content if (section.type) { return this.mapStructuredTypeToContentType(section.type); } // Detect based on presence of specific data structures if (section.terms && Array.isArray(section.terms)) { return 'vocabulary'; } if (section.questions && Array.isArray(section.questions)) { return 'assessment'; } if (section.visualDescription) { return 'visual'; } // Content-based detection if (section.content) { const lowerContent = section.content.toLowerCase(); if (lowerContent.includes('experiment') || lowerContent.includes('procedimento')) { return 'explanatory'; } if (lowerContent.includes('fatores') || lowerContent.includes('1.') || lowerContent.includes('2.')) { return 'list'; } } return 'explanatory'; } /** * Map structured content types to internal content types */ mapStructuredTypeToContentType(structuredType) { const typeMapping = { 'introduction': 'introduction', 'explanation': 'explanatory', 'process': 'explanatory', 'process_explanation': 'explanatory', 'importance': 'explanatory', 'factors': 'list', 'experiments': 'explanatory', 'vocabulary': 'vocabulary', 'assessment': 'assessment', 'diagram_needed': 'visual', 'challenge': 'question' }; return typeMapping[structuredType] || 'explanatory'; } /** * Classify content type based on patterns */ classifyContentType(content) { const lowerContent = content.toLowerCase(); // Introduction patterns if (this.contentTypes.introduction.some(pattern => lowerContent.includes(pattern))) { return 'introduction'; } // Definition patterns if (this.contentTypes.definition.some(pattern => lowerContent.includes(pattern))) { return 'definition'; } // Explanatory patterns if (this.contentTypes.explanatory.some(pattern => lowerContent.includes(pattern))) { return 'explanatory'; } // Question patterns if (this.contentTypes.question.some(pattern => lowerContent.includes(pattern))) { return 'question'; } // List patterns if (this.contentTypes.list.some(pattern => lowerContent.includes(pattern))) { return 'list'; } return 'explanatory'; // Default } /** * Generate widget recommendations based on content analysis */ generateWidgetRecommendations(contentStructure) { const recommendations = []; // Always add header recommendations.push({ widgetType: "head-1", confidence: 1.0, contentSegments: [], content: "Professional lesson header", rationale: "Professional lesson header required", educationalGoal: "organization", priority: 0 }); // Analyze each segment for widget recommendations contentStructure.segments.forEach((segment, index) => { const widgetType = this.mapContentToWidget(segment.type, segment.content, segment); if (widgetType) { recommendations.push({ widgetType: widgetType, confidence: this.calculateConfidence(segment.type, segment.content), contentSegments: [segment.id], content: segment.content, rationale: this.getRationale(widgetType, segment.type), educationalGoal: this.getEducationalGoal(widgetType), priority: index + 1 }); // Debug logging for special widget types if (widgetType === 'quiz-1' || widgetType === 'flashcards-1') { console.error(`[CONTENT_ANALYZER] SPECIAL WIDGET GENERATED: ${widgetType} for segment "${segment.content.substring(0, 50)}..."`); } } }); console.error(`[CONTENT_ANALYZER] Generated ${recommendations.length} recommendations`); const widgetTypes = [...new Set(recommendations.map(r => r.widgetType))]; console.error(`[CONTENT_ANALYZER] Unique widget types: ${widgetTypes.join(', ')}`); return recommendations; } /** * Map content type to appropriate widget * ENHANCED: Now handles structured content types properly */ mapContentToWidget(contentType, content, segment = null) { // Handle structured content types first (CRITICAL FIX) if (contentType === 'assessment') { return 'quiz-1'; } if (contentType === 'vocabulary') { return 'flashcards-1'; } if (contentType === 'visual') { return 'image-1'; } if (contentType === 'question') { return 'quiz-1'; } if (contentType === 'list') { return 'list-1'; } // Fallback to mapping table const mapping = this.widgetMappings[contentType]; if (!mapping) return 'text-1'; // Apply additional logic based on content characteristics if (contentType === 'definition' && content.length < 200) { return 'flashcards-1'; } return mapping.primary || 'text-1'; } /** * Calculate confidence score for widget recommendation * ENHANCED: Now includes confidence for structured content types */ calculateConfidence(contentType, content) { const baseConfidence = { 'introduction': 0.9, 'definition': 0.85, 'explanatory': 0.8, 'question': 0.95, 'list': 0.9, 'assessment': 0.95, // High confidence for quiz content 'vocabulary': 0.9, // High confidence for flashcards 'visual': 0.85 // Good confidence for images }; let confidence = baseConfidence[contentType] || 0.75; // Adjust based on content characteristics if (content.length < 50) confidence -= 0.1; if (content.length > 300) confidence += 0.05; return Math.max(0.5, Math.min(1.0, confidence)); } /** * Get rationale for widget selection */ getRationale(widgetType, contentType) { const rationales = { 'head-1': 'Professional lesson header required', 'text-1': 'Explanatory content detected - suitable for text presentation', 'flashcards-1': 'Definition content detected - perfect for flashcard memorization', 'quiz-1': 'Assessment content detected - ideal for quiz interaction', 'list-1': 'List content detected - structured presentation needed', 'image-1': 'Visual content recommended for enhanced understanding' }; return rationales[widgetType] || 'Content analysis suggests this widget type'; } /** * Get educational goal for widget type */ getEducationalGoal(widgetType) { const goals = { 'head-1': 'organization', 'text-1': 'knowledge_transfer', 'flashcards-1': 'concept_memorization', 'quiz-1': 'assessment', 'list-1': 'information_organization', 'image-1': 'visual_understanding' }; return goals[widgetType] || 'learning_support'; } /** * Analyze educational flow and balance */ analyzeEducationalFlow(recommendations) { const widgetTypes = recommendations.map(r => r.widgetType); const hasIntroduction = widgetTypes.includes('head-1'); const hasMainContent = widgetTypes.includes('text-1') || widgetTypes.includes('list-1'); const hasInteractivity = widgetTypes.includes('flashcards-1') || widgetTypes.includes('image-1') || widgetTypes.includes('quiz-1'); const hasAssessment = widgetTypes.includes('quiz-1'); // Analyze cognitive load distribution const cognitiveLoad = this.analyzeCognitiveLoad(recommendations); // Create recommended sequence const recommendedSequence = this.createRecommendedSequence(recommendations); // Calculate flow score const score = this.calculateFlowScore(hasIntroduction, hasMainContent, hasInteractivity, hasAssessment); // Generate recommendations const flowRecommendations = this.generateFlowRecommendations(hasMainContent, hasInteractivity); return { hasIntroduction, hasMainContent, hasInteractivity, hasAssessment, cognitiveLoadBalance: cognitiveLoad, recommendedSequence, score, recommendations: flowRecommendations }; } /** * Analyze cognitive load distribution */ analyzeCognitiveLoad(recommendations) { let low = 0, medium = 0, high = 0; recommendations.forEach(rec => { if (['head-1', 'image-1'].includes(rec.widgetType)) low++; else if (['text-1', 'list-1', 'flashcards-1'].includes(rec.widgetType)) medium++; else if (['quiz-1'].includes(rec.widgetType)) high++; }); const total = recommendations.length; const distribution = { low: Math.round((low / total) * 100), medium: Math.round((medium / total) * 100), high: Math.round((high / total) * 100) }; const balance = this.evaluateCognitiveBalance(distribution); const recommendation = this.getCognitiveRecommendation(distribution); return { distribution, balance, recommendation }; } /** * Evaluate cognitive load balance */ evaluateCognitiveBalance(distribution) { // Ideal: 30% low, 50% medium, 20% high if (distribution.high === 0 && distribution.medium === 0) { return 'needs_adjustment'; } if (distribution.high > 40) { return 'too_demanding'; } if (distribution.low > 70) { return 'too_simple'; } return 'balanced'; } /** * Get cognitive load recommendation */ getCognitiveRecommendation(distribution) { if (distribution.high === 0) { return 'Consider adding interactive widgets (quiz, hotspots) for better engagement'; } if (distribution.medium === 0) { return 'Add text widgets for substantial educational content'; } return 'Good cognitive load distribution'; } /** * Create recommended widget sequence */ createRecommendedSequence(recommendations) { return recommendations .sort((a, b) => a.priority - b.priority) .map((rec, index) => ({ position: index + 1, widgetType: rec.widgetType, rationale: rec.rationale })); } /** * Calculate educational flow score */ calculateFlowScore(hasIntro, hasMain, hasInteractive, hasAssessment) { let score = 0; if (hasIntro) score += 25; if (hasMain) score += 30; if (hasInteractive) score += 25; if (hasAssessment) score += 20; return score; } /** * Generate flow recommendations */ generateFlowRecommendations(hasMainContent, hasInteractivity) { const recommendations = []; if (!hasMainContent) { recommendations.push('Add text widgets for substantial educational content'); } if (!hasInteractivity) { recommendations.push('Consider adding interactive widgets (quiz, hotspots) for better engagement'); } return recommendations; } /** * Generate optimization suggestions */ generateOptimizations(educationalFlow, recommendations) { const optimizations = []; if (educationalFlow.cognitiveLoadBalance.balance === 'needs_adjustment') { optimizations.push({ type: 'cognitive_load', message: educationalFlow.cognitiveLoadBalance.recommendation, severity: 'suggestion' }); } if (educationalFlow.score < 70) { optimizations.push({ type: 'educational_flow', message: 'Consider adding more diverse widget types for better learning experience', severity: 'recommendation' }); } return optimizations; } /** * Create final widget type selection summary */ createWidgetSelection(recommendations) { const widgetCounts = {}; const widgetInfo = {}; recommendations.forEach(rec => { const type = rec.widgetType; widgetCounts[type] = (widgetCounts[type] || 0) + 1; if (!widgetInfo[type]) { widgetInfo[type] = { confidence: rec.confidence, rationale: rec.rationale }; } }); return Object.keys(widgetCounts).map(type => ({ type: type, count: widgetCounts[type], confidence: widgetInfo[type].confidence, usageRationale: widgetInfo[type].rationale })); } /** * Calculate average confidence across recommendations */ calculateAverageConfidence(recommendations) { if (recommendations.length === 0) return 0; const sum = recommendations.reduce((acc, rec) => acc + rec.confidence, 0); return sum / recommendations.length; } /** * Initialize content type patterns */ initializeContentTypes() { return { introduction: ['introdução', 'objetivo', 'vamos', 'começar', 'introduction', 'overview'], definition: ['é um', 'é uma', 'define-se', 'conceito', 'definição', 'significa'], explanatory: ['porque', 'como', 'quando', 'processo', 'exemplo', 'aplicação'], question: ['?', 'qual', 'como', 'por que', 'o que', 'pergunta'], list: ['primeiro', 'segundo', 'terceiro', '1.', '2.', '3.', 'lista', 'itens'] }; } /** * Initialize widget mapping rules */ initializeWidgetMappings() { return { introduction: { primary: 'text-1', alternatives: ['head-1'] }, definition: { primary: 'flashcards-1', alternatives: ['text-1'] }, explanatory: { primary: 'text-1', alternatives: ['image-1'] }, question: { primary: 'quiz-1', alternatives: ['text-1'] }, list: { primary: 'list-1', alternatives: ['text-1'] } }; } /** * Map section types from Claude Desktop to content types */ mapSectionTypeToContentType(sectionType) { const typeMap = { 'introduction': 'introduction', 'concept_explanation': 'explanatory', 'main_content': 'explanatory', 'interactive_activity': 'assessment', 'vocabulary': 'vocabulary', 'assessment': 'assessment', 'quiz': 'assessment', 'flashcards': 'vocabulary', 'conclusion': 'conclusion', // Map to specific conclusion type 'conclusão': 'conclusion', // Portuguese 'final_thoughts': 'conclusion', 'wrap_up': 'conclusion', 'summary': 'explanatory', 'practice': 'assessment', 'exercises': 'assessment', 'homework': 'homework' // Add homework type }; return typeMap[sectionType] || null; } } /** * Create and export the tool instance for JIT server integration */ export function createContentWidgetAnalyzer() { const analyzer = new ContentWidgetAnalyzer(); return { name: 'analyze_content_for_widgets', description: 'STEP 2: Intelligently analyze natural content and map to optimal widgets. Provides widget suggestions based on content analysis.', inputSchema: { type: 'object', properties: { claudeContent: { type: 'object', description: 'Natural educational content created by Claude (any structure)' } }, required: ['claudeContent'] }, handler: async (input) => { return await analyzer.analyzeContent(input); } }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rkm097git/euconquisto-composer-mcp-poc'

If you have feedback or need assistance with the MCP directory API, please join our Discord server