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.
596 lines (595 loc) • 28.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.ChatTrigger = void 0;
const n8n_workflow_1 = require("n8n-workflow");
class ChatTrigger {
constructor() {
this.description = {
displayName: 'Enhanced Chat Trigger',
name: 'enhancedChatTrigger',
icon: 'fa:comments',
group: ['trigger'],
version: 1,
subtitle: '={{$parameter["chatTitle"] || "Enhanced Chat"}}',
description: 'Enhanced Chat Trigger with advanced features, better UI, and multimedia support',
defaults: {
name: 'Enhanced Chat Trigger',
},
inputs: [],
outputs: ["main" /* NodeConnectionType.Main */],
credentials: [],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'enhanced-chat',
},
],
triggerPanel: {
header: 'Enhanced Chat Interface',
executionsHelp: {
inactive: 'Chat will trigger executions when messages are received. Make your workflow active to start receiving chat messages.',
active: 'Chat is active and ready to receive messages. Use the chat interface below to interact.',
},
activationHint: 'Once activated, you can interact with your workflow through the chat interface below.',
},
properties: [
// === CHAT CONFIGURATION ===
{
displayName: 'Chat Title',
name: 'chatTitle',
type: 'string',
default: 'Enhanced AI Assistant',
description: 'Title displayed in the chat interface',
},
{
displayName: 'Chat Subtitle',
name: 'chatSubtitle',
type: 'string',
default: 'How can I help you today?',
description: 'Subtitle shown in the chat welcome message',
},
{
displayName: 'Welcome Messages',
name: 'welcomeMessages',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {
messages: [
{ text: 'Hi there! 👋' },
{ text: 'I\'m your Enhanced AI Assistant. How can I help you today?' }
]
},
options: [
{
name: 'messages',
displayName: 'Message',
values: [
{
displayName: 'Message Text',
name: 'text',
type: 'string',
default: '',
description: 'Welcome message to display when chat starts',
},
],
},
],
description: 'Welcome messages to display when the chat interface loads',
},
// === CHAT BEHAVIOR ===
{
displayName: 'Chat Mode',
name: 'chatMode',
type: 'options',
options: [
{
name: 'Window Mode',
value: 'window',
description: 'Chat toggle button with hide/show functionality',
},
{
name: 'Fullscreen Mode',
value: 'fullscreen',
description: 'Chat takes up entire available space',
},
],
default: 'window',
description: 'How the chat interface should be displayed',
},
{
displayName: 'Enable File Uploads',
name: 'enableFileUploads',
type: 'boolean',
default: true,
description: 'Allow users to upload files through the chat interface',
},
{
displayName: 'Allowed File Types',
name: 'allowedFileTypes',
type: 'string',
default: 'image/*,application/pdf,.txt,.doc,.docx,.json',
description: 'Comma-separated MIME types for allowed file uploads',
displayOptions: {
show: {
enableFileUploads: [true],
},
},
},
{
displayName: 'Load Previous Session',
name: 'loadPreviousSession',
type: 'boolean',
default: true,
description: 'Whether to load previous chat messages from the session',
},
// === RESPONSE CONFIGURATION ===
{
displayName: 'Response Mode',
name: 'responseMode',
type: 'options',
options: [
{
name: 'Text Response',
value: 'text',
description: 'Send plain text responses',
},
{
name: 'JSON Response',
value: 'json',
description: 'Send structured JSON responses',
},
{
name: 'HTML Response',
value: 'html',
description: 'Send HTML formatted responses',
},
{
name: 'Workflow Output',
value: 'workflow',
description: 'Use workflow output as response',
},
],
default: 'workflow',
description: 'How to format responses sent back to the chat',
},
{
displayName: 'Default Response',
name: 'defaultResponse',
type: 'string',
typeOptions: {
rows: 3,
},
default: 'Thank you for your message! I\'m processing your request...',
description: 'Default response when workflow doesn\'t provide output',
displayOptions: {
show: {
responseMode: ['text', 'json', 'html'],
},
},
},
// === ENHANCED FEATURES ===
{
displayName: 'Enhanced Features',
name: 'enhancedFeatures',
type: 'collection',
placeholder: 'Add Feature',
default: {},
options: [
{
displayName: 'Enable Session Management',
name: 'sessionManagement',
type: 'boolean',
default: true,
description: 'Maintain conversation context across messages',
},
{
displayName: 'Enable Message History',
name: 'messageHistory',
type: 'boolean',
default: true,
description: 'Store and retrieve message history',
},
{
displayName: 'Auto-save Conversations',
name: 'autoSaveConversations',
type: 'boolean',
default: false,
description: 'Automatically save conversations to workflow memory',
},
{
displayName: 'Enable Typing Indicators',
name: 'typingIndicators',
type: 'boolean',
default: true,
description: 'Show typing indicators while processing',
},
{
displayName: 'Enable Message Timestamps',
name: 'messageTimestamps',
type: 'boolean',
default: true,
description: 'Display timestamps on messages',
},
{
displayName: 'Custom CSS Classes',
name: 'customCssClasses',
type: 'string',
default: '',
description: 'Custom CSS classes to apply to the chat interface',
},
],
},
// === STYLING OPTIONS ===
{
displayName: 'Styling Options',
name: 'stylingOptions',
type: 'collection',
placeholder: 'Add Styling',
default: {},
options: [
{
displayName: 'Primary Color',
name: 'primaryColor',
type: 'string',
default: '#667eea',
description: 'Primary color for the chat interface (hex code)',
},
{
displayName: 'Secondary Color',
name: 'secondaryColor',
type: 'string',
default: '#48bb78',
description: 'Secondary color for chat elements (hex code)',
},
{
displayName: 'Background Color',
name: 'backgroundColor',
type: 'string',
default: '#ffffff',
description: 'Background color of the chat interface',
},
{
displayName: 'Text Color',
name: 'textColor',
type: 'string',
default: '#333333',
description: 'Primary text color',
},
{
displayName: 'Chat Width',
name: 'chatWidth',
type: 'number',
default: 400,
description: 'Width of chat window in pixels (window mode)',
},
{
displayName: 'Chat Height',
name: 'chatHeight',
type: 'number',
default: 600,
description: 'Height of chat window in pixels (window mode)',
},
],
},
// === WEBHOOK CONFIGURATION ===
{
displayName: 'Advanced Options',
name: 'advancedOptions',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Custom Webhook Path',
name: 'customWebhookPath',
type: 'string',
default: '',
description: 'Custom path for webhook URL (leave empty for auto-generated)',
},
{
displayName: 'CORS Origins',
name: 'corsOrigins',
type: 'string',
default: '*',
description: 'Allowed origins for CORS (comma-separated domains or * for all)',
},
{
displayName: 'Rate Limiting',
name: 'rateLimiting',
type: 'options',
options: [
{ name: 'None', value: 'none' },
{ name: 'Basic (10/min)', value: 'basic' },
{ name: 'Moderate (5/min)', value: 'moderate' },
{ name: 'Strict (2/min)', value: 'strict' },
],
default: 'basic',
description: 'Rate limiting for chat messages',
},
{
displayName: 'Message Timeout (seconds)',
name: 'messageTimeout',
type: 'number',
default: 30,
description: 'Timeout for processing messages',
},
],
},
// === CHAT INTERFACE PREVIEW ===
{
displayName: 'Chat Interface',
name: 'chatInterface',
type: 'notice',
typeOptions: {
theme: 'info',
},
default: '',
description: 'Live chat interface will appear here when the workflow is activated. The chat will be fully functional and ready to receive messages.',
},
],
};
this.methods = {
loadOptions: {
getChatModes() {
return __awaiter(this, void 0, void 0, function* () {
return [
{
name: 'Window Mode',
value: 'window',
description: 'Chat toggle button with hide/show functionality',
},
{
name: 'Fullscreen Mode',
value: 'fullscreen',
description: 'Chat takes up entire container space',
},
];
});
},
},
};
}
trigger() {
return __awaiter(this, void 0, void 0, function* () {
const webhookName = 'default';
const webhookUrl = this.getInstanceBaseUrl() + '/webhook/' + this.getNode().id;
// Get node parameters with proper defaults
const chatTitle = this.getNodeParameter('chatTitle', '') || 'Enhanced Chat Assistant';
const chatSubtitle = this.getNodeParameter('chatSubtitle', '') || 'How can I help you today?';
const chatMode = this.getNodeParameter('chatMode', 'window');
const enableFileUploads = this.getNodeParameter('enableFileUploads', true);
const allowedFileTypes = this.getNodeParameter('allowedFileTypes', 'image/*,application/pdf,.txt,.doc,.docx,.json');
const loadPreviousSession = this.getNodeParameter('loadPreviousSession', true);
const enhancedFeatures = this.getNodeParameter('enhancedFeatures', {});
const stylingOptions = this.getNodeParameter('stylingOptions', {});
// Create default welcome messages
const initialMessages = [
'Hi there! 👋',
`I'm ${chatTitle}. ${chatSubtitle}`
];
// Build chat configuration
const chatConfig = {
webhookUrl: webhookUrl,
mode: chatMode,
target: '#enhanced-chat-widget',
showWelcomeScreen: true,
loadPreviousSession: loadPreviousSession,
allowFileUploads: enableFileUploads,
allowedFilesMimeTypes: enableFileUploads ? allowedFileTypes : '',
initialMessages: initialMessages,
chatInputKey: 'chatInput',
chatSessionKey: 'sessionId',
defaultLanguage: 'en',
i18n: {
en: {
title: chatTitle,
subtitle: chatSubtitle,
inputPlaceholder: 'Type your message...',
getStarted: 'Start Conversation',
},
},
metadata: {
enhanced: true,
nodeId: this.getNode().id,
workflowId: this.getWorkflow().id,
features: enhancedFeatures,
},
};
// Apply custom styling if provided
if (stylingOptions && Object.keys(stylingOptions).length > 0) {
const customStyles = stylingOptions;
const cssVariables = [];
if (customStyles.primaryColor)
cssVariables.push(`--chat--color-primary: ${customStyles.primaryColor}`);
if (customStyles.secondaryColor)
cssVariables.push(`--chat--color-secondary: ${customStyles.secondaryColor}`);
if (customStyles.backgroundColor)
cssVariables.push(`--chat--color-white: ${customStyles.backgroundColor}`);
if (customStyles.textColor)
cssVariables.push(`--chat--color-dark: ${customStyles.textColor}`);
if (customStyles.chatWidth)
cssVariables.push(`--chat--window--width: ${customStyles.chatWidth}px`);
if (customStyles.chatHeight)
cssVariables.push(`--chat--window--height: ${customStyles.chatHeight}px`);
if (cssVariables.length > 0) {
chatConfig.customStyles = cssVariables.join('; ');
}
}
// Store configuration for webhook handler
ChatTrigger.chatSessions.set(this.getNode().id, {
config: chatConfig,
nodeParameters: {
responseMode: this.getNodeParameter('responseMode', 'workflow'),
defaultResponse: this.getNodeParameter('defaultResponse', 'Thank you for your message! I\'m processing your request...'),
enhancedFeatures: enhancedFeatures,
}
});
// Set up webhook handler for incoming chat messages
const webhookHandler = (context) => __awaiter(this, void 0, void 0, function* () {
return ChatTrigger.handleIncomingMessage(context);
});
return {
manualTriggerFunction: () => __awaiter(this, void 0, void 0, function* () {
// Manual trigger just activates the webhook
// The actual trigger data comes through the webhook
}),
};
});
}
// Static method to handle incoming chat webhook requests
static handleIncomingMessage(context) {
return __awaiter(this, void 0, void 0, function* () {
try {
const body = context.getBodyData();
const headers = context.getHeaderData();
const query = context.getQueryData();
const nodeId = context.getNode().id;
// Get stored configuration
let sessionData = ChatTrigger.chatSessions.get(nodeId);
if (!sessionData) {
// Gracefully handle missing session by creating default configuration
// This can happen after n8n restart or workflow reactivation
const defaultConfig = {
nodeParameters: {
responseMode: 'workflow',
defaultResponse: 'Thank you for your message! I\'m processing your request...',
enhancedFeatures: {
sessionManagement: true,
messageHistory: true,
autoSaveConversations: false,
typingIndicators: true,
messageTimestamps: true,
},
}
};
// Store the default configuration
ChatTrigger.chatSessions.set(nodeId, defaultConfig);
sessionData = defaultConfig;
// Log this recovery for debugging (optional - will only show in n8n logs)
console.log(`Enhanced Chat Trigger: Recovered session for node ${nodeId} after restart/reactivation`);
}
const { nodeParameters } = sessionData;
// Extract message data
const chatInput = body.chatInput || body.message || '';
const sessionId = body.sessionId || body.session || `enhanced_session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const action = query.action || body.action || 'sendMessage';
const files = body.files || [];
// Build enhanced message object
const enhancedMessage = {
id: `enhanced_msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
content: chatInput,
timestamp: new Date().toISOString(),
sessionId: sessionId,
action: action,
files: files,
enhanced: true,
metadata: {
userAgent: headers['user-agent'],
ip: headers['x-forwarded-for'] || headers['x-real-ip'] || 'unknown',
source: 'enhanced-chat-trigger',
features: nodeParameters.enhancedFeatures,
},
};
// Handle session management
if (nodeParameters.enhancedFeatures.sessionManagement && action === 'loadPreviousSession') {
const previousMessages = ChatTrigger.messageHistory.get(sessionId) || [];
return {
webhookResponse: {
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
body: {
messages: previousMessages,
sessionId: sessionId,
enhanced: true,
},
},
workflowData: [[]],
};
}
// Store message in history if enabled
if (nodeParameters.enhancedFeatures.messageHistory) {
if (!ChatTrigger.messageHistory.has(sessionId)) {
ChatTrigger.messageHistory.set(sessionId, []);
}
ChatTrigger.messageHistory.get(sessionId).push(enhancedMessage);
}
// Prepare response based on mode
let responseBody = {
enhanced: true,
sessionId: sessionId,
messageId: enhancedMessage.id,
timestamp: enhancedMessage.timestamp,
};
const responseMode = nodeParameters.responseMode;
const defaultResponse = nodeParameters.defaultResponse;
if (responseMode === 'text') {
responseBody.response = { text: defaultResponse };
}
else if (responseMode === 'json') {
responseBody.response = {
text: defaultResponse,
type: 'json',
data: enhancedMessage
};
}
else if (responseMode === 'html') {
responseBody.response = {
text: `<div style="padding: 10px; background: #f0f8ff; border-radius: 5px;">${defaultResponse}</div>`,
type: 'html'
};
}
return {
webhookResponse: {
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
body: responseBody,
},
workflowData: [
[
{
json: enhancedMessage,
},
],
],
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Enhanced Chat Trigger error: ${errorMessage}`);
}
});
}
// Handle webhook requests (main webhook entry point)
webhook() {
return __awaiter(this, void 0, void 0, function* () {
return ChatTrigger.handleIncomingMessage(this);
});
}
}
exports.ChatTrigger = ChatTrigger;
// Store active chat sessions
ChatTrigger.chatSessions = new Map();
ChatTrigger.messageHistory = new Map();