analyze-content-for-widgets-v1.1.0.js•27.5 kB
#!/usr/bin/env node
/**
* Content Analysis for Widgets Tool v1.1.0 - RELIABILITY FOCUSED
* Dynamically adapts to any educational content while ensuring robust structure preservation
* @version 1.1.0 (January 20, 2025)
* @status DEVELOPMENT - Improved reliability with fail-fast validation
* @reference JIT workflow step 2 of 7
* @enhancement Dynamic adaptation + robust error handling + content preservation
*/
export class ContentWidgetAnalyzerV110 {
constructor() {
this.processingStartTime = null;
this.validationErrors = [];
this.contentTypes = this.initializeContentTypes();
this.widgetMappings = this.initializeWidgetMappings();
}
/**
* Main content analysis entry point - v1.1.0 RELIABLE VERSION
* @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.validationErrors = [];
try {
// FAIL-FAST VALIDATION PHASE
const validationResult = this.validateAndNormalizeInput(input);
if (!validationResult.valid) {
throw new Error(`Content validation failed:\n${validationResult.errors.join('\n')}\n\nReceived input structure: ${JSON.stringify(Object.keys(input), null, 2)}`);
}
const { claudeContent, normalizedContent } = validationResult;
console.error('[CONTENT_ANALYZER_V110] Content validated successfully');
console.error(`[CONTENT_ANALYZER_V110] Processing ${normalizedContent.sections.length} sections with ${normalizedContent.totalCharacters} characters`);
// DYNAMIC CONTENT ANALYSIS PHASE
const contentStructure = this.analyzeContentStructure(normalizedContent);
// WIDGET MAPPING PHASE
const widgetRecommendations = this.generateWidgetRecommendations(contentStructure);
// EDUCATIONAL FLOW ANALYSIS PHASE
const educationalFlow = this.analyzeEducationalFlow(widgetRecommendations);
// OPTIMIZATION PHASE
const optimizations = this.generateOptimizations(educationalFlow, widgetRecommendations);
// FINAL WIDGET SELECTION PHASE
const selectedWidgetTypes = this.createWidgetSelection(widgetRecommendations);
// SUCCESS RESPONSE
return {
success: true,
data: {
contentStructure: {
originalFormat: normalizedContent.detectedFormat,
processedSections: contentStructure.sections.length,
totalContent: normalizedContent.totalCharacters,
dynamicAdaptations: normalizedContent.adaptations
},
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: "Dynamic educational adaptation with structure preservation"
}
},
metadata: {
version: "1.1.0",
timestamp: new Date().toISOString(),
processingTime: Date.now() - this.processingStartTime.getTime(),
inputFormat: normalizedContent.detectedFormat,
sectionsProcessed: normalizedContent.sections.length
}
};
} catch (error) {
console.error('[CONTENT_ANALYZER_V110] FAIL-FAST ERROR:', error.message);
// FAIL-FAST ERROR RESPONSE
return {
success: false,
error: {
code: 'CONTENT_ANALYSIS_FAILED',
message: error.message,
timestamp: new Date().toISOString(),
processingTime: Date.now() - this.processingStartTime.getTime(),
failFast: true,
developmentMode: true,
troubleshooting: {
commonIssues: [
"Content structure not recognized - check if claudeContent is properly structured",
"Missing required properties - ensure content has analyzable structure",
"Type mismatch - verify input is an object with educational content"
],
debugSteps: [
"1. Check input.claudeContent exists and is an object",
"2. Verify content has title, sections, or other educational structure",
"3. Ensure content is not empty or malformed"
]
}
}
};
}
}
/**
* FAIL-FAST VALIDATION with dynamic format detection
* Key innovation: Adapts to ANY content format while ensuring structural integrity
*/
validateAndNormalizeInput(input) {
const errors = [];
// Basic input validation
if (!input || typeof input !== 'object') {
errors.push("Input must be an object containing claudeContent");
return { valid: false, errors };
}
const { claudeContent } = input;
if (!claudeContent) {
errors.push("claudeContent is required");
return { valid: false, errors };
}
if (typeof claudeContent !== 'object') {
errors.push(`claudeContent must be an object, received: ${typeof claudeContent}`);
return { valid: false, errors };
}
// DYNAMIC FORMAT DETECTION
const formatDetection = this.detectContentFormat(claudeContent);
if (!formatDetection.recognized) {
errors.push(`Content format not recognized. Detected keys: [${Object.keys(claudeContent).join(', ')}]. Expected educational content with structure like: title, sections, content, etc.`);
return { valid: false, errors };
}
console.error(`[CONTENT_ANALYZER_V110] Detected format: ${formatDetection.format}`);
console.error(`[CONTENT_ANALYZER_V110] Format confidence: ${formatDetection.confidence}`);
// DYNAMIC NORMALIZATION based on detected format
const normalizedContent = this.normalizeContentDynamically(claudeContent, formatDetection);
// STRUCTURE VALIDATION
if (normalizedContent.sections.length === 0) {
errors.push("No analyzable content sections found. Content appears to be empty or unstructured.");
return { valid: false, errors };
}
if (normalizedContent.totalCharacters < 50) {
errors.push(`Content too short for analysis. Found ${normalizedContent.totalCharacters} characters, minimum 50 required.`);
return { valid: false, errors };
}
console.error(`[CONTENT_ANALYZER_V110] Validation passed: ${normalizedContent.sections.length} sections, ${normalizedContent.totalCharacters} characters`);
return {
valid: true,
claudeContent,
normalizedContent,
errors: []
};
}
/**
* DYNAMIC FORMAT DETECTION - Adapts to any educational content structure
* This preserves the strategic differential while ensuring reliability
*/
detectContentFormat(claudeContent) {
const keys = Object.keys(claudeContent);
let format = 'unknown';
let confidence = 0;
let recognized = false;
// Structured sections format (most common from Claude Desktop)
if (claudeContent.sections && Array.isArray(claudeContent.sections)) {
format = 'structured_sections';
confidence = 0.9;
recognized = true;
}
// Legacy contentSections format
else if (claudeContent.contentSections && Array.isArray(claudeContent.contentSections)) {
format = 'content_sections';
confidence = 0.8;
recognized = true;
}
// Object-based content format
else if (claudeContent.content && typeof claudeContent.content === 'object') {
format = 'object_content';
confidence = 0.7;
recognized = true;
}
// String-based content format
else if (claudeContent.content && typeof claudeContent.content === 'string') {
format = 'string_content';
confidence = 0.6;
recognized = true;
}
// Flat educational object (title + direct properties)
else if (claudeContent.title && Object.keys(claudeContent).length > 2) {
format = 'flat_educational';
confidence = 0.5;
recognized = true;
}
// Minimal content (just text fields)
else if (keys.some(key => typeof claudeContent[key] === 'string' && claudeContent[key].length > 20)) {
format = 'minimal_text';
confidence = 0.4;
recognized = true;
}
return { format, confidence, recognized, keys };
}
/**
* DYNAMIC CONTENT NORMALIZATION - Adapts to detected format
* Preserves all content while creating consistent internal structure
*/
normalizeContentDynamically(claudeContent, formatDetection) {
const { format } = formatDetection;
const sections = [];
const adaptations = [];
let totalCharacters = 0;
console.error(`[CONTENT_ANALYZER_V110] Normalizing format: ${format}`);
switch (format) {
case 'structured_sections':
this.normalizeStructuredSections(claudeContent, sections, adaptations);
break;
case 'content_sections':
this.normalizeContentSections(claudeContent, sections, adaptations);
break;
case 'object_content':
this.normalizeObjectContent(claudeContent, sections, adaptations);
break;
case 'string_content':
this.normalizeStringContent(claudeContent, sections, adaptations);
break;
case 'flat_educational':
this.normalizeFlatEducational(claudeContent, sections, adaptations);
break;
case 'minimal_text':
this.normalizeMinimalText(claudeContent, sections, adaptations);
break;
default:
throw new Error(`Unsupported content format: ${format}`);
}
// Calculate total characters
totalCharacters = sections.reduce((sum, section) => sum + section.content.length, 0);
// Add metadata sections if present
this.addMetadataSections(claudeContent, sections, adaptations);
return {
sections,
adaptations,
totalCharacters,
detectedFormat: format
};
}
/**
* Normalize structured sections format (Claude Desktop standard)
*/
normalizeStructuredSections(claudeContent, sections, adaptations) {
adaptations.push("Processed structured sections array");
claudeContent.sections.forEach((section, index) => {
const sectionContent = this.extractSectionContent(section);
const sectionType = this.detectSectionType(section);
sections.push({
id: `section_${index}`,
title: section.title || `Section ${index + 1}`,
content: sectionContent,
type: sectionType,
originalData: section,
position: index
});
});
}
/**
* Normalize content sections format (legacy)
*/
normalizeContentSections(claudeContent, sections, adaptations) {
adaptations.push("Processed legacy contentSections array");
claudeContent.contentSections.forEach((section, index) => {
const sectionContent = this.extractSectionContent(section);
const sectionType = this.detectSectionType(section);
sections.push({
id: `content_section_${index}`,
title: section.title || `Content Section ${index + 1}`,
content: sectionContent,
type: sectionType,
originalData: section,
position: index
});
});
}
/**
* Normalize object-based content
*/
normalizeObjectContent(claudeContent, sections, adaptations) {
adaptations.push("Processed structured object content");
const objectContent = claudeContent.content;
let sectionIndex = 0;
// Process main content object
Object.entries(objectContent).forEach(([key, value]) => {
if (this.isContentProperty(key, value)) {
const content = this.extractValueContent(value);
if (content.trim().length > 0) {
sections.push({
id: `obj_section_${sectionIndex++}`,
title: this.formatPropertyTitle(key),
content: content,
type: this.inferTypeFromKey(key),
originalData: { key, value },
position: sectionIndex
});
}
}
});
}
/**
* Normalize string-based content
*/
normalizeStringContent(claudeContent, sections, adaptations) {
adaptations.push("Processed string content with markdown parsing");
const content = claudeContent.content;
const parsedSections = this.parseMarkdownToSections(content);
parsedSections.forEach((section, index) => {
sections.push({
id: `string_section_${index}`,
title: section.title,
content: section.content,
type: section.type,
originalData: section,
position: index
});
});
}
/**
* Normalize flat educational object
*/
normalizeFlatEducational(claudeContent, sections, adaptations) {
adaptations.push("Processed flat educational object");
let sectionIndex = 0;
Object.entries(claudeContent).forEach(([key, value]) => {
if (key !== 'title' && this.isContentProperty(key, value)) {
const content = this.extractValueContent(value);
if (content.trim().length > 0) {
sections.push({
id: `flat_section_${sectionIndex++}`,
title: this.formatPropertyTitle(key),
content: content,
type: this.inferTypeFromKey(key),
originalData: { key, value },
position: sectionIndex
});
}
}
});
}
/**
* Normalize minimal text content
*/
normalizeMinimalText(claudeContent, sections, adaptations) {
adaptations.push("Processed minimal text content");
let sectionIndex = 0;
Object.entries(claudeContent).forEach(([key, value]) => {
if (typeof value === 'string' && value.length > 20) {
sections.push({
id: `minimal_section_${sectionIndex++}`,
title: this.formatPropertyTitle(key),
content: value,
type: this.inferTypeFromKey(key),
originalData: { key, value },
position: sectionIndex
});
}
});
}
/**
* Extract content from section object
*/
extractSectionContent(section) {
let content = '';
// Direct content property
if (section.content) {
if (typeof section.content === 'string') {
content += section.content;
} else if (typeof section.content === 'object') {
content += this.extractValueContent(section.content);
}
}
// Add vocabulary terms
if (section.terms && Array.isArray(section.terms)) {
section.terms.forEach(term => {
content += `\n${term.term}: ${term.definition}`;
});
}
// Add questions
if (section.questions && Array.isArray(section.questions)) {
section.questions.forEach(q => {
content += `\n${q.question}`;
if (q.options) {
q.options.forEach(opt => content += `\n- ${opt}`);
}
});
}
return content.trim();
}
/**
* Detect section type from section data
*/
detectSectionType(section) {
// Direct type property
if (section.type) {
return this.mapSectionTypeToContentType(section.type);
}
// Detect from data structures
if (section.terms && Array.isArray(section.terms)) {
return 'vocabulary';
}
if (section.questions && Array.isArray(section.questions)) {
return 'assessment';
}
// Detect from title/content
const title = (section.title || '').toLowerCase();
const content = (section.content || '').toLowerCase();
if (title.includes('quiz') || title.includes('avaliação') || content.includes('questão')) {
return 'assessment';
}
if (title.includes('vocabulário') || title.includes('terms')) {
return 'vocabulary';
}
if (title.includes('introdução') || title.includes('objetivo')) {
return 'introduction';
}
return 'explanatory';
}
/**
* Check if property contains educational content
*/
isContentProperty(key, value) {
if (typeof value !== 'string' && typeof value !== 'object') {
return false;
}
if (typeof value === 'string' && value.length < 10) {
return false;
}
// Skip metadata properties
const metadataKeys = ['id', 'timestamp', 'version', 'metadata', 'config'];
if (metadataKeys.includes(key.toLowerCase())) {
return false;
}
return true;
}
/**
* Extract text content from various value types
*/
extractValueContent(value) {
if (typeof value === 'string') {
return value;
}
if (Array.isArray(value)) {
return value.map(item => this.extractValueContent(item)).join('\n');
}
if (typeof value === 'object' && value !== null) {
return Object.values(value).map(item => this.extractValueContent(item)).join('\n');
}
return String(value);
}
/**
* Format property key as section title
*/
formatPropertyTitle(key) {
return key
.replace(/([A-Z])/g, ' $1')
.replace(/^./, str => str.toUpperCase())
.trim();
}
/**
* Infer content type from property key
*/
inferTypeFromKey(key) {
const keyLower = key.toLowerCase();
if (keyLower.includes('intro') || keyLower.includes('objetivo')) {
return 'introduction';
}
if (keyLower.includes('vocab') || keyLower.includes('terms')) {
return 'vocabulary';
}
if (keyLower.includes('quiz') || keyLower.includes('assessment') || keyLower.includes('questions')) {
return 'assessment';
}
if (keyLower.includes('conclusion') || keyLower.includes('summary')) {
return 'conclusion';
}
return 'explanatory';
}
/**
* Parse markdown content into sections
*/
parseMarkdownToSections(content) {
const sections = [];
const lines = content.split('\n');
let currentSection = null;
let sectionIndex = 0;
lines.forEach(line => {
const trimmed = line.trim();
if (this.isMarkdownHeader(trimmed)) {
// Save previous section
if (currentSection && currentSection.content.trim()) {
sections.push(currentSection);
}
// Start new section
currentSection = {
title: trimmed.replace(/^#+\s*/, ''),
content: '',
type: 'explanatory'
};
} else if (currentSection) {
currentSection.content += line + '\n';
} else if (trimmed) {
// No section started, create default
currentSection = {
title: 'Introduction',
content: line + '\n',
type: 'introduction'
};
}
});
// Don't forget last section
if (currentSection && currentSection.content.trim()) {
sections.push(currentSection);
}
return sections;
}
/**
* Check if line is markdown header
*/
isMarkdownHeader(line) {
return line.match(/^#+\s+/) !== null;
}
/**
* Add metadata sections from title, objectives, etc.
*/
addMetadataSections(claudeContent, sections, adaptations) {
// Add title as header if present
if (claudeContent.title) {
sections.unshift({
id: 'metadata_title',
title: claudeContent.title,
content: claudeContent.title,
type: 'header',
originalData: { source: 'title' },
position: -1
});
adaptations.push("Added title as header section");
}
}
/**
* Analyze content structure (simplified for v1.1.0)
*/
analyzeContentStructure(normalizedContent) {
const sections = normalizedContent.sections.map(section => ({
id: section.id,
content: section.content,
type: section.type,
position: section.position,
length: section.content.length,
title: section.title
}));
return { sections };
}
/**
* 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 section for widget recommendations
contentStructure.sections.forEach((section, index) => {
const widgetType = this.mapContentToWidget(section.type, section.content);
if (widgetType) {
recommendations.push({
widgetType: widgetType,
confidence: this.calculateConfidence(section.type, section.content),
contentSegments: [section.id],
content: section.content,
rationale: this.getRationale(widgetType, section.type),
educationalGoal: this.getEducationalGoal(widgetType),
priority: index + 1
});
}
});
console.error(`[CONTENT_ANALYZER_V110] Generated ${recommendations.length} widget recommendations`);
return recommendations;
}
/**
* Map content type to appropriate widget
*/
mapContentToWidget(contentType, content) {
const mapping = {
'header': 'head-1',
'introduction': 'text-1',
'explanatory': 'text-1',
'vocabulary': 'flashcards-1',
'assessment': 'quiz-1',
'conclusion': 'text-1',
'list': 'list-1'
};
return mapping[contentType] || 'text-1';
}
/**
* Calculate confidence score for widget recommendation
*/
calculateConfidence(contentType, content) {
const baseConfidence = {
'header': 1.0,
'introduction': 0.9,
'explanatory': 0.8,
'vocabulary': 0.9,
'assessment': 0.95,
'conclusion': 0.85,
'list': 0.9
};
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': 'Text content detected - suitable for reading and explanation',
'flashcards-1': 'Vocabulary content detected - perfect for memorization',
'quiz-1': 'Assessment content detected - ideal for knowledge testing',
'list-1': 'List content detected - structured presentation needed'
};
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'
};
return goals[widgetType] || 'learning_support';
}
/**
* Analyze educational flow (simplified for v1.1.0)
*/
analyzeEducationalFlow(recommendations) {
const widgetTypes = recommendations.map(r => r.widgetType);
return {
hasIntroduction: widgetTypes.includes('head-1'),
hasMainContent: widgetTypes.includes('text-1'),
hasInteractivity: widgetTypes.includes('flashcards-1') || widgetTypes.includes('quiz-1'),
hasAssessment: widgetTypes.includes('quiz-1'),
score: widgetTypes.length * 20, // Simple scoring
recommendations: []
};
}
/**
* Generate optimization suggestions (simplified for v1.1.0)
*/
generateOptimizations(educationalFlow, recommendations) {
const optimizations = [];
if (!educationalFlow.hasInteractivity) {
optimizations.push({
type: 'interactivity',
message: 'Consider adding interactive elements for better engagement',
severity: 'suggestion'
});
}
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;
}
/**
* Map section types to content types
*/
mapSectionTypeToContentType(sectionType) {
const typeMap = {
'introduction': 'introduction',
'concept_explanation': 'explanatory',
'main_content': 'explanatory',
'vocabulary': 'vocabulary',
'assessment': 'assessment',
'quiz': 'assessment',
'conclusion': 'conclusion'
};
return typeMap[sectionType] || 'explanatory';
}
/**
* Initialize content type patterns
*/
initializeContentTypes() {
return {
introduction: ['introdução', 'objetivo', 'vamos', 'começar'],
vocabulary: ['vocabulário', 'termos', 'definições'],
assessment: ['quiz', 'avaliação', 'questões', 'perguntas'],
explanatory: ['explicação', 'conceito', 'processo'],
conclusion: ['conclusão', 'resumo', 'fechamento']
};
}
/**
* Initialize widget mapping rules
*/
initializeWidgetMappings() {
return {
introduction: { primary: 'text-1', alternatives: ['head-1'] },
vocabulary: { primary: 'flashcards-1', alternatives: ['text-1'] },
assessment: { primary: 'quiz-1', alternatives: ['text-1'] },
explanatory: { primary: 'text-1', alternatives: [] },
conclusion: { primary: 'text-1', alternatives: [] }
};
}
}
/**
* Create and export the tool instance for JIT server integration
*/
export function createContentWidgetAnalyzerV110() {
const analyzer = new ContentWidgetAnalyzerV110();
return {
name: 'analyze_content_for_widgets_v110',
description: 'STEP 2 v1.1.0: Reliably analyze any educational content format with fail-fast validation and dynamic adaptation. Preserves strategic differential while ensuring robust structure handling.',
inputSchema: {
type: 'object',
properties: {
claudeContent: {
type: 'object',
description: 'Natural educational content in any format - dynamically adapts to structure'
}
},
required: ['claudeContent']
},
handler: async (input) => {
return await analyzer.analyzeContent(input);
}
};
}