UNPKG

n8n-nodes-enhanced-chat

Version:

Enhanced Chat UI for n8n - Complete interactive chat solution with Enhanced Chat Trigger, Chat UI Renderer, HTML Renderer, and Callback Handler nodes. Features dynamic forms, media, WebRTC, and interactive workflows.

601 lines (600 loc) 28.6 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HTMLRenderer = void 0; const n8n_workflow_1 = require("n8n-workflow"); class HTMLRenderer { constructor() { this.description = { displayName: 'HTML Renderer', name: 'htmlRenderer', icon: 'file:htmlRenderer.svg', group: ['transform'], version: 1, description: 'Renders dynamic HTML content for Enhanced Chat UI with support for forms, media, and WebRTC', defaults: { name: 'HTML Renderer', }, inputs: ["main" /* NodeConnectionType.Main */], outputs: ["main" /* NodeConnectionType.Main */], properties: [ // === CONTENT CONFIGURATION === { displayName: 'Content Type', name: 'contentType', type: 'options', options: [ { name: 'Text', value: 'text', description: 'Plain text content', }, { name: 'HTML', value: 'html', description: 'Rich HTML content', }, { name: 'Form', value: 'form', description: 'Interactive form elements', }, { name: 'Video', value: 'video', description: 'Video playback content', }, { name: 'Audio', value: 'audio', description: 'Audio playback content', }, { name: 'WebRTC', value: 'webrtc', description: 'WebRTC communication interface', }, ], default: 'html', description: 'Type of content to render', }, // === HTML CONTENT === { displayName: 'HTML Content', name: 'htmlContent', type: 'string', typeOptions: { rows: 10, }, default: '<div class="enhanced-content">\n <h3>Welcome!</h3>\n <p>This is dynamic HTML content rendered in chat.</p>\n</div>', description: 'HTML content to render or template string', displayOptions: { show: { contentType: ['html'], }, }, }, { displayName: 'Text Content', name: 'textContent', type: 'string', typeOptions: { rows: 5, }, default: 'This is plain text content that will be safely rendered.', description: 'Plain text content to display', displayOptions: { show: { contentType: ['text'], }, }, }, // === FORM CONFIGURATION === { displayName: 'Form Configuration', name: 'formConfig', type: 'collection', placeholder: 'Add Form Setting', default: {}, displayOptions: { show: { contentType: ['form'], }, }, options: [ { displayName: 'Form Title', name: 'title', type: 'string', default: 'Feedback Form', description: 'Title for the form', }, { displayName: 'Form Fields', name: 'fields', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: {}, placeholder: 'Add Field', options: [ { name: 'field', displayName: 'Field', values: [ { displayName: 'Field Type', name: 'type', type: 'options', options: [ { name: 'Text Input', value: 'text' }, { name: 'Email Input', value: 'email' }, { name: 'Textarea', value: 'textarea' }, { name: 'Select Dropdown', value: 'select' }, { name: 'Radio Buttons', value: 'radio' }, { name: 'Checkboxes', value: 'checkbox' }, { name: 'Number Input', value: 'number' }, { name: 'Date Input', value: 'date' }, ], default: 'text', }, { displayName: 'Field Label', name: 'label', type: 'string', default: 'Field Label', }, { displayName: 'Field Name', name: 'name', type: 'string', default: 'fieldName', }, { displayName: 'Required', name: 'required', type: 'boolean', default: false, }, { displayName: 'Placeholder', name: 'placeholder', type: 'string', default: '', }, { displayName: 'Options (for select/radio/checkbox)', name: 'options', type: 'string', default: 'Option 1,Option 2,Option 3', description: 'Comma-separated list of options', displayOptions: { show: { type: ['select', 'radio', 'checkbox'], }, }, }, ], }, ], }, { displayName: 'Submit Button Text', name: 'submitText', type: 'string', default: 'Submit', description: 'Text for the submit button', }, ], }, // === MEDIA CONFIGURATION === { displayName: 'Video Configuration', name: 'videoConfig', type: 'collection', placeholder: 'Add Video Setting', default: {}, displayOptions: { show: { contentType: ['video'], }, }, options: [ { displayName: 'Video URL', name: 'url', type: 'string', default: '', description: 'URL of the video to embed', }, { displayName: 'Video Type', name: 'type', type: 'options', options: [ { name: 'MP4', value: 'mp4' }, { name: 'WebM', value: 'webm' }, { name: 'YouTube', value: 'youtube' }, { name: 'Vimeo', value: 'vimeo' }, ], default: 'mp4', }, { displayName: 'Video Title', name: 'title', type: 'string', default: 'Video Player', }, { displayName: 'Auto Play', name: 'autoplay', type: 'boolean', default: false, }, { displayName: 'Show Controls', name: 'controls', type: 'boolean', default: true, }, ], }, { displayName: 'Audio Configuration', name: 'audioConfig', type: 'collection', placeholder: 'Add Audio Setting', default: {}, displayOptions: { show: { contentType: ['audio'], }, }, options: [ { displayName: 'Audio URL', name: 'url', type: 'string', default: '', description: 'URL of the audio file', }, { displayName: 'Audio Type', name: 'type', type: 'options', options: [ { name: 'MP3', value: 'mp3' }, { name: 'WAV', value: 'wav' }, { name: 'OGG', value: 'ogg' }, ], default: 'mp3', }, { displayName: 'Audio Title', name: 'title', type: 'string', default: 'Audio Player', }, { displayName: 'Auto Play', name: 'autoplay', type: 'boolean', default: false, }, { displayName: 'Show Controls', name: 'controls', type: 'boolean', default: true, }, ], }, // === WEBRTC CONFIGURATION === { displayName: 'WebRTC Configuration', name: 'webrtcConfig', type: 'collection', placeholder: 'Add WebRTC Setting', default: {}, displayOptions: { show: { contentType: ['webrtc'], }, }, options: [ { displayName: 'WebRTC Type', name: 'type', type: 'options', options: [ { name: 'Video Call', value: 'video' }, { name: 'Audio Call', value: 'audio' }, { name: 'Screen Share', value: 'screen' }, ], default: 'video', }, { displayName: 'Room ID', name: 'roomId', type: 'string', default: '', description: 'Unique room identifier for the WebRTC session', }, { displayName: 'STUN/TURN Servers', name: 'iceServers', type: 'string', default: 'stun:stun.l.google.com:19302', description: 'ICE servers for WebRTC connection (comma-separated)', }, { displayName: 'Enable Video', name: 'video', type: 'boolean', default: true, }, { displayName: 'Enable Audio', name: 'audio', type: 'boolean', default: true, }, ], }, // === SECURITY & CALLBACK SETTINGS === { displayName: 'Security & Callback Settings', name: 'securitySettings', type: 'collection', placeholder: 'Add Security Setting', default: {}, options: [ { displayName: 'Sanitize HTML Output', name: 'sanitizeOutput', type: 'boolean', default: true, description: 'Sanitize HTML to prevent XSS attacks', }, { displayName: 'Callback URL', name: 'callbackUrl', type: 'string', default: '', description: 'URL to handle form submissions and interactions', }, { displayName: 'Session ID', name: 'sessionId', type: 'string', default: '', description: 'Session identifier for tracking user interactions', }, { displayName: 'CORS Origins', name: 'corsOrigins', type: 'string', default: '*', description: 'Allowed origins for CORS (comma-separated)', }, { displayName: 'Trusted Content', name: 'trustedContent', type: 'boolean', default: false, description: 'Skip sanitization for trusted HTML content', }, ], }, // === STYLING OPTIONS === { displayName: 'Styling Options', name: 'stylingOptions', type: 'collection', placeholder: 'Add Styling', default: {}, options: [ { displayName: 'Container CSS Classes', name: 'containerClasses', type: 'string', default: 'enhanced-html-content', description: 'CSS classes for the container element', }, { displayName: 'Custom CSS', name: 'customCss', type: 'string', typeOptions: { rows: 5, }, default: '', description: 'Custom CSS to include with the content', }, { displayName: 'Theme', name: 'theme', type: 'options', options: [ { name: 'Default', value: 'default' }, { name: 'Dark', value: 'dark' }, { name: 'Light', value: 'light' }, { name: 'Custom', value: 'custom' }, ], default: 'default', }, ], }, ], }; } execute() { return __awaiter(this, void 0, void 0, function* () { const items = this.getInputData(); const returnData = []; for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { try { // Get node parameters const contentType = this.getNodeParameter('contentType', itemIndex); const securitySettings = this.getNodeParameter('securitySettings', itemIndex); const stylingOptions = this.getNodeParameter('stylingOptions', itemIndex); // Extract security settings const sanitizeOutput = securitySettings.sanitizeOutput !== false; const callbackUrl = securitySettings.callbackUrl || ''; const sessionId = securitySettings.sessionId || `html_session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const corsOrigins = securitySettings.corsOrigins || '*'; const trustedContent = securitySettings.trustedContent === true; // Helper functions const escapeHtml = (text) => { return text .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#39;'); }; // Generate content based on type let htmlContent = ''; let contentMeta = {}; if (contentType === 'text') { const textContent = this.getNodeParameter('textContent', itemIndex); htmlContent = `<div class="text-content"><p>${escapeHtml(textContent)}</p></div>`; } else if (contentType === 'html') { htmlContent = this.getNodeParameter('htmlContent', itemIndex); } else if (contentType === 'form') { const formConfig = this.getNodeParameter('formConfig', itemIndex); const title = formConfig.title || 'Form'; const submitText = formConfig.submitText || 'Submit'; htmlContent = ` <div class="enhanced-form"> <h3>${escapeHtml(title)}</h3> <form data-callback-url="${callbackUrl}" data-session-id="${sessionId}" class="enhanced-form-element"> <div class="form-field"> <label for="sample_field">Sample Field</label> <input type="text" name="sample_field" id="sample_field" placeholder="Enter text..."> </div> <div class="form-actions"> <button type="submit" class="submit-btn">${escapeHtml(submitText)}</button> </div> </form> </div> `; contentMeta = { formId: `form_${sessionId}_${Date.now()}`, hasCallback: !!callbackUrl, }; } else if (contentType === 'video') { const videoConfig = this.getNodeParameter('videoConfig', itemIndex); const url = videoConfig.url || ''; const title = videoConfig.title || 'Video Player'; htmlContent = ` <div class="video-container"> <h4>${escapeHtml(title)}</h4> <video width="100%" controls preload="metadata"> <source src="${escapeHtml(url)}" type="video/mp4"> Your browser does not support the video tag. </video> </div> `; contentMeta = { mediaType: 'video' }; } else if (contentType === 'audio') { const audioConfig = this.getNodeParameter('audioConfig', itemIndex); const url = audioConfig.url || ''; const title = audioConfig.title || 'Audio Player'; htmlContent = ` <div class="audio-container"> <h4>${escapeHtml(title)}</h4> <audio controls preload="metadata"> <source src="${escapeHtml(url)}" type="audio/mp3"> Your browser does not support the audio tag. </audio> </div> `; contentMeta = { mediaType: 'audio' }; } else if (contentType === 'webrtc') { const webrtcConfig = this.getNodeParameter('webrtcConfig', itemIndex); const roomId = webrtcConfig.roomId || `room_${sessionId}`; htmlContent = ` <div class="webrtc-container" data-room-id="${roomId}"> <h4>WebRTC Video Call</h4> <div class="webrtc-controls"> <button class="webrtc-start">Start Call</button> <button class="webrtc-stop" style="display:none;">Stop</button> </div> <div class="webrtc-video-container"> <video id="localVideo" autoplay muted></video> <video id="remoteVideo" autoplay></video> </div> </div> `; contentMeta = { mediaType: 'webrtc', roomId: roomId }; } else { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unsupported content type: ${contentType}`); } // Basic HTML sanitization if enabled if (sanitizeOutput && !trustedContent) { htmlContent = htmlContent .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') .replace(/javascript:/gi, '') .replace(/on\w+\s*=/gi, ''); } // Apply basic styling const containerClasses = stylingOptions.containerClasses || 'enhanced-html-content'; const customCss = stylingOptions.customCss || ''; const defaultCSS = ` .enhanced-html-content { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; background: #fff; border-radius: 8px; padding: 16px; margin: 8px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .enhanced-form { max-width: 500px; } .form-field { margin-bottom: 16px; } .form-field label { display: block; margin-bottom: 4px; font-weight: 500; } .form-field input { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; } .submit-btn { background: #667eea; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; } .video-container, .audio-container, .webrtc-container { max-width: 100%; text-align: center; } `; htmlContent = `<style>${defaultCSS}${customCss}</style><div class="${containerClasses}">${htmlContent}</div>`; // Build output data const outputData = { type: 'html', contentType: contentType, html: htmlContent, meta: Object.assign({ callbackUrl: callbackUrl, sessionId: sessionId, timestamp: new Date().toISOString(), corsOrigins: corsOrigins, sanitized: sanitizeOutput && !trustedContent }, contentMeta), // Include original input data originalData: items[itemIndex].json, }; returnData.push({ json: outputData, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new n8n_workflow_1.NodeOperationError(this.getNode(), `HTML Renderer error: ${errorMessage}`); } } return [returnData]; }); } } exports.HTMLRenderer = HTMLRenderer;