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.

711 lines (710 loc) 32.7 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.ChatRenderer = void 0; const n8n_workflow_1 = require("n8n-workflow"); class ChatRenderer { constructor() { this.description = { displayName: 'Chat UI Renderer', name: 'chatRenderer', icon: 'fa:paint-brush', group: ['transform'], version: 1, subtitle: '={{$parameter["renderMode"] || "HTML Output"}}', description: 'Render chat conversations as HTML using official @n8n/chat components and styling', defaults: { name: 'Chat UI Renderer', }, inputs: ["main" /* NodeConnectionType.Main */], outputs: ["main" /* NodeConnectionType.Main */], credentials: [], properties: [ // === RENDER CONFIGURATION === { displayName: 'Render Mode', name: 'renderMode', type: 'options', options: [ { name: 'HTML Output', value: 'html', description: 'Generate complete HTML representation of the chat', }, { name: 'Component Parts', value: 'components', description: 'Generate individual message components', }, { name: 'Preview Mode', value: 'preview', description: 'Generate interactive preview with embedded styles', }, { name: 'Export Mode', value: 'export', description: 'Generate standalone HTML file for export', }, ], default: 'html', description: 'How to render the chat conversation', }, // === MESSAGE SOURCE === { displayName: 'Message Source', name: 'messageSource', type: 'options', options: [ { name: 'Input Data', value: 'input', description: 'Use messages from input data', }, { name: 'Manual Messages', value: 'manual', description: 'Manually define messages', }, { name: 'Enhanced Chat Format', value: 'enhanced', description: 'Use Enhanced Chat Trigger output format', }, ], default: 'input', description: 'Source of chat messages to render', }, // === MANUAL MESSAGES === { displayName: 'Chat Messages', name: 'chatMessages', type: 'fixedCollection', typeOptions: { multipleValues: true, }, default: { messages: [ { sender: 'bot', text: 'Hello! How can I help you today?', timestamp: new Date().toISOString() }, { sender: 'user', text: 'I need help with my account', timestamp: new Date().toISOString() } ] }, options: [ { name: 'messages', displayName: 'Message', values: [ { displayName: 'Sender', name: 'sender', type: 'options', options: [ { name: 'Bot', value: 'bot' }, { name: 'User', value: 'user' }, ], default: 'bot', description: 'Who sent this message', }, { displayName: 'Message Text', name: 'text', type: 'string', typeOptions: { rows: 3, }, default: '', description: 'The message content (supports Markdown)', }, { displayName: 'Timestamp', name: 'timestamp', type: 'string', default: '', placeholder: '2024-01-01T12:00:00.000Z', description: 'Message timestamp (ISO format)', }, { displayName: 'Message ID', name: 'id', type: 'string', default: '', description: 'Unique message identifier (auto-generated if empty)', }, ], }, ], displayOptions: { show: { messageSource: ['manual'], }, }, description: 'Manual chat messages to render', }, // === CHAT APPEARANCE === { displayName: 'Chat Configuration', name: 'chatConfig', type: 'collection', placeholder: 'Add Configuration', default: {}, options: [ { displayName: 'Chat Title', name: 'title', type: 'string', default: 'Chat Conversation', description: 'Title displayed at the top of the chat', }, { displayName: 'Chat Subtitle', name: 'subtitle', type: 'string', default: '', description: 'Subtitle shown below the title', }, { displayName: 'Chat Mode', name: 'mode', type: 'options', options: [ { name: 'Window Mode', value: 'window' }, { name: 'Fullscreen Mode', value: 'fullscreen' }, ], default: 'window', description: 'Chat display mode', }, { displayName: 'Show Timestamps', name: 'showTimestamps', type: 'boolean', default: true, description: 'Display message timestamps', }, { displayName: 'Enable Markdown', name: 'enableMarkdown', type: 'boolean', default: true, description: 'Render Markdown in messages', }, ], }, // === STYLING OPTIONS === { displayName: 'Styling Options', name: 'styling', type: 'collection', placeholder: 'Add Styling', default: {}, options: [ { 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', description: 'Chat theme to apply', }, { displayName: 'Primary Color', name: 'primaryColor', type: 'string', default: '#667eea', description: 'Primary color for the chat interface', }, { displayName: 'Secondary Color', name: 'secondaryColor', type: 'string', default: '#48bb78', description: 'Secondary color for chat elements', }, { displayName: 'Background Color', name: 'backgroundColor', type: 'string', default: '#ffffff', description: 'Background color of the chat', }, { displayName: 'Text Color', name: 'textColor', type: 'string', default: '#333333', description: 'Primary text color', }, { displayName: 'Chat Width', name: 'chatWidth', type: 'string', default: '400px', description: 'Width of the chat container', }, { displayName: 'Chat Height', name: 'chatHeight', type: 'string', default: '600px', description: 'Height of the chat container', }, ], }, // === OUTPUT OPTIONS === { displayName: 'Output Options', name: 'outputOptions', type: 'collection', placeholder: 'Add Output Option', default: {}, options: [ { displayName: 'Include CSS', name: 'includeCss', type: 'boolean', default: true, description: 'Include CSS styles in the output', }, { displayName: 'Include JavaScript', name: 'includeJs', type: 'boolean', default: false, description: 'Include interactive JavaScript functionality', }, { displayName: 'Compress Output', name: 'compress', type: 'boolean', default: false, description: 'Minify the HTML output', }, { displayName: 'Add Wrapper', name: 'addWrapper', type: 'boolean', default: true, description: 'Wrap output in a container div', }, { displayName: 'Custom CSS Classes', name: 'customClasses', type: 'string', default: '', description: 'Additional CSS classes to apply', }, ], }, ], }; this.methods = { loadOptions: { getThemeOptions() { return __awaiter(this, void 0, void 0, function* () { return [ { name: 'Default', value: 'default' }, { name: 'Dark', value: 'dark' }, { name: 'Light', value: 'light' }, { name: 'Custom', value: 'custom' }, ]; }); }, }, }; } 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 renderMode = this.getNodeParameter('renderMode', itemIndex); const messageSource = this.getNodeParameter('messageSource', itemIndex); const chatConfig = this.getNodeParameter('chatConfig', itemIndex); const styling = this.getNodeParameter('styling', itemIndex); const outputOptions = this.getNodeParameter('outputOptions', itemIndex); // Extract messages based on source let messages = []; if (messageSource === 'manual') { const manualMessages = this.getNodeParameter('chatMessages', itemIndex); messages = manualMessages.messages || []; } else if (messageSource === 'enhanced') { // Handle Enhanced Chat Trigger format const itemData = items[itemIndex].json; if (itemData.enhanced && itemData.content) { messages = [{ id: itemData.id || 'msg_1', sender: 'user', text: itemData.content, timestamp: itemData.timestamp, }]; } } else { // Default: try to extract from input data const itemData = items[itemIndex].json; // Try common message array field names for (const field of ['messages', 'chatMessages', 'conversation', 'data']) { if (Array.isArray(itemData[field])) { messages = itemData[field]; break; } } // If single message object if (messages.length === 0 && (itemData.text || itemData.content)) { messages = [itemData]; } } // Validate messages if (!Array.isArray(messages) || messages.length === 0) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No valid messages found to render'); } // Normalize messages to standard format messages = messages.map((msg, index) => { const normalizedMessage = { id: msg.id || msg.messageId || `msg_${index + 1}`, sender: msg.sender || (msg.from === 'user' ? 'user' : 'bot'), text: msg.text || msg.content || msg.message || '', timestamp: msg.timestamp || new Date().toISOString(), files: msg.files || [], }; // Ensure sender is valid if (!['bot', 'user'].includes(normalizedMessage.sender)) { normalizedMessage.sender = 'bot'; } return normalizedMessage; }); // Generate CSS styles const theme = styling.theme || 'default'; // CSS Variables const cssVariables = []; if (styling.primaryColor) cssVariables.push(`--chat--color-primary: ${styling.primaryColor}`); if (styling.secondaryColor) cssVariables.push(`--chat--color-secondary: ${styling.secondaryColor}`); if (styling.backgroundColor) cssVariables.push(`--chat--color-white: ${styling.backgroundColor}`); if (styling.textColor) cssVariables.push(`--chat--color-dark: ${styling.textColor}`); // Add defaults if none provided if (cssVariables.length === 0) { cssVariables.push('--chat--color-primary: #667eea', '--chat--color-secondary: #48bb78', '--chat--color-white: #ffffff', '--chat--color-dark: #333333', '--chat--color-light: #f2f4f8', '--chat--color-medium: #d2d4d9'); } else { // Add missing defaults if (!styling.primaryColor) cssVariables.push('--chat--color-primary: #667eea'); if (!styling.secondaryColor) cssVariables.push('--chat--color-secondary: #48bb78'); if (!styling.backgroundColor) cssVariables.push('--chat--color-white: #ffffff'); if (!styling.textColor) cssVariables.push('--chat--color-dark: #333333'); cssVariables.push('--chat--color-light: #f2f4f8', '--chat--color-medium: #d2d4d9'); } // Apply theme-specific styles if (theme === 'dark') { cssVariables.push('--chat--color-white: #1a1a1a', '--chat--color-dark: #ffffff', '--chat--color-light: #2a2a2a', '--chat--color-medium: #404040'); } // Base CSS let css = ` :root { ${cssVariables.join('; ')} } .chat-renderer { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; line-height: 1.5; color: var(--chat--color-dark); background: var(--chat--color-white); border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden; ${styling.chatWidth ? `width: ${styling.chatWidth};` : ''} ${styling.chatHeight ? `height: ${styling.chatHeight};` : ''} } .chat-header { padding: 16px; background: var(--chat--color-dark); color: var(--chat--color-white); border-bottom: 1px solid var(--chat--color-medium); } .chat-title { margin: 0; font-size: 18px; font-weight: 600; } .chat-subtitle { margin: 4px 0 0 0; font-size: 14px; opacity: 0.8; } .chat-messages { padding: 16px; max-height: 500px; overflow-y: auto; } .chat-message { margin-bottom: 12px; max-width: 80%; padding: 12px 16px; border-radius: 18px; word-wrap: break-word; } .chat-message.from-bot { background: var(--chat--color-light); color: var(--chat--color-dark); margin-right: auto; border-bottom-left-radius: 4px; } .chat-message.from-user { background: var(--chat--color-primary); color: var(--chat--color-white); margin-left: auto; border-bottom-right-radius: 4px; } .message-timestamp { font-size: 11px; opacity: 0.6; margin-top: 4px; } .message-text { margin: 0; } .message-text pre { background: rgba(0,0,0,0.1); padding: 8px; border-radius: 4px; overflow-x: auto; } .message-text code { background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 3px; } `; // Helper functions const escapeHtml = (text) => { return text.replace(/[&<>"']/g, (match) => { const escapeChars = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }; return escapeChars[match]; }); }; const renderBasicMarkdown = (text) => { if (chatConfig.enableMarkdown === false) return text; return text .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') .replace(/\*(.*?)\*/g, '<em>$1</em>') .replace(/`(.*?)`/g, '<code>$1</code>') .replace(/```([\s\S]*?)```/g, '<pre>$1</pre>') .replace(/\n/g, '<br>'); }; const formatTimestamp = (timestamp) => { try { const date = new Date(timestamp); return date.toLocaleTimeString(); } catch (_a) { return timestamp; } }; // Render messages HTML const renderMessagesHTML = () => { return messages.map(message => { const showTimestamps = chatConfig.showTimestamps !== false; let messageText = escapeHtml(message.text); messageText = renderBasicMarkdown(messageText); const timestamp = showTimestamps && message.timestamp ? `<div class="message-timestamp">${formatTimestamp(message.timestamp)}</div>` : ''; return ` <div class="chat-message from-${message.sender}" data-message-id="${message.id}"> <div class="message-text">${messageText}</div> ${timestamp} </div> `; }).join(''); }; // Generate output based on render mode let renderedOutput; if (renderMode === 'html') { const includeCss = outputOptions.includeCss !== false; const addWrapper = outputOptions.addWrapper !== false; const customClasses = outputOptions.customClasses || ''; let html = ''; if (addWrapper) { html += `<div class="chat-renderer ${customClasses}">`; } // Add header if title is provided if (chatConfig.title) { html += ` <div class="chat-header"> <h1 class="chat-title">${escapeHtml(chatConfig.title)}</h1> ${chatConfig.subtitle ? `<p class="chat-subtitle">${escapeHtml(chatConfig.subtitle)}</p>` : ''} </div> `; } // Render messages html += '<div class="chat-messages">'; html += renderMessagesHTML(); html += '</div>'; if (addWrapper) { html += '</div>'; } renderedOutput = { html: html, css: includeCss ? css : '', messageCount: messages.length, }; } else if (renderMode === 'components') { const components = messages.map((message, index) => ({ id: message.id, sender: message.sender, html: `<div class="chat-message from-${message.sender}" data-message-id="${message.id}"> <div class="message-text">${renderBasicMarkdown(escapeHtml(message.text))}</div> </div>`, data: message, index: index, })); renderedOutput = { components: components, css: css, messageCount: messages.length, }; } else if (renderMode === 'preview') { const htmlResult = { html: '', css: css, messageCount: messages.length, }; // Build HTML for preview let html = `<div class="chat-renderer">`; if (chatConfig.title) { html += ` <div class="chat-header"> <h1 class="chat-title">${escapeHtml(chatConfig.title)}</h1> ${chatConfig.subtitle ? `<p class="chat-subtitle">${escapeHtml(chatConfig.subtitle)}</p>` : ''} </div> `; } html += '<div class="chat-messages">'; html += renderMessagesHTML(); html += '</div>'; html += '</div>'; const previewHtml = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Chat Preview - ${chatConfig.title || 'Chat Conversation'}</title> <style>${css}</style> </head> <body style="margin: 20px; background: #f5f5f5;"> ${html} </body> </html> `; renderedOutput = { preview: previewHtml, html: html, css: css, messageCount: messages.length, }; } else if (renderMode === 'export') { // Same as preview but with export data let html = `<div class="chat-renderer">`; if (chatConfig.title) { html += ` <div class="chat-header"> <h1 class="chat-title">${escapeHtml(chatConfig.title)}</h1> ${chatConfig.subtitle ? `<p class="chat-subtitle">${escapeHtml(chatConfig.subtitle)}</p>` : ''} </div> `; } html += '<div class="chat-messages">'; html += renderMessagesHTML(); html += '</div>'; html += '</div>'; const previewHtml = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Chat Export - ${chatConfig.title || 'Chat Conversation'}</title> <style>${css}</style> </head> <body style="margin: 20px; background: #f5f5f5;"> ${html} </body> </html> `; const exportData = { filename: `chat-export-${new Date().toISOString().split('T')[0]}.html`, content: previewHtml, mimeType: 'text/html', }; renderedOutput = { export: exportData, preview: previewHtml, html: html, css: css, messageCount: messages.length, }; } else { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unsupported render mode: ${renderMode}`); } // Add metadata const outputData = Object.assign(Object.assign({}, renderedOutput), { metadata: { renderMode: renderMode, messageCount: messages.length, generatedAt: new Date().toISOString(), chatConfig: chatConfig, styling: styling, } }); returnData.push({ json: outputData, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Chat Renderer error: ${errorMessage}`); } } return [returnData]; }); } } exports.ChatRenderer = ChatRenderer;