get-widget-requirements.js•29.7 kB
#!/usr/bin/env node
/**
* Widget Requirements Tool v5.2.0 - FULLY OPERATIONAL
* Just-In-Time approach: Provides precise API requirements for selected widgets only
* @version 5.2.0 (January 12, 2025)
* @status FULLY OPERATIONAL - Precise JIT requirements delivery
* @reference JIT workflow step 3 of 7
* @milestone v5.2.0 - Token-efficient widget-specific requirements
*/
export class WidgetRequirementsTool {
constructor() {
this.processingStartTime = null;
this.requirementsDatabase = this.initializeRequirementsDatabase();
}
/**
* Main requirements entry point for JIT workflow
* @param {Object} input - Contains selectedWidgets (array or object with widget types)
* @returns {Object} Precise API requirements for selected widgets only
*/
async getWidgetRequirements(input) {
this.processingStartTime = new Date();
try {
const { selectedWidgets } = input;
// Validate input
if (!selectedWidgets) {
throw new Error('selectedWidgets is required');
}
// Normalize widget types from different input formats
const widgetTypes = this.extractWidgetTypes(selectedWidgets);
if (widgetTypes.length === 0) {
const supportedFormats = [
'Array of widget objects: [{ type: "head-1", count: 1 }, ...]',
'Array of strings: ["head-1", "text-1", "quiz-1"]',
'Object with widgets array: { widgets: [...] }',
'Object with selectedWidgetTypes: { selectedWidgetTypes: [...] }',
'Single string: "head-1"'
];
throw new Error(`No valid widget types found in selectedWidgets.
Supported formats:
${supportedFormats.join('\n')}
Received: ${JSON.stringify(selectedWidgets, null, 2)}`);
}
// Get specific requirements for each widget type
const specificRequirements = this.getSpecificRequirements(widgetTypes);
// Generate targeted examples
const targetedExamples = this.generateTargetedExamples(widgetTypes);
// Create validation rules
const validationRules = this.generateValidationChecklist(widgetTypes);
// Create JIT requirements response
const requirements = {
success: true,
data: {
selectedWidgetTypes: widgetTypes,
specificRequirements: specificRequirements,
targetedExamples: targetedExamples,
validationRules: validationRules,
jitGuidance: {
approach: "Token-efficient Just-In-Time delivery",
principle: "Only provide requirements for widgets you will actually use",
benefit: "Precise, relevant requirements without overwhelming detail",
nextStep: "Use these requirements to structure your lesson data in validate_lesson_data"
},
lessonStructureGuidance: {
requiredFormat: {
metadata: {
topic: "string - lesson topic/title",
gradeLevel: "string - target grade level",
subject: "string - subject area",
learningObjectives: "array - learning goals"
},
widgets: "array - widget objects with type and content matching requirements above"
},
criticalReminder: "Structure your lesson data exactly as shown - this format is required for validate_lesson_data tool"
},
commonPitfalls: this.getCommonPitfalls(widgetTypes),
qualityTips: this.getQualityTips(widgetTypes)
},
debug: {
timestamp: new Date().toISOString(),
processingTime: Date.now() - this.processingStartTime.getTime(),
widgetTypesProcessed: widgetTypes.length,
requirementsGenerated: Object.keys(specificRequirements).length
}
};
return requirements;
} catch (error) {
console.error('[WIDGET_REQUIREMENTS] Error:', error.message);
return {
success: false,
error: {
code: 'REQUIREMENTS_ERROR',
message: error.message,
timestamp: new Date().toISOString()
}
};
}
}
/**
* Extract widget types from various input formats
* ENHANCED: Now handles multiple flexible parameter formats
*/
extractWidgetTypes(selectedWidgets) {
let widgetTypes = [];
console.error('[WIDGET_REQUIREMENTS] Extracting widget types from:', typeof selectedWidgets);
console.error('[WIDGET_REQUIREMENTS] Input structure:', JSON.stringify(selectedWidgets, null, 2));
// Handle direct array format: [{ type: "head-1", count: 1, ... }, ...]
if (Array.isArray(selectedWidgets)) {
widgetTypes = selectedWidgets.map(item => {
if (typeof item === 'string') return item;
if (item && typeof item === 'object' && item.type) return item.type;
return null;
}).filter(Boolean);
}
// Handle nested object with widgets array: { widgets: [...] }
else if (selectedWidgets.widgets && Array.isArray(selectedWidgets.widgets)) {
widgetTypes = selectedWidgets.widgets.map(item =>
typeof item === 'string' ? item : item.type
).filter(Boolean);
}
// Handle object with selectedWidgetTypes array
else if (selectedWidgets.selectedWidgetTypes && Array.isArray(selectedWidgets.selectedWidgetTypes)) {
widgetTypes = selectedWidgets.selectedWidgetTypes.map(item =>
typeof item === 'string' ? item : item.type
).filter(Boolean);
}
// Handle object with widgetTypes array
else if (selectedWidgets.widgetTypes && Array.isArray(selectedWidgets.widgetTypes)) {
widgetTypes = selectedWidgets.widgetTypes.filter(type => typeof type === 'string');
}
// Handle mapping rationale structure (from analyze_content_for_widgets output)
else if (selectedWidgets.mappingRationale && selectedWidgets.mappingRationale.widgetTypes) {
widgetTypes = selectedWidgets.mappingRationale.widgetTypes;
}
// Handle single widget type
else if (typeof selectedWidgets === 'string') {
widgetTypes = [selectedWidgets];
}
// Handle object with direct widget type properties
else if (selectedWidgets && typeof selectedWidgets === 'object') {
// Try to extract from any property that looks like it contains widget types
Object.keys(selectedWidgets).forEach(key => {
const value = selectedWidgets[key];
if (Array.isArray(value)) {
value.forEach(item => {
if (typeof item === 'string' && this.isValidWidgetType(item)) {
widgetTypes.push(item);
} else if (item && item.type && this.isValidWidgetType(item.type)) {
widgetTypes.push(item.type);
}
});
}
});
}
// Remove duplicates and ensure valid types
const uniqueTypes = [...new Set(widgetTypes)].filter(type => this.isValidWidgetType(type));
console.error('[WIDGET_REQUIREMENTS] Extracted widget types:', uniqueTypes);
return uniqueTypes;
}
/**
* Get common pitfalls for selected widget types
*/
getCommonPitfalls(widgetTypes) {
const pitfalls = [];
if (widgetTypes.includes('quiz-1')) {
pitfalls.push({
widget: "quiz-1",
pitfall: "Using 'answers' instead of 'options'",
solution: "Use 'options' array in your content - API will convert to 'answers'"
});
}
if (widgetTypes.includes('flashcards-1')) {
pitfalls.push({
widget: "flashcards-1",
pitfall: "Inconsistent field names",
solution: "Use 'question'/'answer' OR 'front'/'back' consistently"
});
}
if (widgetTypes.includes('text-1')) {
pitfalls.push({
widget: "text-1",
pitfall: "Plain text instead of HTML",
solution: "Wrap content in HTML tags like <p>, <h2>, <ul>, etc."
});
}
return pitfalls;
}
/**
* Get quality tips for selected widget types
*/
getQualityTips(widgetTypes) {
const tips = [];
if (widgetTypes.includes('head-1')) {
tips.push("Always start lessons with head-1 widget for professional appearance");
}
if (widgetTypes.includes('flashcards-1')) {
tips.push("Keep flashcard questions short and answers comprehensive but concise");
}
if (widgetTypes.includes('quiz-1')) {
tips.push("Include plausible distractors in quiz options to test real understanding");
}
if (widgetTypes.includes('text-1')) {
tips.push("Structure text content with headings, paragraphs, and lists for better readability");
}
if (widgetTypes.includes('image-1')) {
tips.push("Choose high-quality, relevant images that enhance understanding");
}
return tips;
}
/**
* Get specific API requirements for widget types
*/
getSpecificRequirements(widgetTypes) {
const specificReqs = {};
widgetTypes.forEach(type => {
if (this.requirementsDatabase[type]) {
specificReqs[type] = this.requirementsDatabase[type];
}
});
return specificReqs;
}
/**
* Initialize comprehensive widget requirements database
*/
initializeRequirementsDatabase() {
return {
'head-1': {
apiName: "head-1",
description: "Professional lesson header - always use as first widget",
requiredFields: {
category: {
type: "string",
format: "Plain text or HTML",
example: "\"CIÊNCIAS\" or \"<p>BIOLOGIA</p>\"",
apiFieldName: "category"
},
author_name: {
type: "string",
format: "Plain text or HTML",
example: "\"Professor João\" or \"<p>Professor João</p>\"",
apiFieldName: "author_name"
}
},
optionalFields: {
author_office: {
type: "string",
format: "Plain text or HTML",
example: "\"Especialista em Biologia\"",
apiFieldName: "author_office"
},
background_image: {
type: "string",
format: "Valid URL",
example: "\"https://images.unsplash.com/photo-1234567890\"",
apiFieldName: "background_image"
},
avatar: {
type: "string",
format: "Valid URL",
example: "\"https://ui-avatars.com/api/?name=Professor\"",
apiFieldName: "avatar"
}
},
complexity: "low"
},
'text-1': {
apiName: "text-1",
description: "Rich HTML content display",
requiredFields: {
text: {
type: "string",
format: "HTML content, minimum 20 characters",
example: "\"<h2>Título</h2><p>Conteúdo educacional detalhado...</p>\"",
apiFieldName: "text",
validation: "Must be at least 20 characters long"
}
},
optionalFields: {
content_title: {
type: "string",
format: "Plain text title",
example: "\"Introdução à Fotossíntese\"",
apiFieldName: "content_title"
},
background_color: {
type: "string",
format: "Hex color code",
example: "\"#FFFFFF\"",
apiFieldName: "background_color"
}
},
complexity: "medium"
},
'flashcards-1': {
apiName: "flashcards-1",
description: "Interactive flip cards for memorization",
requiredFields: {
flashcards_items: {
type: "array",
format: "Array of flashcard objects",
apiFieldName: "flashcards_items",
itemStructure: {
question: {
type: "string",
format: "Front of card content",
example: "\"Clorofila\"",
apiFieldName: "question"
},
answer: {
type: "string",
format: "Back of card content",
example: "\"Pigmento verde que captura luz solar\"",
apiFieldName: "answer"
}
},
alternativeFormat: {
front: "Can use \"front\" instead of \"question\"",
back: "Can use \"back\" instead of \"answer\""
}
}
},
complexity: "low"
},
'quiz-1': {
apiName: "quiz-1",
description: "Interactive multiple-choice quiz",
requiredFields: {
questions: {
type: "array",
format: "Array of question objects",
apiFieldName: "questions",
itemStructure: {
question: {
type: "string",
format: "Question text, minimum 10 characters",
example: "\"Qual é o principal produto da fotossíntese?\"",
apiFieldName: "question"
},
options: {
type: "array",
format: "Array of answer choices (will become \"answers\" in API)",
example: "[\"Oxigênio\", \"Glicose\", \"Água\", \"CO2\"]",
apiFieldName: "options",
apiConversion: "Converted to \"answers\" field in API"
},
correct_option: {
type: "number",
format: "Zero-based index of correct answer",
example: "1 (for second option)",
apiFieldName: "correct_option"
}
}
}
},
optionalFields: {
max_attempts: {
type: "number",
format: "Number between 1-5",
example: "3",
apiFieldName: "max_attempts"
}
},
criticalNote: "Use \"options\" array in your content - API will convert to \"answers\"",
complexity: "high"
},
'image-1': {
apiName: "image-1",
description: "Single image display",
requiredFields: {
image: {
type: "string",
format: "Valid image URL",
example: "\"https://images.unsplash.com/photo-1234567890\"",
apiFieldName: "image",
validation: "Must be accessible image URL"
}
},
optionalFields: {
caption: {
type: "string",
format: "Image description/caption",
example: "\"Processo de fotossíntese em folhas verdes\"",
apiFieldName: "caption"
}
},
complexity: "low"
},
'list-1': {
apiName: "list-1",
description: "Structured list presentation",
requiredFields: {
items: {
type: "array",
format: "Array of list items",
example: "[\"Item 1\", \"Item 2\", \"Item 3\"]",
apiFieldName: "items",
validation: "Must have at least 2 items"
}
},
optionalFields: {
content_title: {
type: "string",
format: "List title",
example: "\"Tipos de Transferência de Calor\"",
apiFieldName: "content_title"
}
},
complexity: "low"
}
};
}
/**
* Generate targeted examples for selected widgets only
*/
generateTargetedExamples(widgetTypes) {
const examples = {};
widgetTypes.forEach(type => {
examples[type] = this.createWidgetExample(type);
});
return examples;
}
/**
* Create widget-specific example content
*/
createWidgetExample(widgetType) {
const exampleTemplates = {
'head-1': {
description: 'Professional lesson header with required fields',
example: {
type: 'head-1',
content: {
category: 'CIÊNCIAS',
author_name: 'Professor(a) Virtual',
author_office: 'Especialista em Biologia'
}
},
notes: 'Always use as first widget. Category and author_name are required.'
},
'text-1': {
description: 'HTML educational content with minimum 20 characters',
example: {
type: 'text-1',
content: {
text: '<h2>Título da Seção</h2><p>Conteúdo educacional detalhado explicando conceitos importantes. Mínimo de 20 caracteres necessário para validação.</p>'
}
},
notes: 'Use HTML formatting. Minimum 20 characters required.'
},
'quiz-1': {
description: 'Multiple-choice quiz with options array and correct_option index',
example: {
type: 'quiz-1',
content: {
questions: [
{
question: 'Qual é o principal produto da fotossíntese?',
options: ['Oxigênio', 'Glicose', 'Água', 'CO2'],
correct_option: 1
}
],
max_attempts: 3
}
},
notes: 'CRITICAL: Use "options" array (not "choices"). Use "correct_option" index (0-based).'
},
'flashcards-1': {
description: 'Flip cards with question/answer format',
example: {
type: 'flashcards-1',
content: {
flashcards_items: [
{
question: 'Clorofila',
answer: 'Pigmento verde responsável por capturar a luz solar'
}
]
}
},
notes: 'Use "flashcards_items" array with "question"/"answer" format.'
},
'list-1': {
description: 'Structured list with items array',
example: {
type: 'list-1',
content: {
items: ['Capturar luz solar', 'Absorver CO2', 'Produzir glicose'],
list_type: 'numbered'
}
},
notes: 'CRITICAL: Use "items" array (not "list_items"). Optional list_type: bullet, numbered, checkbox.'
},
'image-1': {
description: 'Single image display with caption',
example: {
type: 'image-1',
content: {
image: 'https://images.unsplash.com/photo-1416879595882-3373a0480b5b?w=800',
caption: 'Processo de fotossíntese em folhas verdes'
}
},
notes: 'Image URL must be valid and accessible. Caption is optional.'
},
'video-1': {
description: 'Video content with valid URL',
example: {
type: 'video-1',
content: {
video: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
}
},
notes: 'Video URL must be valid YouTube, Vimeo, or direct video link.'
},
'gallery-1': {
description: 'Multiple images in carousel format',
example: {
type: 'gallery-1',
content: {
slides: [
{
image: 'https://images.unsplash.com/photo-1',
caption: 'Primeira etapa do processo'
},
{
image: 'https://images.unsplash.com/photo-2',
caption: 'Segunda etapa do processo'
}
]
}
},
notes: 'Each slide needs image URL. Caption is optional.'
},
'hotspots-1': {
description: 'Interactive image with clickable markers',
example: {
type: 'hotspots-1',
content: {
background_image: 'https://images.unsplash.com/photo-plant-cell',
markers: [
{
x: 50,
y: 30,
title: 'Cloroplasto',
content: 'Organela responsável pela fotossíntese'
}
]
}
},
notes: 'CRITICAL: Use numeric coordinates (50, 30) not strings ("50%"). API converts to percentages.'
}
};
return exampleTemplates[widgetType] || {
description: `Example not available for ${widgetType}`,
example: {},
notes: 'Refer to general API documentation'
};
}
/**
* Create focused reminders for selected widgets
*/
createFocusedReminders(widgetTypes) {
const allReminders = this.apiCatalog.getAPIRequirementsForClaude().criticalReminders;
const focused = [];
// Widget-specific critical reminders
const widgetReminders = {
'quiz-1': 'Quiz questions: Use "options" array with "correct_option" index (API will convert to "answers")',
'list-1': 'List widgets: Use "items" array (API will convert to "list_items")',
'hotspots-1': 'Hotspots: Use numeric coordinates like 50, 30 (API will convert to "50%", "30%")',
'flashcards-1': 'Flashcards: Use "flashcards_items" array with "question"/"answer" or "front"/"back"',
'text-1': 'All text content: Minimum 20 characters for validation',
'image-1': 'Image URLs: Must be complete valid URLs starting with https://',
'video-1': 'Video URLs: Must be complete valid URLs starting with https://',
'gallery-1': 'Gallery slides: Each slide must have valid image URL',
'head-1': 'Header: category and author_name are required fields'
};
widgetTypes.forEach(type => {
if (widgetReminders[type]) {
focused.push(widgetReminders[type]);
}
});
// Always include general reminders
focused.push('URLs: Must be complete valid URLs starting with https://');
focused.push('Text content: Minimum 20 characters for all text fields');
return focused;
}
/**
* Generate widget-specific implementation tips
*/
generateWidgetSpecificTips(widgetTypes) {
const tips = {};
const tipDatabase = {
'head-1': [
'Always use as the first widget in your lesson',
'Category should reflect the subject (e.g., "CIÊNCIAS", "MATEMÁTICA")',
'Author name can be simple like "Professor(a) Virtual"'
],
'text-1': [
'Use HTML formatting for better presentation (<h2>, <p>, <strong>, <em>)',
'Break long content into multiple text widgets for better readability',
'Ensure minimum 20 characters for validation success'
],
'quiz-1': [
'Use clear, unambiguous question wording',
'Provide 3-5 answer options for optimal cognitive load',
'Include distractors that test common misconceptions',
'Set max_attempts to 2-3 for optimal learning'
],
'flashcards-1': [
'Keep question/answer pairs concise and focused',
'Use for vocabulary, definitions, and key concepts',
'Consider 5-15 cards per flashcard widget for optimal recall'
],
'list-1': [
'Use for step-by-step procedures or feature lists',
'Choose appropriate list_type: bullet, numbered, or checkbox',
'Keep list items concise and parallel in structure'
],
'image-1': [
'Use descriptive captions for accessibility',
'Ensure images are relevant to educational content',
'Consider image size and loading time'
],
'video-1': [
'Ensure videos are educational and age-appropriate',
'Keep video length appropriate for attention span',
'Provide context before and after video content'
],
'gallery-1': [
'Use for step-by-step visual processes',
'Ensure logical sequence in slide order',
'Write descriptive captions for each slide'
],
'hotspots-1': [
'Use for complex diagrams requiring detailed exploration',
'Place markers strategically for optimal learning',
'Write clear, informative content for each hotspot'
]
};
widgetTypes.forEach(type => {
if (tipDatabase[type]) {
tips[type] = tipDatabase[type];
}
});
return tips;
}
/**
* Get formatting guidance for selected widgets
*/
getFormattingGuidance(widgetTypes) {
const guidance = {
overview: 'Follow these format requirements for your selected widgets',
fieldNameMappings: {},
dataTypeRequirements: {},
validationRules: {}
};
// Field name mappings for widgets that need conversion
const mappings = {
'quiz-1': 'Your "options" array becomes "answers" in API',
'list-1': 'Your "items" array becomes "list_items" in API',
'hotspots-1': 'Your numeric coordinates become percentage strings in API'
};
widgetTypes.forEach(type => {
if (mappings[type]) {
guidance.fieldNameMappings[type] = mappings[type];
}
});
// Data type requirements
const dataTypes = {
'head-1': { category: 'string', author_name: 'string' },
'text-1': { text: 'string (min 20 chars)' },
'quiz-1': { questions: 'array', options: 'array of strings', correct_option: 'number (0-based index)' },
'flashcards-1': { flashcards_items: 'array of objects' },
'list-1': { items: 'array of strings' },
'image-1': { image: 'string (valid URL)' },
'video-1': { video: 'string (valid URL)' },
'gallery-1': { slides: 'array of objects with image URLs' },
'hotspots-1': { background_image: 'string (URL)', markers: 'array', x: 'number', y: 'number' }
};
widgetTypes.forEach(type => {
if (dataTypes[type]) {
guidance.dataTypeRequirements[type] = dataTypes[type];
}
});
return guidance;
}
/**
* Generate validation checklist for selected widgets
*/
generateValidationChecklist(widgetTypes) {
const checklist = {};
const validationRules = {
'head-1': [
'✓ Category field is present and non-empty',
'✓ Author name field is present and non-empty'
],
'text-1': [
'✓ Text content is present',
'✓ Text content has minimum 20 characters',
'✓ HTML tags are properly formatted'
],
'quiz-1': [
'✓ Questions array is present and non-empty',
'✓ Each question has "options" array (not "choices")',
'✓ Each question has "correct_option" number',
'✓ Options array has 2-6 items',
'✓ Correct_option index is valid (0-based)'
],
'flashcards-1': [
'✓ flashcards_items array is present',
'✓ Each item has question and answer fields',
'✓ Items array has 2-20 cards'
],
'list-1': [
'✓ Items array is present (not "list_items")',
'✓ Items array has 1-20 items',
'✓ All items are non-empty strings'
],
'image-1': [
'✓ Image URL is present and valid',
'✓ Image URL starts with https://',
'✓ Caption is optional but recommended'
],
'video-1': [
'✓ Video URL is present and valid',
'✓ Video URL starts with https://',
'✓ Video URL is from supported platform'
],
'gallery-1': [
'✓ Slides array is present',
'✓ Each slide has valid image URL',
'✓ Slides array has 2-15 items'
],
'hotspots-1': [
'✓ Background image URL is valid',
'✓ Markers array is present',
'✓ Each marker has numeric x, y coordinates (not strings)',
'✓ Each marker has title and content',
'✓ Coordinates are between 0-100 range'
]
};
widgetTypes.forEach(type => {
if (validationRules[type]) {
checklist[type] = validationRules[type];
}
});
return checklist;
}
/**
* Check if widget type is valid
*/
isValidWidgetType(type) {
const validTypes = [
'head-1', 'text-1', 'quiz-1', 'flashcards-1',
'image-1', 'video-1', 'list-1', 'gallery-1', 'hotspots-1'
];
return validTypes.includes(type);
}
/**
* Build widget complexity mapping
*/
buildWidgetComplexity() {
return {
'head-1': 'low',
'image-1': 'low',
'list-1': 'low',
'flashcards-1': 'low',
'text-1': 'medium',
'gallery-1': 'medium',
'video-1': 'medium',
'quiz-1': 'high',
'hotspots-1': 'high'
};
}
/**
* Build format examples database
*/
buildFormatExamples() {
return {
fieldNameExamples: {
quiz: 'options: ["A", "B", "C"], correct_option: 1',
list: 'items: ["Item 1", "Item 2", "Item 3"]',
hotspots: 'markers: [{x: 50, y: 30, title: "Title", content: "Content"}]'
},
dataTypeExamples: {
string: '"Example text content"',
number: '42',
array: '["item1", "item2", "item3"]',
object: '{"key": "value"}',
url: '"https://example.com/image.jpg"'
}
};
}
}
/**
* Create and export the tool instance for JIT server integration
*/
export function createWidgetRequirementsTool() {
const tool = new WidgetRequirementsTool();
return {
name: 'get_widget_requirements',
description: 'STEP 3: Get precise API requirements for selected widgets only. Just-in-time delivery of specific field formats.',
inputSchema: {
type: 'object',
properties: {
selectedWidgets: {
type: 'object',
description: 'Selected widget types from analyze_content_for_widgets tool or array of widget type strings'
}
},
required: ['selectedWidgets']
},
handler: async (input) => {
return await tool.getWidgetRequirements(input);
}
};
}