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