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.

200 lines (199 loc) 10.4 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.Callback = void 0; class Callback { constructor() { this.description = { displayName: 'Enhanced Callback', name: 'enhancedCallback', group: ['trigger'], version: 1, description: 'A callback node for n8n to handle chat callbacks and render output', defaults: { name: 'Enhanced Callback', }, inputs: [], outputs: ['main'], webhooks: [ { name: 'default', httpMethod: 'POST', responseMode: 'onReceived', path: 'callback', }, ], properties: [ { displayName: 'Output Format', name: 'outputFormat', type: 'options', options: [ { name: 'Text', value: 'text' }, { name: 'HTML', value: 'html' }, ], default: 'text', description: 'Format for callback output', }, { displayName: 'Enable Dynamic HTML', name: 'enableDynamicHTML', type: 'boolean', default: false, description: 'Allow dynamic HTML rendering in chat UI', }, { displayName: 'Sanitize Response', name: 'sanitizeResponse', type: 'boolean', default: true, description: 'Sanitize callback response before rendering', }, { displayName: 'Enable Form Rendering', name: 'enableForm', type: 'boolean', default: false, description: 'Render a form for user input in the callback output', }, { displayName: 'Enable Audio Upload', name: 'enableAudioUpload', type: 'boolean', default: false, description: 'Show audio file upload in the callback output', }, { displayName: 'Enable Video Upload', name: 'enableVideoUpload', type: 'boolean', default: false, description: 'Show video file upload in the callback output', }, ], }; } webhook() { return __awaiter(this, void 0, void 0, function* () { const req = this.getRequestObject(); const res = this.getResponseObject(); const nodeParams = this.getNode().parameters; const outputFormat = nodeParams.outputFormat || 'text'; const enableDynamicHTML = nodeParams.enableDynamicHTML; const sanitizeResponse = nodeParams.sanitizeResponse !== false; let content = ''; let receivedHeaders = {}; if (req.body && typeof req.body === 'object') { content = req.body.message || req.body.content || ''; receivedHeaders = req.body.headers || {}; } // Collect X-Chat-* headers from HTTP headers const xChatHeaders = {}; for (const key in req.headers) { if (key.toLowerCase().startsWith('x-chat-')) { const value = req.headers[key]; xChatHeaders[key] = Array.isArray(value) ? value.join(', ') : value; } } // Simple HTML sanitizer (removes <script> and on* attributes) function sanitizeHtml(html) { return html .replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '') .replace(/on\w+="[^"]*"/gi, ''); } let renderedContent = ''; if (outputFormat === 'html' && enableDynamicHTML) { renderedContent = sanitizeResponse ? sanitizeHtml(content) : content; } else { renderedContent = `<pre style="white-space: pre-wrap;">${content.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</pre>`; } const enableForm = nodeParams.enableForm; const enableAudioUpload = nodeParams.enableAudioUpload; const enableVideoUpload = nodeParams.enableVideoUpload; let submittedValue = ''; let uploadedAudioName = ''; let uploadedVideoName = ''; let audioPreview = ''; let videoPreview = ''; if (req.method === 'POST') { if (req.body && typeof req.body === 'object') { if (req.body.formInput !== undefined) { submittedValue = req.body.formInput; } } // For UI-only demo, check for uploaded file names in form fields (simulate, as no file processing) if (req.body && typeof req.body === 'object') { if (req.body.audioFileName) { uploadedAudioName = req.body.audioFileName; audioPreview = `<audio controls src='' style='width:100%; margin-top:8px;'>Your browser does not support the audio element.</audio>`; } if (req.body.videoFileName) { uploadedVideoName = req.body.videoFileName; videoPreview = `<video controls src='' style='width:100%; margin-top:8px; max-height:200px;'>Your browser does not support the video tag.</video>`; } } } // Render headers for display function renderHeadersTable(headers, title) { let rows = ''; for (const k in headers) { const v = headers[k]; rows += `<tr><td>${k}</td><td>${Array.isArray(v) ? v.join(', ') : v}</td></tr>`; } return rows ? `<div style="margin-top:16px;"><strong>${title}</strong><table border=1 style="border-collapse:collapse; margin-top:4px; font-size:0.95em;"><tr><th>Header</th><th>Value</th></tr>${rows}</table></div>` : ''; } const html = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Callback Output</title> <style> body { font-family: sans-serif; background: #f8fafc; margin: 0; padding: 40px; } #output-container { max-width: 500px; margin: 0 auto; border: 1px solid #ccc; border-radius: 8px; background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 24px; } #output-header { font-weight: bold; color: #2563eb; margin-bottom: 16px; } #output-content { font-size: 1.1em; color: #222; } table { width: 100%; } th, td { padding: 4px 8px; } th { background: #f1f5f9; } #form-section { margin-top: 24px; } #submitted-value { margin-top: 12px; color: #059669; } </style> </head> <body> <div id="output-container"> <div id="output-header">Callback Output</div> <div id="output-content">${renderedContent}</div> ${renderHeadersTable(receivedHeaders, 'Headers from JSON body')} ${renderHeadersTable(xChatHeaders, 'X-Chat-* HTTP Headers')} ${enableForm || enableAudioUpload || enableVideoUpload ? `<div id="form-section"><form method="POST" enctype="multipart/form-data">${enableForm ? `<input type="text" name="formInput" placeholder="Type something..." required style="padding:8px; width:70%;"><input type="hidden" id="audioFileName" name="audioFileName">` : ''}${enableAudioUpload ? `<input type="file" name="audioFile" accept="audio/*" style="margin-left:8px;" onchange="if(this.files[0])document.getElementById('audioFileName').value=this.files[0].name;">` : ''}${enableVideoUpload ? `<input type="file" name="videoFile" accept="video/*" style="margin-left:8px;" onchange="if(this.files[0])document.getElementById('videoFileName').value=this.files[0].name;">` : ''}<button type="submit" style="padding:8px 16px; margin-left:8px; background:#2563eb; color:#fff; border:none; border-radius:4px;">Submit</button></form></div>` : ''} ${submittedValue ? `<div id="submitted-value"><strong>Submitted Value:</strong> ${submittedValue}</div>` : ''} ${uploadedAudioName ? `<div id="audio-uploaded"><strong>Audio Uploaded:</strong> ${uploadedAudioName}${audioPreview}</div>` : ''} ${uploadedVideoName ? `<div id="video-uploaded"><strong>Video Uploaded:</strong> ${uploadedVideoName}${videoPreview}</div>` : ''} </div> </body> </html>`; res.setHeader('Content-Type', 'text/html'); return { workflowData: [[{ json: { received: true, content, receivedHeaders, xChatHeaders, submittedValue, uploadedAudioName, uploadedVideoName } }]], webhookResponse: { body: html, headers: { 'Content-Type': 'text/html' }, statusCode: 200, }, }; }); } } exports.Callback = Callback; // Default export for backward compatibility with tests exports.default = Callback;