/** * Universal Workflows JavaScript Framework * Handles all agent interactions with direct N8N integration */ class WorkflowProcessor { constructor(agentSlug, webhookUrl, price) { this.agentSlug = agentSlug; this.webhookUrl = webhookUrl; this.price = price; this.sessionId = this.generateSessionId(); this.processing = false; } /** * Handle form submission - main entry point */ async handleFormSubmission(event) { event.preventDefault(); if (this.processing) { this.showToast('Please wait, processing your previous request...', 'warning'); return; } const form = event.target; const formData = new FormData(form); // Convert FormData to object const data = {}; for (let [key, value] of formData.entries()) { data[key] = value; } await this.processWorkflow(data, formData); } /** * Main workflow processing function */ async processWorkflow(data, formData = null) { try { this.processing = true; // 1. Validate form if (!this.validateForm(data)) { this.processing = false; return; } // 2. Check authentication and balance if (!await this.checkBalance()) { this.processing = false; return; } // 3. Show processing status this.showProcessing(); // 4. Call N8N directly const result = await this.callN8N(data, formData); if (result && result.output) { // 5. Deduct balance via Django API await this.deductBalance(); // 6. Display results this.displayResults(result); this.showToast('Processing completed successfully!', 'success'); } else { throw new Error('No output received from N8N'); } } catch (error) { console.error('Workflow processing error:', error); this.showError(`Processing failed: ${error.message}`); this.showToast('Processing failed. Please try again.', 'error'); } finally { this.processing = false; this.hideProcessing(); } } /** * Call N8N webhook directly */ async callN8N(data, formData = null) { const messageText = this.formatMessage(data); const payload = { sessionId: this.sessionId, message: { text: messageText }, agentSlug: this.agentSlug, timestamp: new Date().toISOString() }; // Handle file uploads if present if (formData && this.hasFileUploads(data)) { // For file uploads, we need to handle differently return await this.callN8NWithFiles(messageText, formData); } const response = await fetch(this.webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error(`N8N webhook failed: ${response.status} ${response.statusText}`); } return await response.json(); } /** * Handle N8N calls with file uploads */ async callN8NWithFiles(messageText, formData) { // Create multipart form data for file uploads const uploadData = new FormData(); uploadData.append('sessionId', this.sessionId); uploadData.append('message', JSON.stringify({ text: messageText })); uploadData.append('agentSlug', this.agentSlug); // Add files for (let [key, value] of formData.entries()) { if (value instanceof File) { uploadData.append(key, value); } } const response = await fetch(this.webhookUrl, { method: 'POST', body: uploadData }); if (!response.ok) { throw new Error(`N8N webhook with files failed: ${response.status} ${response.statusText}`); } return await response.json(); } /** * Deduct wallet balance via Django API */ async deductBalance() { const response = await fetch('/wallet/api/deduct/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.getCsrfToken() }, body: JSON.stringify({ amount: this.price, description: `${this.agentSlug} processing`, agent: this.agentSlug }) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Balance deduction failed'); } const result = await response.json(); this.updateWalletBalance(result.new_balance); return result; } /** * Validate form data */ validateForm(data) { const requiredFields = document.querySelectorAll('[required]'); let isValid = true; requiredFields.forEach(field => { const value = data[field.name]; if (!value || (typeof value === 'string' && value.trim() === '')) { this.showFieldError(field, 'This field is required'); isValid = false; } else { this.clearFieldError(field); } }); return isValid; } /** * Check user authentication and balance */ async checkBalance() { const isAuthenticated = document.body.getAttribute('data-user-authenticated') === 'true'; if (!isAuthenticated) { this.showToast('Please log in to use this agent', 'error'); setTimeout(() => { window.location.href = '/auth/login/'; }, 2000); return false; } // Get current balance from wallet card const balanceElement = document.querySelector('[data-wallet-balance]'); if (balanceElement) { const currentBalance = parseFloat(balanceElement.textContent.replace(/[^\d.]/g, '')); if (currentBalance < this.price) { this.showToast(`Insufficient balance. You need ${this.price} AED but have ${currentBalance} AED`, 'error'); return false; } } return true; } /** * Format message for N8N based on agent configuration */ formatMessage(data) { // Create a descriptive message based on the agent and data let message = `Process ${this.agentSlug} request:\n\n`; for (const [key, value] of Object.entries(data)) { if (value && key !== 'csrfmiddlewaretoken') { const fieldLabel = this.getFieldLabel(key) || key.replace(/[_-]/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); message += `${fieldLabel}: ${value}\n`; } } return message.trim(); } /** * Get field label from DOM */ getFieldLabel(fieldName) { const field = document.querySelector(`[name="${fieldName}"]`); if (field) { const label = document.querySelector(`label[for="${field.id}"]`); if (label) { return label.textContent.replace('*', '').trim(); } } return null; } /** * Check if form has file uploads */ hasFileUploads(data) { return Object.values(data).some(value => value instanceof File); } /** * Display processing status */ showProcessing() { const processingStatus = document.getElementById('processingStatus'); const resultsContainer = document.getElementById('resultsContainer'); const submitBtn = document.getElementById('submitBtn'); if (processingStatus) { processingStatus.style.display = 'block'; processingStatus.scrollIntoView({ behavior: 'smooth', block: 'start' }); } if (resultsContainer) { resultsContainer.style.display = 'none'; } if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Processing...'; } } /** * Hide processing status */ hideProcessing() { const processingStatus = document.getElementById('processingStatus'); const submitBtn = document.getElementById('submitBtn'); if (processingStatus) { processingStatus.style.display = 'none'; } if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = `🚀 Process with ${this.agentSlug.replace(/-/g, ' ')} (${this.price} AED)`; } } /** * Display results */ displayResults(result) { const resultsContainer = document.getElementById('resultsContainer'); const resultsContent = document.querySelector('.results-content'); if (!resultsContainer || !resultsContent) return; // Clear previous results resultsContent.innerHTML = ''; // Create result content const resultDiv = document.createElement('div'); resultDiv.className = 'workflow-result'; if (result.output) { // Create formatted output const outputDiv = document.createElement('div'); outputDiv.className = 'result-output'; // Handle different output formats if (typeof result.output === 'string') { outputDiv.innerHTML = this.formatTextOutput(result.output); } else if (typeof result.output === 'object') { outputDiv.innerHTML = this.formatObjectOutput(result.output); } else { outputDiv.textContent = String(result.output); } resultDiv.appendChild(outputDiv); } // Add action buttons const actionsDiv = document.createElement('div'); actionsDiv.className = 'result-actions'; actionsDiv.innerHTML = ` `; resultDiv.appendChild(actionsDiv); resultsContent.appendChild(resultDiv); // Show results container resultsContainer.style.display = 'block'; resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Store results for actions this.lastResult = result; } /** * Format text output with proper styling */ formatTextOutput(text) { // Convert newlines to HTML breaks and preserve formatting return text .replace(/\n\n/g, '
')
.replace(/\n/g, '
')
.replace(/^(.*)/, '
$1') .replace(/(.*?)$/, '$1
') .replace(/\*\*(.*?)\*\*/g, '$1') // Bold .replace(/\*(.*?)\*/g, '$1'); // Italic } /** * Format object output as structured data */ formatObjectOutput(obj) { if (obj.formatted_content) { return this.formatTextOutput(obj.formatted_content); } let html = '