Skip to main content
Glama
nlp-widget-parser.ts9.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; } }

Latest Blog Posts

MCP directory API

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

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

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