nlp-widget-parser.ts•9.7 kB
/**
* Natural Language Processing for Widget Creation
* Converts natural language prompts into Composer widget structures
*/
export interface CompositionWidget {
id: string;
type: string;
content_title?: string;
primary_color?: string;
secondary_color?: string;
category?: string;
background_image?: string;
avatar?: string;
avatar_border_color?: string;
author_name?: string;
author_office?: string;
show_category?: boolean;
show_author_name?: boolean;
show_divider?: boolean;
content?: string;
text?: string;
html?: string;
items?: any[];
title?: string;
subtitle?: string;
gallery_items?: any[];
hotspot_items?: any[];
}
export class NLPWidgetParser {
private widgetTemplates = {
'head-1': {
type: 'head-1',
content_title: null,
primary_color: '#FFFFFF',
secondary_color: '#aa2c23',
background_image: 'https://pocs.digitalpages.com.br/rdpcomposer/media/head-1/background.png',
avatar: 'https://pocs.digitalpages.com.br/rdpcomposer/media/head-1/avatar.png',
avatar_border_color: '#00643e',
show_category: true,
show_author_name: true,
show_divider: true,
},
'text-1': {
type: 'text-1',
},
'gallery-1': {
type: 'gallery-1',
gallery_items: [],
},
'hotspot-1': {
type: 'hotspot-1',
hotspot_items: [],
},
'list-1': {
type: 'list-1',
items: [],
},
};
private keywordMap = {
header: ['header', 'título', 'cabeçalho', 'title', 'heading', 'h1', 'h2'],
text: ['text', 'texto', 'paragraph', 'paragrafo', 'content', 'conteúdo'],
gallery: ['gallery', 'galeria', 'images', 'imagens', 'photos', 'fotos'],
hotspot: ['hotspot', 'interactive', 'interativo', 'clickable', 'clicável'],
list: ['list', 'lista', 'items', 'itens', 'bullet', 'numbered'],
};
public parsePromptToWidgets(prompt: string): CompositionWidget[] {
const widgets: CompositionWidget[] = [];
// Split prompt into sections
const sections = this.splitIntoSections(prompt);
sections.forEach((section, index) => {
const widget = this.parseSection(section, index);
if (widget) {
widgets.push(widget);
}
});
// If no widgets were created, create a default text widget
if (widgets.length === 0) {
widgets.push(this.createTextWidget(prompt, 0));
}
return widgets;
}
private splitIntoSections(prompt: string): string[] {
// Split by double newlines or common section indicators
const sections = prompt.split(/\n\s*\n+|(?=^[#*-]\s)|(?=^\d+\.)/gm);
return sections.filter(s => s.trim().length > 0);
}
private parseSection(section: string, index: number): CompositionWidget | null {
const trimmedSection = section.trim();
const firstLine = trimmedSection.split('\n')[0].toLowerCase();
// Detect widget type based on content analysis
const widgetType = this.detectWidgetType(trimmedSection);
switch (widgetType) {
case 'header':
return this.createHeaderWidget(trimmedSection, index);
case 'gallery':
return this.createGalleryWidget(trimmedSection, index);
case 'hotspot':
return this.createHotspotWidget(trimmedSection, index);
case 'list':
return this.createListWidget(trimmedSection, index);
default:
return this.createTextWidget(trimmedSection, index);
}
}
private detectWidgetType(content: string): string {
const lowerContent = content.toLowerCase();
// Check for explicit widget type mentions
for (const [type, keywords] of Object.entries(this.keywordMap)) {
if (keywords.some(keyword => lowerContent.includes(keyword))) {
return type;
}
}
// Heuristic detection
if (content.startsWith('#') || content.match(/^[A-Z][^.!?]*$/m)) {
return 'header';
}
if (content.includes('image') || content.includes('foto') || content.includes('picture')) {
return 'gallery';
}
if (content.match(/^\s*[-•*]\s/m) || content.match(/^\s*\d+\.\s/m)) {
return 'list';
}
if (content.includes('click') || content.includes('interactive')) {
return 'hotspot';
}
return 'text';
}
private createHeaderWidget(content: string, index: number): CompositionWidget {
const lines = content.split('\n');
const title = lines[0].replace(/^#+\s*/, '').trim();
const subtitle = lines.length > 1 ? lines[1].trim() : '';
return {
id: this.generateId(index),
...this.widgetTemplates['head-1'],
content_title: null,
category: `<p>${title}</p>`,
author_name: subtitle ? `<p>${subtitle}</p>` : '<p>Author</p>',
author_office: '<p>Description</p>',
};
}
private createTextWidget(content: string, index: number): CompositionWidget {
return {
id: this.generateId(index),
...this.widgetTemplates['text-1'],
content: `<p>${this.formatTextContent(content)}</p>`,
};
}
private createGalleryWidget(content: string, index: number): CompositionWidget {
const images = this.extractImageReferences(content);
return {
id: this.generateId(index),
...this.widgetTemplates['gallery-1'],
gallery_items: images.map((img, i) => ({
id: `img-${index}-${i}`,
url: img.url || 'https://via.placeholder.com/400x300',
caption: img.caption || `Image ${i + 1}`,
alt: img.alt || img.caption || `Gallery image ${i + 1}`,
})),
};
}
private createHotspotWidget(content: string, index: number): CompositionWidget {
const hotspots = this.extractHotspotData(content);
return {
id: this.generateId(index),
...this.widgetTemplates['hotspot-1'],
hotspot_items: hotspots,
background_image: 'https://via.placeholder.com/800x600',
};
}
private createListWidget(content: string, index: number): CompositionWidget {
const items = this.extractListItems(content);
return {
id: this.generateId(index),
...this.widgetTemplates['list-1'],
items: items.map((item, i) => ({
id: `item-${index}-${i}`,
text: item,
order: i + 1,
})),
};
}
private formatTextContent(content: string): string {
// Convert markdown-like formatting to HTML
return content
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/\n/g, '<br>')
.trim();
}
private extractImageReferences(content: string): Array<{url?: string; caption?: string; alt?: string}> {
const images: Array<{url?: string; caption?: string; alt?: string}> = [];
// Look for image URLs
const urlMatches = content.match(/https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp)/gi);
if (urlMatches) {
urlMatches.forEach(url => {
images.push({ url });
});
}
// Look for image descriptions
const lines = content.split('\n');
lines.forEach(line => {
if (line.toLowerCase().includes('image') || line.toLowerCase().includes('foto')) {
images.push({ caption: line.trim() });
}
});
// If no images found, create placeholder
if (images.length === 0) {
images.push({ caption: 'Gallery Image' });
}
return images;
}
private extractHotspotData(content: string): any[] {
const hotspots: any[] = [];
const lines = content.split('\n');
lines.forEach((line, i) => {
if (line.includes('click') || line.includes('interactive')) {
hotspots.push({
id: `hotspot-${i}`,
x: Math.random() * 80 + 10, // Random position
y: Math.random() * 80 + 10,
content: line.trim(),
title: `Hotspot ${i + 1}`,
});
}
});
return hotspots;
}
private extractListItems(content: string): string[] {
const lines = content.split('\n');
const items: string[] = [];
lines.forEach(line => {
const trimmed = line.trim();
if (trimmed.match(/^[-•*]\s/) || trimmed.match(/^\d+\.\s/)) {
items.push(trimmed.replace(/^[-•*]\s|^\d+\.\s/, ''));
}
});
// If no list items found, split by sentences
if (items.length === 0) {
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
items.push(...sentences.map(s => s.trim()));
}
return items;
}
private generateId(index: number): string {
return `${Date.now()}-${index}-${Math.random().toString(36).substr(2, 9)}`;
}
public applyChangesToWidgets(widgets: CompositionWidget[], changes: string): CompositionWidget[] {
const lowerChanges = changes.toLowerCase();
const updatedWidgets = [...widgets];
if (lowerChanges.includes('add') || lowerChanges.includes('adicionar')) {
// Parse the changes as new content and add widgets
const newWidgets = this.parsePromptToWidgets(changes);
updatedWidgets.push(...newWidgets);
}
if (lowerChanges.includes('remove') || lowerChanges.includes('remover')) {
// Simple removal logic - remove last widget
if (updatedWidgets.length > 0) {
updatedWidgets.pop();
}
}
if (lowerChanges.includes('change color') || lowerChanges.includes('mudar cor')) {
// Extract color and apply to header widgets
const colorMatch = changes.match(/#[0-9a-fA-F]{6}|rgb\([^)]+\)|[a-zA-Z]+/);
if (colorMatch) {
updatedWidgets.forEach(widget => {
if (widget.type === 'head-1') {
widget.secondary_color = colorMatch[0];
}
});
}
}
return updatedWidgets;
}
}