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
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.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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
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;