diff --git a/netcop_hub/urls.py b/netcop_hub/urls.py index b1b8a0d..e593803 100644 --- a/netcop_hub/urls.py +++ b/netcop_hub/urls.py @@ -25,16 +25,8 @@ urlpatterns = [ path('wallet/', include('wallet.urls')), path('', include('agent_base.urls')), - # New unified workflows (will replace individual agent apps) - path('workflows/', include('workflows.urls')), - - # Legacy individual agent apps (will be deprecated) - path('agents/weather-reporter/', include('weather_reporter.urls')), - path('agents/data-analyzer/', include('data_analyzer.urls')), - path('agents/job-posting-generator/', include('job_posting_generator.urls')), - path('agents/social-ads-generator/', include('social_ads_generator.urls')), - path('agents/email-writer/', include('email_writer.urls')), - path('agents/five-whys-analyzer/', include('five_whys_analyzer.urls')), + # Unified workflows system for all agents + path('agents/', include('workflows.urls')), path('', include('core.urls')), ] diff --git a/static/js/data-analyzer.js b/static/js/data-analyzer.js index e51b271..5af6839 100644 --- a/static/js/data-analyzer.js +++ b/static/js/data-analyzer.js @@ -1,40 +1,322 @@ /** - * Data Analyzer Agent - Specific JavaScript - * Uses WorkflowsCore for all shared functionality + * Data Analyzer - Agent-Specific JavaScript + * Handles unique functionality for Data Analyzer agent + * Uses WorkflowsCore architecture like other agents */ -// Initialize data analyzer functionality -document.addEventListener('DOMContentLoaded', function() { - console.log('Data Analyzer loaded'); - - // Initialize file upload - const fileInput = document.getElementById('dataFile'); - if (fileInput) { - fileInput.addEventListener('change', handleFileChange); +class DataAnalyzerProcessor extends WorkflowsCore { + constructor() { + super(); + this.agentSlug = 'data-analyzer'; + this.webhookUrl = 'http://localhost:5678/webhook/simple-pdf-processor'; + this.price = 8.0; // Will be overridden by template data + this.sessionId = this.constructor.generateSessionId(); + + // Initialize on page load + this.initialize(); } - // Initialize drag and drop - const uploadArea = document.querySelector('.file-upload-area'); - if (uploadArea && fileInput) { - WorkflowsCore.setupDragAndDrop(uploadArea, fileInput); + initialize() { + // Set data attributes from page + const priceElement = document.body.getAttribute('data-agent-price'); + if (priceElement) { + this.price = parseFloat(priceElement); + } + + // Initialize form submission + const form = document.getElementById('agentForm'); + if (form) { + form.addEventListener('submit', this.handleFormSubmission.bind(this)); + } + + // Initialize file upload functionality + this.initializeFileUpload(); + + // Initialize form validation + this.initializeFormValidation(); + + // Set initial radio selection + const firstRadio = document.querySelector('.radio-card'); + if (firstRadio && !document.querySelector('.radio-card.selected')) { + firstRadio.classList.add('selected'); + const input = firstRadio.querySelector('input[type="radio"]'); + if (input) input.checked = true; + } } - // Set initial radio selection - const firstRadio = document.querySelector('.radio-card'); - if (firstRadio && !document.querySelector('.radio-card.selected')) { - firstRadio.classList.add('selected'); - const input = firstRadio.querySelector('input[type="radio"]'); - if (input) input.checked = true; + /** + * Initialize file upload functionality + */ + initializeFileUpload() { + const fileInput = document.getElementById('dataFile'); + if (fileInput) { + fileInput.addEventListener('change', this.handleFileChange.bind(this)); + } + + // Initialize drag and drop + const uploadArea = document.querySelector('.file-upload-area'); + if (uploadArea && fileInput) { + this.constructor.setupDragAndDrop(uploadArea, fileInput); + } } - // Handle form submission - const form = document.getElementById('agentForm'); - if (form) { - form.addEventListener('submit', handleFormSubmission); + /** + * Handle form submission with hybrid N8N/Django approach + */ + async handleFormSubmission(e) { + e.preventDefault(); + + if (!this.isFormValid()) { + this.constructor.showToast('Please upload a file and select analysis type', 'error'); + return; + } + + // Check authentication and balance + if (!this.constructor.checkAuthentication()) return; + if (!this.constructor.checkBalance(this.price)) return; + + // Show processing status and disable submit button + this.constructor.showProcessing('Analyzing your data file...'); + + const submitBtn = document.getElementById('generateBtn'); + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.textContent = '⏳ Analyzing...'; + } + + try { + // Try direct N8N integration for better performance (with Django fallback) + const useDirectN8N = false; // Feature flag - disabled for file uploads (complex) + + if (useDirectN8N) { + await this.processViaDirectN8N(e.target); + } else { + // For file uploads, use immediate Django processing (N8N direct upload is complex) + await this.processViaDjangoImmediate(e.target); + } + } catch (error) { + console.error('Form submission error:', error); + this.constructor.hideProcessing(); + this.constructor.showToast('❌ Connection error. Please try again.', 'error'); + this.resetSubmitButton(); + } } -}); + + /** + * Django processing for file uploads (immediate response for data analyzer) + */ + async processViaDjangoImmediate(form) { + const formData = new FormData(form); + + const response = await fetch(window.location.href, { + method: 'POST', + body: formData, + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }); + + const result = await response.json(); + + if (result.success && result.analysis_results) { + // Data analyzer returns results immediately, no polling needed + this.constructor.hideProcessing(); + + if (result.wallet_balance !== undefined) { + this.constructor.updateWalletBalance(result.wallet_balance); + } + + // Display results immediately + const analysisData = result.analysis_results; + const formattedHtml = this.formatAnalysisResults(analysisData); + WorkflowsCore.showResults(formattedHtml, 'Analysis Results'); + this.constructor.showToast('✅ Data analysis completed successfully!', 'success'); + + this.resetSubmitButton(); + } else { + this.constructor.hideProcessing(); + this.constructor.showToast(`❌ ${result.error || 'Processing failed'}`, 'error'); + this.resetSubmitButton(); + } + } + + /** + * Form validation specific to Data Analyzer + */ + initializeFormValidation() { + const fileInput = document.getElementById('dataFile'); + const analysisTypeInputs = document.querySelectorAll('input[name="analysisType"]'); + + if (fileInput) { + fileInput.addEventListener('change', () => this.validateField('dataFile')); + } + + analysisTypeInputs.forEach(input => { + input.addEventListener('change', () => this.validateField('analysisType')); + }); + } + + validateField(fieldName) { + switch (fieldName) { + case 'dataFile': + const fileInput = document.getElementById('dataFile'); + if (!fileInput.files || fileInput.files.length === 0) { + this.constructor.showFieldError('dataFile', 'Please select a data file'); + return false; + } + + const file = fileInput.files[0]; + const maxSize = 10 * 1024 * 1024; // 10MB + if (file.size > maxSize) { + this.constructor.showFieldError('dataFile', 'File too large. Maximum size is 10MB'); + return false; + } + + const allowedExtensions = ['.pdf']; + const fileExtension = '.' + file.name.split('.').pop().toLowerCase(); + if (!allowedExtensions.includes(fileExtension)) { + this.constructor.showFieldError('dataFile', 'Unsupported file type. Please use PDF files only'); + return false; + } + break; + + case 'analysisType': + const analysisType = document.querySelector('input[name="analysisType"]:checked'); + if (!analysisType) { + this.constructor.showFieldError('analysisType', 'Please select an analysis type'); + return false; + } + break; + } + + this.constructor.clearFieldError(fieldName); + return true; + } + + isFormValid() { + const fileValid = this.validateField('dataFile'); + const analysisValid = this.validateField('analysisType'); + + return fileValid && analysisValid; + } + + /** + * Handle file change events + */ + handleFileChange(event) { + const file = event.target.files[0]; + const fileNameDisplay = document.getElementById('fileName'); + const fileSizeDisplay = document.getElementById('fileSize'); + const uploadArea = document.querySelector('.file-upload-area'); + + if (file) { + if (fileNameDisplay) { + fileNameDisplay.textContent = `✅ ${file.name}`; + fileNameDisplay.style.display = 'block'; + } + if (fileSizeDisplay) { + fileSizeDisplay.textContent = this.formatFileSize(file.size); + fileSizeDisplay.style.display = 'block'; + } + + // Add visual feedback + if (uploadArea) { + uploadArea.classList.add('file-selected'); + } + + // Clear any previous errors + this.constructor.clearFieldError('dataFile'); + + // Validate file immediately + this.validateField('dataFile'); + } else { + // Reset display if no file + if (fileNameDisplay) { + fileNameDisplay.textContent = ''; + fileNameDisplay.style.display = 'none'; + } + if (fileSizeDisplay) { + fileSizeDisplay.textContent = ''; + fileSizeDisplay.style.display = 'none'; + } + if (uploadArea) uploadArea.classList.remove('file-selected'); + } + } + + /** + * Format file size for display + */ + formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } + + /** + * Format analysis results for HTML display + */ + formatAnalysisResults(analysisData) { + let resultsHtml = '
Analysis completed: ${new Date(analysisData.timestamp).toLocaleString()}
`; + } else { + resultsHtml += `Analysis completed: ${new Date().toLocaleString()}
`; + } + + return resultsHtml; + } + + /** + * Escape HTML to prevent XSS + */ + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + /** + * Reset submit button to original state + */ + resetSubmitButton() { + const submitBtn = document.getElementById('generateBtn'); + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.textContent = `🚀 Analyze Data (${this.price} AED)`; + } + } +} -// Data Analyzer specific functions +// Data Analyzer specific functions (global for template onclick handlers) function selectRadio(value) { // Remove selected class from all cards document.querySelectorAll('.radio-card').forEach(card => { @@ -54,158 +336,69 @@ function selectRadio(value) { } } -function handleFileChange(event) { - const file = event.target.files[0]; - const uploadArea = document.querySelector('.file-upload-area'); - const uploadText = document.querySelector('.upload-text'); - - if (file) { - uploadArea.classList.add('file-selected'); - uploadText.innerHTML = ` -Analysis completed successfully. Your data has been processed.
'; +function resetForm() { + const form = document.getElementById('agentForm'); + if (form) { + form.reset(); } - if (analysisData.timestamp) { - resultsHtml += `Analysis completed: ${new Date(analysisData.timestamp).toLocaleString()}
`; + const resultsContainer = document.getElementById('resultsContainer'); + const processingStatus = document.getElementById('processingStatus'); + + if (resultsContainer) resultsContainer.style.display = 'none'; + if (processingStatus) processingStatus.style.display = 'none'; + + // Clear file display + const fileNameDisplay = document.getElementById('fileName'); + const fileSizeDisplay = document.getElementById('fileSize'); + if (fileNameDisplay) { + fileNameDisplay.textContent = ''; + fileNameDisplay.style.display = 'none'; + } + if (fileSizeDisplay) { + fileSizeDisplay.textContent = ''; + fileSizeDisplay.style.display = 'none'; } - WorkflowsCore.showResults(resultsHtml, 'Analysis Results'); - WorkflowsCore.showToast('✅ Data analysis completed successfully!', 'success'); -} - -function isFormValid() { - const fileInput = document.getElementById('dataFile'); - const analysisType = document.querySelector('input[name="analysisType"]:checked'); - - // Clear previous errors + // Clear validation errors WorkflowsCore.clearFieldError('dataFile'); WorkflowsCore.clearFieldError('analysisType'); - let isValid = true; - - if (!fileInput.files || fileInput.files.length === 0) { - WorkflowsCore.showFieldError('dataFile', 'Please select a data file'); - WorkflowsCore.showToast('Please select a data file', 'error'); - isValid = false; + // Reset radio selection + const firstRadio = document.querySelector('.radio-card'); + if (firstRadio) { + document.querySelectorAll('.radio-card').forEach(card => card.classList.remove('selected')); + firstRadio.classList.add('selected'); + const input = firstRadio.querySelector('input[type="radio"]'); + if (input) input.checked = true; } - if (!analysisType) { - WorkflowsCore.showFieldError('analysisType', 'Please select an analysis type'); - WorkflowsCore.showToast('Please select an analysis type', 'error'); - isValid = false; + // Scroll back to form + const formSection = document.getElementById('agentForm'); + if (formSection) { + formSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); } - - return isValid; -} \ No newline at end of file +} + +// Initialize Data Analyzer Processor when DOM is ready +document.addEventListener('DOMContentLoaded', function() { + // Initialize processor (data attributes set by template) + window.dataAnalyzerProcessor = new DataAnalyzerProcessor(); +}); \ No newline at end of file diff --git a/static/js/job-posting-generator.js b/static/js/job-posting-generator.js new file mode 100644 index 0000000..a2e2f53 --- /dev/null +++ b/static/js/job-posting-generator.js @@ -0,0 +1,428 @@ +/** + * Job Posting Generator - Agent-Specific JavaScript + * Handles unique functionality for Job Posting Generator agent + * Uses WorkflowsCore architecture like other agents + */ + +class JobPostingGeneratorProcessor extends WorkflowsCore { + constructor() { + super(); + this.agentSlug = 'job-posting-generator'; + this.webhookUrl = 'http://localhost:5678/webhook/43f84411-eaaa-488c-9b1f-856e90d0aaf6'; + this.price = 4.0; // Will be overridden by template data + this.sessionId = this.constructor.generateSessionId(); + + // Initialize on page load + this.initialize(); + } + + initialize() { + // Set data attributes from page + const priceElement = document.body.getAttribute('data-agent-price'); + if (priceElement) { + this.price = parseFloat(priceElement); + } + + // Initialize form submission + const form = document.getElementById('agentForm'); + if (form) { + form.addEventListener('submit', this.handleFormSubmission.bind(this)); + } + + // Initialize form validation + this.initializeFormValidation(); + } + + /** + * Handle form submission with hybrid N8N/Django approach + */ + async handleFormSubmission(e) { + e.preventDefault(); + + if (!this.isFormValid()) { + this.constructor.showToast('Please fill in all required fields correctly', 'error'); + return; + } + + // Check authentication and balance + if (!this.constructor.checkAuthentication()) return; + if (!this.constructor.checkBalance(this.price)) return; + + // Show processing status and disable submit button + this.constructor.showProcessing('Creating your professional job posting...'); + + const submitBtn = document.getElementById('generateBtn'); + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.textContent = '⏳ Generating...'; + } + + try { + // Direct N8N integration + await this.processViaDirectN8N(e.target); + } catch (error) { + console.error('Form submission error:', error); + this.constructor.hideProcessing(); + this.constructor.showToast('❌ Connection error. Please try again.', 'error'); + this.resetSubmitButton(); + } + } + + /** + * Direct N8N processing for better performance + */ + async processViaDirectN8N(form) { + try { + const formData = new FormData(form); + + // Extract form data + const jobTitle = formData.get('job_title').trim(); + const companyName = formData.get('company_name').trim(); + const jobDescription = formData.get('job_description').trim(); + const seniorityLevel = formData.get('seniority_level'); + const contractType = formData.get('contract_type'); + const location = formData.get('location').trim(); + const language = formData.get('language') || 'English'; + + // Create message for N8N + const messageText = `Create a professional job posting for: ${jobTitle} at ${companyName}. Description: ${jobDescription}. Seniority: ${seniorityLevel}. Contract: ${contractType}. Location: ${location}. Language: ${language}. Make it comprehensive and attractive to candidates.`; + + const webhookData = { + sessionId: this.sessionId, + message: { text: messageText } + }; + + // Direct N8N webhook call + const response = await fetch(this.webhookUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(webhookData), + signal: AbortSignal.timeout(60000) // 60 second timeout + }); + + if (!response.ok) { + throw new Error(`N8N error: ${response.status}`); + } + + const contentType = response.headers.get('content-type'); + let data; + if (contentType && contentType.includes('application/json')) { + data = await response.json().catch(() => response.text()); + } else { + data = await response.text(); + } + + // Process successful N8N response + this.constructor.hideProcessing(); + + // Deduct wallet balance via Django API + await this.constructor.deductBalance( + this.price, + `Job Posting Generator - ${jobTitle} at ${companyName}`, + this.agentSlug + ); + + // Display results using the enhanced display function + this.displayDirectN8NResults(data, jobTitle, companyName); + + this.constructor.showToast('✅ Job posting generated successfully!', 'success'); + + } catch (error) { + console.error('N8N processing error:', error); + this.constructor.hideProcessing(); + this.constructor.showToast('❌ Processing failed. Please try again.', 'error'); + this.resetSubmitButton(); + } + } + + + /** + * Form validation specific to Job Posting Generator + */ + initializeFormValidation() { + const requiredFields = ['job_title', 'company_name', 'job_description', 'seniority_level', 'contract_type', 'location']; + + requiredFields.forEach(fieldName => { + const field = document.getElementById(fieldName); + if (field) { + field.addEventListener('blur', () => this.validateField(fieldName)); + field.addEventListener('input', () => this.validateField(fieldName)); + } + }); + } + + validateField(fieldName) { + const field = document.getElementById(fieldName); + if (!field) return true; + + const value = field.value.trim(); + + switch (fieldName) { + case 'job_title': + if (!value) { + this.constructor.showFieldError(fieldName, 'Job title is required'); + return false; + } + if (value.length < 3) { + this.constructor.showFieldError(fieldName, 'Job title should be at least 3 characters'); + return false; + } + break; + + case 'company_name': + if (!value) { + this.constructor.showFieldError(fieldName, 'Company name is required'); + return false; + } + if (value.length < 2) { + this.constructor.showFieldError(fieldName, 'Company name should be at least 2 characters'); + return false; + } + break; + + case 'job_description': + if (!value) { + this.constructor.showFieldError(fieldName, 'Job description is required'); + return false; + } + break; + + case 'seniority_level': + case 'contract_type': + if (!value) { + const fieldLabel = fieldName.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()); + this.constructor.showFieldError(fieldName, `${fieldLabel} is required`); + return false; + } + break; + + case 'location': + if (!value) { + this.constructor.showFieldError(fieldName, 'Location is required'); + return false; + } + if (value.length < 3) { + this.constructor.showFieldError(fieldName, 'Location should be at least 3 characters'); + return false; + } + break; + } + + this.constructor.clearFieldError(fieldName); + return true; + } + + isFormValid() { + const requiredFields = ['job_title', 'company_name', 'job_description', 'seniority_level', 'contract_type', 'location']; + + let isValid = true; + requiredFields.forEach(fieldName => { + if (!this.validateField(fieldName)) { + isValid = false; + } + }); + + return isValid; + } + + + /** + * Display results from direct N8N call + */ + displayDirectN8NResults(data, jobTitle, companyName) { + const resultsContainer = document.getElementById('resultsContainer'); + const resultsContent = document.getElementById('resultsContent'); + + if (!resultsContainer || !resultsContent) return; + + let content = ''; + + // Handle different N8N response formats + if (typeof data === 'string') { + content = data; + } else if (data && typeof data === 'object') { + content = data.output || data.text || data.content || data.job_posting || data.result || data.message || JSON.stringify(data, null, 2); + } else { + content = 'Job posting generated successfully!'; + } + + // Clear and populate results securely + resultsContent.textContent = ''; + this.renderSecureJobContent(resultsContent, content); + + // Show results container + resultsContainer.style.display = 'block'; + resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' }); + + this.resetSubmitButton(); + } + + /** + * Secure content rendering for job postings without innerHTML to prevent XSS + */ + renderSecureJobContent(container, content) { + // Sanitize and validate content + if (!content || typeof content !== 'string') { + container.textContent = 'No content available'; + return; + } + + // Create wrapper div + const wrapper = document.createElement('div'); + wrapper.className = 'job-posting-content'; + + // Split content into lines and process safely + const lines = content.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + if (!line) { + // Add line break for empty lines + if (i > 0) wrapper.appendChild(document.createElement('br')); + continue; + } + + let element; + + // Handle headers (but escape content) + if (line.startsWith('### ')) { + element = document.createElement('h3'); + element.className = 'job-section-title'; + element.textContent = line.substring(4); + } else if (line.startsWith('## ')) { + element = document.createElement('h2'); + element.className = 'job-section-title'; + element.textContent = line.substring(3); + } else if (line.startsWith('# ')) { + element = document.createElement('h1'); + element.className = 'job-section-title'; + element.textContent = line.substring(2); + } else if (line.startsWith('- ')) { + // Handle list items + element = document.createElement('li'); + element.textContent = line.substring(2); + } else { + // Handle regular text with basic formatting + element = document.createElement('p'); + element.className = 'job-paragraph'; + this.formatJobTextSecurely(element, line); + } + + wrapper.appendChild(element); + } + + container.appendChild(wrapper); + } + + /** + * Format job posting text with basic styling while preventing XSS + */ + formatJobTextSecurely(element, text) { + // Simple approach: handle bold and italic formatting securely + const parts = []; + let currentText = text; + + // Process **bold** text + currentText = currentText.replace(/\*\*(.*?)\*\*/g, (match, content) => { + const placeholder = `__BOLD_${parts.length}__`; + parts.push({type: 'bold', content: content}); + return placeholder; + }); + + // Process *italic* text + currentText = currentText.replace(/\*(.*?)\*/g, (match, content) => { + const placeholder = `__ITALIC_${parts.length}__`; + parts.push({type: 'italic', content: content}); + return placeholder; + }); + + // Split by placeholders and create DOM elements + const segments = currentText.split(/(__(?:BOLD|ITALIC)_\d+__)/); + + segments.forEach(segment => { + if (segment.startsWith('__BOLD_')) { + const index = parseInt(segment.match(/\d+/)[0]); + const strong = document.createElement('strong'); + strong.textContent = parts[index].content; + element.appendChild(strong); + } else if (segment.startsWith('__ITALIC_')) { + const index = parseInt(segment.match(/\d+/)[0]); + const em = document.createElement('em'); + em.textContent = parts[index].content; + element.appendChild(em); + } else if (segment) { + element.appendChild(document.createTextNode(segment)); + } + }); + } + + + /** + * Escape HTML to prevent XSS + */ + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + /** + * Reset submit button to original state + */ + resetSubmitButton() { + const submitBtn = document.getElementById('generateBtn'); + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.textContent = `💼 Generate Job Posting (${this.price} AED)`; + } + } +} + +// Result action functions (global for button onclick handlers) +function copyResults() { + const content = document.getElementById('resultsContent'); + if (content) { + const text = content.textContent || ''; + WorkflowsCore.copyToClipboard(text, 'Job posting copied to clipboard!'); + } +} + +function downloadResults() { + const content = document.getElementById('resultsContent'); + if (content) { + const text = content.textContent || ''; + const jobTitle = document.getElementById('job_title')?.value || 'job-posting'; + const filename = `${jobTitle.toLowerCase().replace(/\s+/g, '-')}-${Date.now()}.txt`; + WorkflowsCore.downloadAsFile(text, filename, 'Job posting downloaded!'); + } +} + +function resetForm() { + const form = document.getElementById('agentForm'); + if (form) { + form.reset(); + } + + const resultsContainer = document.getElementById('resultsContainer'); + const processingStatus = document.getElementById('processingStatus'); + + if (resultsContainer) resultsContainer.style.display = 'none'; + if (processingStatus) processingStatus.style.display = 'none'; + + // Clear validation errors + const fields = ['job_title', 'company_name', 'job_description', 'seniority_level', 'contract_type', 'location']; + fields.forEach(field => WorkflowsCore.clearFieldError(field)); + + // Scroll back to form + const formSection = document.getElementById('agentForm'); + if (formSection) { + formSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } +} + +// Initialize Job Posting Generator Processor when DOM is ready +document.addEventListener('DOMContentLoaded', function() { + // Initialize processor (data attributes set by template) + window.jobPostingGeneratorProcessor = new JobPostingGeneratorProcessor(); +}); \ No newline at end of file diff --git a/static/js/social-ads.js b/static/js/social-ads.js index 26cd1e9..3de3477 100644 --- a/static/js/social-ads.js +++ b/static/js/social-ads.js @@ -65,14 +65,8 @@ class SocialAdsProcessor extends WorkflowsCore { } try { - // Try direct N8N integration for better performance (with Django fallback) - const useDirectN8N = true; // Feature flag for direct integration - - if (useDirectN8N) { - await this.processViaDirectN8N(e.target); - } else { - await this.processViaDjango(e.target); - } + // Direct N8N integration + await this.processViaDirectN8N(e.target); } catch (error) { console.error('Form submission error:', error); this.constructor.hideProcessing(); @@ -138,41 +132,14 @@ class SocialAdsProcessor extends WorkflowsCore { this.constructor.showToast('✅ Social ads generated successfully!', 'success'); } catch (error) { - console.error('Direct N8N error:', error); - this.constructor.showToast('❌ Direct processing failed, trying Django backend...', 'info'); - - // Fallback to Django processing - await this.processViaDjango(form); - } - } - - /** - * Django processing fallback - */ - async processViaDjango(form) { - const formData = new FormData(form); - - const response = await fetch(window.location.href, { - method: 'POST', - body: formData, - headers: { 'X-Requested-With': 'XMLHttpRequest' } - }); - - const result = await response.json(); - - if (result.success && result.request_id) { - // Start polling for results - this.checkResults(result.request_id); - if (result.wallet_balance !== undefined) { - this.constructor.updateWalletBalance(result.wallet_balance); - } - } else { + console.error('N8N processing error:', error); this.constructor.hideProcessing(); - this.constructor.showToast(`❌ ${result.error || 'Processing failed'}`, 'error'); + this.constructor.showToast('❌ Processing failed. Please try again.', 'error'); this.resetSubmitButton(); } } + /** * Display results from direct N8N call */ @@ -361,79 +328,7 @@ class SocialAdsProcessor extends WorkflowsCore { return isValid; } - /** - * Check results (polling for Django completion) - */ - checkResults(requestId) { - let pollCount = 0; - const maxPolls = 30; // 5 minutes max - - const pollInterval = setInterval(() => { - pollCount++; - - fetch(`/workflows/api/status/${requestId}/`) - .then(response => response.json()) - .then(result => { - if (result.status === 'completed') { - clearInterval(pollInterval); - this.displayDjangoResults(result); - } else if (result.status === 'failed') { - clearInterval(pollInterval); - this.constructor.hideProcessing(); - this.constructor.showToast('❌ Social ads generation failed. Please try again.', 'error'); - this.resetSubmitButton(); - } else if (pollCount >= maxPolls) { - clearInterval(pollInterval); - this.constructor.hideProcessing(); - this.constructor.showToast('⏰ Processing is taking longer than expected. Please check back later.', 'error'); - this.resetSubmitButton(); - } - // Continue polling if still processing - }) - .catch(error => { - console.error('Status check error:', error); - if (pollCount >= maxPolls) { - clearInterval(pollInterval); - this.constructor.hideProcessing(); - this.constructor.showToast('❌ Connection error during processing.', 'error'); - this.resetSubmitButton(); - } - }); - }, 10000); // Check every 10 seconds - } - /** - * Display results from Django processing - */ - displayDjangoResults(result) { - const resultsContainer = document.getElementById('resultsContainer'); - const resultsContent = document.getElementById('resultsContent'); - - if (result.success || result.output) { - this.constructor.hideProcessing(); - - const adContent = result.output || result.ad_copy_content || result.content || 'Social ads generated successfully!'; - if (resultsContent) { - resultsContent.textContent = ''; - this.renderSecureContent(resultsContent, adContent); - } - - if (resultsContainer) { - resultsContainer.style.display = 'block'; - resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - - this.constructor.showToast('✅ Social ads completed successfully!', 'success'); - } else if (result.error) { - this.constructor.hideProcessing(); - this.constructor.showToast(`❌ Error: ${result.error}`, 'error'); - } else { - this.constructor.hideProcessing(); - this.constructor.showToast('❌ Failed to generate social ads. Please try again.', 'error'); - } - - this.resetSubmitButton(); - } /** * Reset submit button to original state diff --git a/workflows/config/agents.py b/workflows/config/agents.py index 0279ce5..b306c7b 100644 --- a/workflows/config/agents.py +++ b/workflows/config/agents.py @@ -8,7 +8,7 @@ AGENT_CONFIGS = { 'name': 'Social Ads Generator', 'description': 'Create engaging social media advertisements with AI-powered content generation', 'category': 'marketing', - 'price': 5.0, + 'price': 6.0, 'icon': '📱', 'webhook_url': 'http://localhost:5678/webhook/2dc234d8-7217-454a-83e9-81afe5b4fe2d', }, @@ -17,9 +17,9 @@ AGENT_CONFIGS = { 'name': 'Job Posting Generator', 'description': 'Create professional job postings that attract top talent', 'category': 'content', - 'price': 4.0, + 'price': 10.0, 'icon': '💼', - 'webhook_url': 'http://localhost:5678/webhook/job-posting-webhook-id', + 'webhook_url': 'http://localhost:5678/webhook/43f84411-eaaa-488c-9b1f-856e90d0aaf6', }, 'five-whys-analyzer': { diff --git a/workflows/templates/workflows/components/quick_agents_panel.html b/workflows/templates/workflows/components/quick_agents_panel.html index 869c681..3a040a9 100644 --- a/workflows/templates/workflows/components/quick_agents_panel.html +++ b/workflows/templates/workflows/components/quick_agents_panel.html @@ -9,7 +9,7 @@