mirror of
https://github.com/thecyberlearn/quantum-ai-v3.git
synced 2026-08-18 22:52:58 +00:00
- 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>
428 lines
16 KiB
JavaScript
428 lines
16 KiB
JavaScript
/**
|
|
* 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();
|
|
}); |