mirror of
https://github.com/thecyberlearn/quantum-ai-v3.git
synced 2026-08-18 20:12:57 +00:00
✅ All agents working: Direct N8N integration, fixed routing, and pricing sync
- Fixed URL conflicts between legacy and unified workflows system
- Data analyzer: Fixed button ID mismatch and added file display elements
- Job posting generator: Connected to real N8N webhook, added prefilled test data
- Social ads generator: Direct N8N integration, added prefilled test data
- Quick agents panel: Fixed URLs to use /agents/ prefix
- Pricing: Synced config with database (data-analyzer: 8 AED, job-posting: 10 AED, social-ads: 6 AED)
- Removed Django fallbacks for cleaner direct N8N processing
- All agents accessible via /agents/{slug}/ with consistent architecture
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
adab6e4cef
commit
73d514152c
@ -25,16 +25,8 @@ urlpatterns = [
|
|||||||
path('wallet/', include('wallet.urls')),
|
path('wallet/', include('wallet.urls')),
|
||||||
path('', include('agent_base.urls')),
|
path('', include('agent_base.urls')),
|
||||||
|
|
||||||
# New unified workflows (will replace individual agent apps)
|
# Unified workflows system for all agents
|
||||||
path('workflows/', include('workflows.urls')),
|
path('agents/', 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')),
|
|
||||||
|
|
||||||
path('', include('core.urls')),
|
path('', include('core.urls')),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,40 +1,322 @@
|
|||||||
/**
|
/**
|
||||||
* Data Analyzer Agent - Specific JavaScript
|
* Data Analyzer - Agent-Specific JavaScript
|
||||||
* Uses WorkflowsCore for all shared functionality
|
* Handles unique functionality for Data Analyzer agent
|
||||||
|
* Uses WorkflowsCore architecture like other agents
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Initialize data analyzer functionality
|
class DataAnalyzerProcessor extends WorkflowsCore {
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
constructor() {
|
||||||
console.log('Data Analyzer loaded');
|
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 file upload
|
// Initialize on page load
|
||||||
const fileInput = document.getElementById('dataFile');
|
this.initialize();
|
||||||
if (fileInput) {
|
|
||||||
fileInput.addEventListener('change', handleFileChange);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize drag and drop
|
initialize() {
|
||||||
const uploadArea = document.querySelector('.file-upload-area');
|
// Set data attributes from page
|
||||||
if (uploadArea && fileInput) {
|
const priceElement = document.body.getAttribute('data-agent-price');
|
||||||
WorkflowsCore.setupDragAndDrop(uploadArea, fileInput);
|
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');
|
* Initialize file upload functionality
|
||||||
if (firstRadio && !document.querySelector('.radio-card.selected')) {
|
*/
|
||||||
firstRadio.classList.add('selected');
|
initializeFileUpload() {
|
||||||
const input = firstRadio.querySelector('input[type="radio"]');
|
const fileInput = document.getElementById('dataFile');
|
||||||
if (input) input.checked = true;
|
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');
|
* Handle form submission with hybrid N8N/Django approach
|
||||||
if (form) {
|
*/
|
||||||
form.addEventListener('submit', handleFormSubmission);
|
async handleFormSubmission(e) {
|
||||||
}
|
e.preventDefault();
|
||||||
});
|
|
||||||
|
|
||||||
// Data Analyzer specific functions
|
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 = '<h3>✅ Analysis Complete</h3>';
|
||||||
|
|
||||||
|
// Check if we have structured sections data
|
||||||
|
if (analysisData && typeof analysisData === 'object' && analysisData.sections && Array.isArray(analysisData.sections) && analysisData.sections.length > 0) {
|
||||||
|
resultsHtml += '<div class="analysis-sections">';
|
||||||
|
|
||||||
|
analysisData.sections.forEach(section => {
|
||||||
|
if (section.heading && section.content) {
|
||||||
|
resultsHtml += `
|
||||||
|
<div style="background: var(--surface-variant); border-radius: var(--radius-md); padding: var(--spacing-lg); margin-bottom: var(--spacing-md); border-left: 4px solid var(--primary);">
|
||||||
|
<h4 style="color: var(--primary); font-weight: 600; margin: 0 0 var(--spacing-md) 0; font-size: 16px;">📋 ${this.escapeHtml(section.heading)}</h4>
|
||||||
|
<div style="color: var(--on-surface); line-height: 1.6; font-size: 14px;">${this.escapeHtml(section.content).replace(/\n/g, '<br>')}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resultsHtml += '</div>';
|
||||||
|
} else {
|
||||||
|
// Handle simple text response or fallback
|
||||||
|
const content = typeof analysisData === 'string' ? analysisData : JSON.stringify(analysisData, null, 2);
|
||||||
|
resultsHtml += `
|
||||||
|
<div style="background: var(--surface-variant); border-radius: var(--radius-md); padding: var(--spacing-lg); margin-bottom: var(--spacing-md); border-left: 4px solid var(--primary);">
|
||||||
|
<h4 style="color: var(--primary); font-weight: 600; margin: 0 0 var(--spacing-md) 0; font-size: 16px;">📊 Analysis Results</h4>
|
||||||
|
<div style="color: var(--on-surface); line-height: 1.6; font-size: 14px; white-space: pre-wrap;">${this.escapeHtml(content)}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timestamp
|
||||||
|
if (analysisData && analysisData.timestamp) {
|
||||||
|
resultsHtml += `<p style="margin-top: var(--spacing-lg); text-align: center; color: var(--on-surface-variant);"><small>Analysis completed: ${new Date(analysisData.timestamp).toLocaleString()}</small></p>`;
|
||||||
|
} else {
|
||||||
|
resultsHtml += `<p style="margin-top: var(--spacing-lg); text-align: center; color: var(--on-surface-variant);"><small>Analysis completed: ${new Date().toLocaleString()}</small></p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (global for template onclick handlers)
|
||||||
function selectRadio(value) {
|
function selectRadio(value) {
|
||||||
// Remove selected class from all cards
|
// Remove selected class from all cards
|
||||||
document.querySelectorAll('.radio-card').forEach(card => {
|
document.querySelectorAll('.radio-card').forEach(card => {
|
||||||
@ -54,158 +336,69 @@ function selectRadio(value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileChange(event) {
|
// Result action functions (global for button onclick handlers)
|
||||||
const file = event.target.files[0];
|
function copyResults() {
|
||||||
const uploadArea = document.querySelector('.file-upload-area');
|
const content = document.getElementById('resultsContent');
|
||||||
const uploadText = document.querySelector('.upload-text');
|
if (content) {
|
||||||
|
const text = content.textContent || '';
|
||||||
if (file) {
|
WorkflowsCore.copyToClipboard(text, 'Analysis results copied to clipboard!');
|
||||||
uploadArea.classList.add('file-selected');
|
|
||||||
uploadText.innerHTML = `
|
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
|
||||||
<span style="font-size: 24px;">📄</span>
|
|
||||||
<div>
|
|
||||||
<div style="font-weight: 500;">${file.name}</div>
|
|
||||||
<div style="font-size: 12px; color: var(--on-surface-variant);">${WorkflowsCore.formatFileSize(file.size)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
WorkflowsCore.showToast(`File selected: ${file.name}`, 'success');
|
|
||||||
} else {
|
|
||||||
uploadArea.classList.remove('file-selected');
|
|
||||||
uploadText.innerHTML = `
|
|
||||||
<div class="upload-icon">📁</div>
|
|
||||||
<div><strong>Click to upload</strong> or drag and drop</div>
|
|
||||||
<div>PDF files only</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFormSubmission(e) {
|
function downloadResults() {
|
||||||
e.preventDefault();
|
const content = document.getElementById('resultsContent');
|
||||||
|
if (content) {
|
||||||
// Validate form
|
const text = content.textContent || '';
|
||||||
if (!isFormValid()) {
|
WorkflowsCore.downloadAsFile(text, 'data-analysis-results.txt', 'Analysis results downloaded!');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check authentication and balance using WorkflowsCore
|
|
||||||
if (!WorkflowsCore.checkAuthentication()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentPrice = parseFloat(document.body.getAttribute('data-agent-price'));
|
|
||||||
if (!WorkflowsCore.checkBalance(agentPrice)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show processing status
|
|
||||||
WorkflowsCore.showProcessing('Analyzing Your Data...');
|
|
||||||
|
|
||||||
// Submit form with AJAX
|
|
||||||
const formData = new FormData(e.target);
|
|
||||||
|
|
||||||
fetch(window.location.href, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
headers: {
|
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (response.headers.get('content-type')?.includes('application/json')) {
|
|
||||||
return response.json();
|
|
||||||
} else {
|
|
||||||
return response.text().then(html => {
|
|
||||||
console.log('HTML response received');
|
|
||||||
return { success: true, processing: true };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(result => {
|
|
||||||
if (result.success && result.analysis_results) {
|
|
||||||
// Handle JSON response with analysis data
|
|
||||||
displayAnalysisResults(result.analysis_results);
|
|
||||||
|
|
||||||
// Update wallet balance if provided
|
|
||||||
if (result.wallet_balance !== undefined) {
|
|
||||||
WorkflowsCore.updateWalletBalance(result.wallet_balance);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (result.success && result.processing) {
|
|
||||||
// Show processing message
|
|
||||||
WorkflowsCore.showToast('🔄 Processing started successfully!', 'success');
|
|
||||||
|
|
||||||
// Show unavailable message after timeout (since N8N integration may not be active)
|
|
||||||
setTimeout(() => {
|
|
||||||
WorkflowsCore.hideProcessing();
|
|
||||||
WorkflowsCore.showToast('⚠️ Analysis service temporarily unavailable. Please try again later.', 'error');
|
|
||||||
}, 30000); // 30 second timeout
|
|
||||||
|
|
||||||
} else {
|
|
||||||
WorkflowsCore.hideProcessing();
|
|
||||||
WorkflowsCore.showToast(`❌ ${result.error || 'Processing failed'}`, 'error');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Form submission error:', error);
|
|
||||||
WorkflowsCore.hideProcessing();
|
|
||||||
WorkflowsCore.showToast('❌ Connection error. Please try again.', 'error');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayAnalysisResults(analysisData) {
|
function resetForm() {
|
||||||
WorkflowsCore.hideProcessing();
|
const form = document.getElementById('agentForm');
|
||||||
|
if (form) {
|
||||||
let resultsHtml = '<h3>✅ Analysis Complete</h3>';
|
form.reset();
|
||||||
|
|
||||||
if (analysisData.sections && analysisData.sections.length > 0) {
|
|
||||||
resultsHtml += '<div class="analysis-sections">';
|
|
||||||
|
|
||||||
analysisData.sections.forEach(section => {
|
|
||||||
if (section.heading && section.content) {
|
|
||||||
resultsHtml += `
|
|
||||||
<div style="background: var(--surface-variant); border-radius: var(--radius-md); padding: var(--spacing-lg); margin-bottom: var(--spacing-md); border-left: 4px solid var(--primary);">
|
|
||||||
<h4 style="color: var(--primary); font-weight: 600; margin: 0 0 var(--spacing-md) 0; font-size: 16px;">📋 ${section.heading}</h4>
|
|
||||||
<div style="color: var(--on-surface); line-height: 1.6; font-size: 14px;">${section.content.replace(/\n/g, '<br>')}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resultsHtml += '</div>';
|
|
||||||
} else {
|
|
||||||
resultsHtml += '<p>Analysis completed successfully. Your data has been processed.</p>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (analysisData.timestamp) {
|
const resultsContainer = document.getElementById('resultsContainer');
|
||||||
resultsHtml += `<p style="margin-top: var(--spacing-lg); text-align: center; color: var(--on-surface-variant);"><small>Analysis completed: ${new Date(analysisData.timestamp).toLocaleString()}</small></p>`;
|
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');
|
// Clear validation errors
|
||||||
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
|
|
||||||
WorkflowsCore.clearFieldError('dataFile');
|
WorkflowsCore.clearFieldError('dataFile');
|
||||||
WorkflowsCore.clearFieldError('analysisType');
|
WorkflowsCore.clearFieldError('analysisType');
|
||||||
|
|
||||||
let isValid = true;
|
// Reset radio selection
|
||||||
|
const firstRadio = document.querySelector('.radio-card');
|
||||||
if (!fileInput.files || fileInput.files.length === 0) {
|
if (firstRadio) {
|
||||||
WorkflowsCore.showFieldError('dataFile', 'Please select a data file');
|
document.querySelectorAll('.radio-card').forEach(card => card.classList.remove('selected'));
|
||||||
WorkflowsCore.showToast('Please select a data file', 'error');
|
firstRadio.classList.add('selected');
|
||||||
isValid = false;
|
const input = firstRadio.querySelector('input[type="radio"]');
|
||||||
|
if (input) input.checked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!analysisType) {
|
// Scroll back to form
|
||||||
WorkflowsCore.showFieldError('analysisType', 'Please select an analysis type');
|
const formSection = document.getElementById('agentForm');
|
||||||
WorkflowsCore.showToast('Please select an analysis type', 'error');
|
if (formSection) {
|
||||||
isValid = false;
|
formSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
}
|
}
|
||||||
|
|
||||||
return isValid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize Data Analyzer Processor when DOM is ready
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize processor (data attributes set by template)
|
||||||
|
window.dataAnalyzerProcessor = new DataAnalyzerProcessor();
|
||||||
|
});
|
||||||
428
static/js/job-posting-generator.js
Normal file
428
static/js/job-posting-generator.js
Normal file
@ -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();
|
||||||
|
});
|
||||||
@ -65,14 +65,8 @@ class SocialAdsProcessor extends WorkflowsCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try direct N8N integration for better performance (with Django fallback)
|
// Direct N8N integration
|
||||||
const useDirectN8N = true; // Feature flag for direct integration
|
await this.processViaDirectN8N(e.target);
|
||||||
|
|
||||||
if (useDirectN8N) {
|
|
||||||
await this.processViaDirectN8N(e.target);
|
|
||||||
} else {
|
|
||||||
await this.processViaDjango(e.target);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Form submission error:', error);
|
console.error('Form submission error:', error);
|
||||||
this.constructor.hideProcessing();
|
this.constructor.hideProcessing();
|
||||||
@ -138,41 +132,14 @@ class SocialAdsProcessor extends WorkflowsCore {
|
|||||||
this.constructor.showToast('✅ Social ads generated successfully!', 'success');
|
this.constructor.showToast('✅ Social ads generated successfully!', 'success');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Direct N8N error:', error);
|
console.error('N8N processing 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 {
|
|
||||||
this.constructor.hideProcessing();
|
this.constructor.hideProcessing();
|
||||||
this.constructor.showToast(`❌ ${result.error || 'Processing failed'}`, 'error');
|
this.constructor.showToast('❌ Processing failed. Please try again.', 'error');
|
||||||
this.resetSubmitButton();
|
this.resetSubmitButton();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display results from direct N8N call
|
* Display results from direct N8N call
|
||||||
*/
|
*/
|
||||||
@ -361,79 +328,7 @@ class SocialAdsProcessor extends WorkflowsCore {
|
|||||||
return isValid;
|
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
|
* Reset submit button to original state
|
||||||
|
|||||||
@ -8,7 +8,7 @@ AGENT_CONFIGS = {
|
|||||||
'name': 'Social Ads Generator',
|
'name': 'Social Ads Generator',
|
||||||
'description': 'Create engaging social media advertisements with AI-powered content generation',
|
'description': 'Create engaging social media advertisements with AI-powered content generation',
|
||||||
'category': 'marketing',
|
'category': 'marketing',
|
||||||
'price': 5.0,
|
'price': 6.0,
|
||||||
'icon': '📱',
|
'icon': '📱',
|
||||||
'webhook_url': 'http://localhost:5678/webhook/2dc234d8-7217-454a-83e9-81afe5b4fe2d',
|
'webhook_url': 'http://localhost:5678/webhook/2dc234d8-7217-454a-83e9-81afe5b4fe2d',
|
||||||
},
|
},
|
||||||
@ -17,9 +17,9 @@ AGENT_CONFIGS = {
|
|||||||
'name': 'Job Posting Generator',
|
'name': 'Job Posting Generator',
|
||||||
'description': 'Create professional job postings that attract top talent',
|
'description': 'Create professional job postings that attract top talent',
|
||||||
'category': 'content',
|
'category': 'content',
|
||||||
'price': 4.0,
|
'price': 10.0,
|
||||||
'icon': '💼',
|
'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': {
|
'five-whys-analyzer': {
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<div class="quick-agents-grid">
|
<div class="quick-agents-grid">
|
||||||
{% if available_agents %}
|
{% if available_agents %}
|
||||||
{% for agent_slug, agent_info in available_agents.items %}
|
{% for agent_slug, agent_info in available_agents.items %}
|
||||||
<a href="/workflows/{{ agent_slug }}/" class="quick-agent-card">
|
<a href="/agents/{{ agent_slug }}/" class="quick-agent-card">
|
||||||
<div class="agent-icon">{{ agent_info.icon }}</div>
|
<div class="agent-icon">{{ agent_info.icon }}</div>
|
||||||
<div class="agent-info">
|
<div class="agent-info">
|
||||||
<h4>{{ agent_info.name }}</h4>
|
<h4>{{ agent_info.name }}</h4>
|
||||||
@ -19,7 +19,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- Fallback to hardcoded agents if available_agents not provided -->
|
<!-- Fallback to hardcoded agents if available_agents not provided -->
|
||||||
<a href="/workflows/data-analyzer/" class="quick-agent-card">
|
<a href="/agents/data-analyzer/" class="quick-agent-card">
|
||||||
<div class="agent-icon">📊</div>
|
<div class="agent-icon">📊</div>
|
||||||
<div class="agent-info">
|
<div class="agent-info">
|
||||||
<h4>Data Analyzer</h4>
|
<h4>Data Analyzer</h4>
|
||||||
@ -27,7 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/workflows/weather-reporter/" class="quick-agent-card">
|
<a href="/agents/weather-reporter/" class="quick-agent-card">
|
||||||
<div class="agent-icon">🌤️</div>
|
<div class="agent-icon">🌤️</div>
|
||||||
<div class="agent-info">
|
<div class="agent-info">
|
||||||
<h4>Weather Reporter</h4>
|
<h4>Weather Reporter</h4>
|
||||||
@ -35,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/workflows/social-ads-generator/" class="quick-agent-card">
|
<a href="/agents/social-ads-generator/" class="quick-agent-card">
|
||||||
<div class="agent-icon">📢</div>
|
<div class="agent-icon">📢</div>
|
||||||
<div class="agent-info">
|
<div class="agent-info">
|
||||||
<h4>Social Ads Generator</h4>
|
<h4>Social Ads Generator</h4>
|
||||||
@ -43,7 +43,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/workflows/job-posting-generator/" class="quick-agent-card">
|
<a href="/agents/job-posting-generator/" class="quick-agent-card">
|
||||||
<div class="agent-icon">💼</div>
|
<div class="agent-icon">💼</div>
|
||||||
<div class="agent-info">
|
<div class="agent-info">
|
||||||
<h4>Job Posting Generator</h4>
|
<h4>Job Posting Generator</h4>
|
||||||
@ -51,7 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/workflows/five-whys-analyzer/" class="quick-agent-card">
|
<a href="/agents/five-whys-analyzer/" class="quick-agent-card">
|
||||||
<div class="agent-icon">🤔</div>
|
<div class="agent-icon">🤔</div>
|
||||||
<div class="agent-info">
|
<div class="agent-info">
|
||||||
<h4>Five Whys Analyzer</h4>
|
<h4>Five Whys Analyzer</h4>
|
||||||
|
|||||||
@ -180,6 +180,11 @@ document.body.setAttribute('data-agent-price', '{{ agent_config.price }}');
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="file" id="dataFile" name="file" accept=".pdf" style="display: none;" required>
|
<input type="file" id="dataFile" name="file" accept=".pdf" style="display: none;" required>
|
||||||
|
|
||||||
|
<!-- File Display Elements -->
|
||||||
|
<div id="fileName" style="margin-top: var(--spacing-sm); color: var(--success); font-weight: 500; display: none;"></div>
|
||||||
|
<div id="fileSize" style="margin-top: var(--spacing-xs); color: var(--on-surface-variant); font-size: 12px; display: none;"></div>
|
||||||
|
|
||||||
<div class="form-help">Supported format: PDF files only. Max size: 10MB</div>
|
<div class="form-help">Supported format: PDF files only. Max size: 10MB</div>
|
||||||
<div id="dataFile-error" class="form-error" style="display: none;"></div>
|
<div id="dataFile-error" class="form-error" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -212,7 +217,7 @@ document.body.setAttribute('data-agent-price', '{{ agent_config.price }}');
|
|||||||
<div style="margin-top: var(--spacing-lg);">
|
<div style="margin-top: var(--spacing-lg);">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
{% if user.wallet_balance >= agent_config.price %}
|
{% if user.wallet_balance >= agent_config.price %}
|
||||||
<button type="submit" class="btn btn-primary btn-full" id="analyzeBtn">
|
<button type="submit" class="btn btn-primary btn-full" id="generateBtn">
|
||||||
🚀 Analyze Data ({{ agent_config.price }} AED)
|
🚀 Analyze Data ({{ agent_config.price }} AED)
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
289
workflows/templates/workflows/job-posting-generator.html
Normal file
289
workflows/templates/workflows/job-posting-generator.html
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Job Posting Generator - Quantum Tasks AI{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<link rel="stylesheet" href="{% static 'css/agent-base.css' %}?v={{ timestamp }}">
|
||||||
|
<style>
|
||||||
|
/* Job Posting Generator Specific Styles */
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid var(--outline-variant);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--on-surface);
|
||||||
|
font-family: inherit;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea:hover {
|
||||||
|
border-color: var(--on-surface-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Form Sections */
|
||||||
|
.section-container {
|
||||||
|
margin-bottom: var(--spacing-xl);
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
background: var(--surface-variant);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--outline-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subtitle {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--on-surface);
|
||||||
|
margin: 0 0 var(--spacing-lg) 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Job Posting Results Styling */
|
||||||
|
.job-posting-content {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--on-surface);
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-section-title {
|
||||||
|
color: var(--primary);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 1.5rem 0 0.75rem 0 !important;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 2px solid var(--outline-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-section-title:first-child {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-paragraph {
|
||||||
|
margin: 1rem 0;
|
||||||
|
text-align: justify;
|
||||||
|
color: var(--on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-list {
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-list li {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-list li::marker {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.job-section-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-container {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<script>
|
||||||
|
// Set data attributes for JavaScript access
|
||||||
|
document.body.setAttribute('data-user-authenticated', '{{ user.is_authenticated|yesno:"true,false" }}');
|
||||||
|
document.body.setAttribute('data-agent-price', '{{ agent_config.price }}');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="agent-container">
|
||||||
|
<!-- Agent Header Component -->
|
||||||
|
{% include "workflows/components/agent_header.html" with agent_title="Job Posting Generator" agent_subtitle="Create professional job postings that attract top talent" %}
|
||||||
|
|
||||||
|
<!-- Quick Agent Access Panel Component -->
|
||||||
|
{% include "workflows/components/quick_agents_panel.html" %}
|
||||||
|
|
||||||
|
<!-- Main Agent Grid -->
|
||||||
|
<div class="agent-grid">
|
||||||
|
<!-- Job Posting Form Widget -->
|
||||||
|
<div class="agent-widget widget-large" style="flex: 1; margin-right: clamp(0px, var(--spacing-lg), 2vw);">
|
||||||
|
<div class="widget-header">
|
||||||
|
<h3 class="widget-title">
|
||||||
|
<span class="widget-icon">💼</span>
|
||||||
|
Job Posting Configuration
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="widget-content">
|
||||||
|
<form id="agentForm" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<!-- Basic Job Information -->
|
||||||
|
<div class="section-container">
|
||||||
|
<h4 class="section-subtitle">
|
||||||
|
<span>📝</span>
|
||||||
|
Basic Information
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Job Title *</label>
|
||||||
|
<input type="text" name="job_title" id="job_title" class="form-input"
|
||||||
|
placeholder="e.g., Senior Software Engineer"
|
||||||
|
value="Senior Full Stack Developer" required>
|
||||||
|
<div id="job_title-error" class="form-error" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Company Name *</label>
|
||||||
|
<input type="text" name="company_name" id="company_name" class="form-input"
|
||||||
|
placeholder="e.g., TechCorp Inc."
|
||||||
|
value="Quantum Technologies Inc." required>
|
||||||
|
<div id="company_name-error" class="form-error" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Job Description *</label>
|
||||||
|
<textarea name="job_description" id="job_description" class="form-textarea"
|
||||||
|
placeholder="Describe the role, requirements, and company culture..."
|
||||||
|
rows="4" required>We are looking for a talented Senior Full Stack Developer to join our innovative team. You will work on cutting-edge projects using modern technologies like React, Node.js, and Python. Strong problem-solving skills and experience with cloud platforms preferred.</textarea>
|
||||||
|
<div class="form-help">Provide a detailed description of the role and requirements</div>
|
||||||
|
<div id="job_description-error" class="form-error" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Position Details -->
|
||||||
|
<div class="section-container">
|
||||||
|
<h4 class="section-subtitle">
|
||||||
|
<span>🎯</span>
|
||||||
|
Position Details
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Seniority Level *</label>
|
||||||
|
<select name="seniority_level" id="seniority_level" class="form-input" required>
|
||||||
|
<option value="">Select level...</option>
|
||||||
|
<option value="entry">Entry Level</option>
|
||||||
|
<option value="mid">Mid Level</option>
|
||||||
|
<option value="senior" selected>Senior Level</option>
|
||||||
|
<option value="lead">Lead/Principal</option>
|
||||||
|
<option value="executive">Executive</option>
|
||||||
|
</select>
|
||||||
|
<div id="seniority_level-error" class="form-error" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Contract Type *</label>
|
||||||
|
<select name="contract_type" id="contract_type" class="form-input" required>
|
||||||
|
<option value="">Select type...</option>
|
||||||
|
<option value="full-time" selected>Full-time</option>
|
||||||
|
<option value="part-time">Part-time</option>
|
||||||
|
<option value="contract">Contract</option>
|
||||||
|
<option value="freelance">Freelance</option>
|
||||||
|
<option value="internship">Internship</option>
|
||||||
|
</select>
|
||||||
|
<div id="contract_type-error" class="form-error" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Location *</label>
|
||||||
|
<input type="text" name="location" id="location" class="form-input"
|
||||||
|
placeholder="e.g., Dubai, UAE or Remote"
|
||||||
|
value="Dubai, UAE (Remote)" required>
|
||||||
|
<div id="location-error" class="form-error" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Language</label>
|
||||||
|
<select name="language" id="language" class="form-input">
|
||||||
|
<option value="English">English</option>
|
||||||
|
<option value="Arabic">Arabic</option>
|
||||||
|
<option value="Spanish">Spanish</option>
|
||||||
|
<option value="French">French</option>
|
||||||
|
<option value="German">German</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<div style="margin-top: var(--spacing-lg);">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
{% if user.wallet_balance >= agent_config.price %}
|
||||||
|
<button type="submit" class="btn btn-primary btn-full" id="generateBtn">
|
||||||
|
💼 Generate Job Posting ({{ agent_config.price }} AED)
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<div style="background: #fef2f2; color: #dc2626; padding: var(--spacing-md); border-radius: var(--radius-md); margin-bottom: var(--spacing-md); font-size: 14px; font-weight: 500; text-align: center;">
|
||||||
|
Insufficient balance! You need {{ agent_config.price }} AED.
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'wallet:wallet' %}" class="btn btn-primary btn-full" style="text-decoration: none;">
|
||||||
|
💰 Top Up Wallet
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'authentication:login' %}" class="btn btn-primary btn-full">
|
||||||
|
🔐 Login to Continue
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- How It Works Widget -->
|
||||||
|
<div class="agent-widget widget-small" style="min-width: min(280px, 100%); max-width: min(280px, 100%); margin-left: auto;">
|
||||||
|
<div class="widget-header">
|
||||||
|
<h3 class="widget-title">
|
||||||
|
<span class="widget-icon">ℹ️</span>
|
||||||
|
How It Works
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="widget-content">
|
||||||
|
<ol class="info-list">
|
||||||
|
<li>Enter job requirements</li>
|
||||||
|
<li>Configure position details</li>
|
||||||
|
<li>AI processes your information</li>
|
||||||
|
<li>Get professional job posting</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<!-- Other Agents Button -->
|
||||||
|
<button class="quick-agent-toggle btn btn-secondary btn-full" onclick="toggleQuickAgents()"
|
||||||
|
title="Quick access to other agents"
|
||||||
|
aria-label="Open quick access panel for other AI agents"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="quickAgentsPanel"
|
||||||
|
style="margin-top: var(--spacing-md);">
|
||||||
|
<span class="toggle-icon" aria-hidden="true">🚀</span>
|
||||||
|
<span class="toggle-text">Explore Other Agents</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Processing Status Component -->
|
||||||
|
{% include "workflows/components/processing_status.html" with status_title="Creating Job Posting..." status_text="Please wait while we generate your professional job posting..." %}
|
||||||
|
|
||||||
|
<!-- Results Component -->
|
||||||
|
{% include "workflows/components/results_container.html" with results_title="Generated Job Posting" %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script src="{% static 'js/workflows-core.js' %}?v={{ timestamp }}"></script>
|
||||||
|
<script src="{% static 'js/job-posting-generator.js' %}?v={{ timestamp }}"></script>
|
||||||
|
{% endblock %}
|
||||||
@ -248,7 +248,7 @@ document.body.setAttribute('data-agent-price', '{{ agent_config.price }}');
|
|||||||
<label class="form-label" for="description">📝 Describe what you'd like to generate *</label>
|
<label class="form-label" for="description">📝 Describe what you'd like to generate *</label>
|
||||||
<textarea id="description" name="description" class="form-textarea"
|
<textarea id="description" name="description" class="form-textarea"
|
||||||
placeholder="Describe the product, service, or campaign you want to create an ad for. Include key features, target audience, and any specific messaging you want to emphasize."
|
placeholder="Describe the product, service, or campaign you want to create an ad for. Include key features, target audience, and any specific messaging you want to emphasize."
|
||||||
required rows="4"></textarea>
|
required rows="4">Revolutionary AI-powered task management app that helps teams boost productivity by 300%. Features smart scheduling, automated workflows, and real-time collaboration. Perfect for startups and growing businesses looking to streamline operations.</textarea>
|
||||||
<div class="form-help">Provide clear, specific information about your product or service for better ad copy</div>
|
<div class="form-help">Provide clear, specific information about your product or service for better ad copy</div>
|
||||||
<div id="description-error" class="form-error" style="display: none;"></div>
|
<div id="description-error" class="form-error" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -256,7 +256,7 @@ document.body.setAttribute('data-agent-price', '{{ agent_config.price }}');
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="language">🌐 Language</label>
|
<label class="form-label" for="language">🌐 Language</label>
|
||||||
<select id="language" name="language" class="form-input">
|
<select id="language" name="language" class="form-input">
|
||||||
<option value="English">English</option>
|
<option value="English" selected>English</option>
|
||||||
<option value="Arabic">Arabic (العربية)</option>
|
<option value="Arabic">Arabic (العربية)</option>
|
||||||
<option value="Spanish">Spanish (Español)</option>
|
<option value="Spanish">Spanish (Español)</option>
|
||||||
<option value="French">French (Français)</option>
|
<option value="French">French (Français)</option>
|
||||||
@ -276,7 +276,7 @@ document.body.setAttribute('data-agent-price', '{{ agent_config.price }}');
|
|||||||
<select id="social_platform" name="social_platform" class="form-input" required>
|
<select id="social_platform" name="social_platform" class="form-input" required>
|
||||||
<option value="">Select a platform...</option>
|
<option value="">Select a platform...</option>
|
||||||
<option value="facebook">Facebook</option>
|
<option value="facebook">Facebook</option>
|
||||||
<option value="instagram">Instagram</option>
|
<option value="instagram" selected>Instagram</option>
|
||||||
<option value="linkedin">LinkedIn</option>
|
<option value="linkedin">LinkedIn</option>
|
||||||
<option value="twitter">X (Twitter)</option>
|
<option value="twitter">X (Twitter)</option>
|
||||||
<option value="tiktok">TikTok</option>
|
<option value="tiktok">TikTok</option>
|
||||||
@ -290,7 +290,7 @@ document.body.setAttribute('data-agent-price', '{{ agent_config.price }}');
|
|||||||
<label class="form-label" for="include_emoji">😊 Include Emoji *</label>
|
<label class="form-label" for="include_emoji">😊 Include Emoji *</label>
|
||||||
<select id="include_emoji" name="include_emoji" class="form-input" required>
|
<select id="include_emoji" name="include_emoji" class="form-input" required>
|
||||||
<option value="">Select an option...</option>
|
<option value="">Select an option...</option>
|
||||||
<option value="yes">Yes</option>
|
<option value="yes" selected>Yes</option>
|
||||||
<option value="no">No</option>
|
<option value="no">No</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="form-help">Whether to include emojis in the ad copy</div>
|
<div class="form-help">Whether to include emojis in the ad copy</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user