analyze-content-for-widgets.js•49.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);
}
};
}