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
JavaScript
;
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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
// 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;