quantum-ai-v3/static/js/agents-core.js
Claude 400f2b7a09 📄 Add PDF Summarizer agent with advanced file upload system
- Create Document Processing category for file-based agents
- Implement PDF Summarizer with comprehensive 4-field form schema
- Add file upload field type with drag-and-drop support
- Build professional file upload UI with progress indicators
- Update agents-core.js to handle multipart form data uploads
- Add direct N8N webhook integration for file processing
- Support multiple analysis types: summary, key points, sentiment analysis
- Implement file size validation and type checking

File Upload Features:
- Drag and drop PDF files directly onto upload area
- Visual feedback with file name and size display
- Remove file functionality with single click
- Accept only PDF files with 10MB size limit
- Professional styling with hover and focus states

Form Options:
- Analysis type selection (summary, key points, detailed analysis)
- Language auto-detection or manual selection
- Output length control (short, medium, long)
- Real-time form validation and error handling

Price: 8.0 AED for comprehensive document analysis
Webhook: Matches multipart form-data structure exactly

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-31 19:18:31 +05:30

541 lines
20 KiB
JavaScript

/**
* Agents Core - Dynamic Agent Execution System
* Handles form submission and N8N integration for any agent
*/
class AgentsCore extends WorkflowsCore {
constructor() {
super();
this.agentId = document.body.getAttribute('data-agent-id');
this.agentSlug = document.body.getAttribute('data-agent-slug');
this.webhookUrl = document.body.getAttribute('data-webhook-url');
this.price = parseFloat(document.body.getAttribute('data-agent-price') || '0');
this.sessionId = this.constructor.generateSessionId();
// Initialize on page load
this.initialize();
}
initialize() {
// Initialize form submission
const form = document.getElementById('agentForm');
if (form) {
form.addEventListener('submit', this.handleFormSubmission.bind(this));
}
// Initialize form validation
this.initializeDynamicFormValidation();
}
/**
* Handle form submission with agents API integration
*/
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('Executing agent...');
const submitBtn = document.getElementById('generateBtn');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.textContent = '⏳ Processing...';
}
try {
// Use agents API for execution
await this.executeViaAgentsAPI(e.target);
} catch (error) {
console.error('Form submission error:', error);
this.constructor.hideProcessing();
this.constructor.showToast('❌ Connection error. Please try again.', 'error');
this.resetSubmitButton();
}
}
/**
* Execute agent via the agents API
*/
async executeViaAgentsAPI(form) {
try {
const formData = new FormData(form);
// Check if form contains file uploads
const hasFiles = Array.from(formData.entries()).some(([key, value]) =>
value instanceof File && key !== 'csrfmiddlewaretoken'
);
if (hasFiles) {
// Handle file upload via multipart form data
await this.executeWithFileUpload(formData);
} else {
// Handle regular form data via JSON API
await this.executeWithJsonAPI(formData);
}
} catch (error) {
console.error('Agent execution error:', error);
this.constructor.hideProcessing();
this.constructor.showToast(`${error.message}`, 'error');
this.resetSubmitButton();
}
}
/**
* Execute agent with file upload using multipart form data
*/
async executeWithFileUpload(formData) {
// Get CSRF token
const csrfToken = formData.get('csrfmiddlewaretoken');
// Prepare multipart form data for direct webhook call (similar to workflows)
const webhookFormData = new FormData();
// Add files and regular form fields
for (let [key, value] of formData.entries()) {
if (key !== 'csrfmiddlewaretoken') {
if (value instanceof File) {
webhookFormData.append('file', value);
} else {
// Map form fields to webhook expected format
if (key === 'analysis_type') {
webhookFormData.append('analysisType', value);
} else {
webhookFormData.append(key, value);
}
}
}
}
// Call webhook directly for file uploads (similar to workflows approach)
const response = await fetch(this.webhookUrl, {
method: 'POST',
body: webhookFormData,
signal: AbortSignal.timeout(120000) // 2 minute timeout for file processing
});
if (!response.ok) {
throw new Error(`File processing failed: ${response.status}`);
}
// Parse response
const data = await response.json();
// Deduct wallet balance manually since we bypassed the API
await this.deductBalanceForFileUpload();
// Process successful execution
this.constructor.hideProcessing();
// Display results
this.displayFileProcessingResults(data);
this.constructor.showToast('✅ File processed successfully!', 'success');
}
/**
* Execute agent with JSON API (for non-file uploads)
*/
async executeWithJsonAPI(formData) {
// Extract all form data dynamically
const inputData = {};
for (let [key, value] of formData.entries()) {
if (key !== 'csrfmiddlewaretoken') {
inputData[key] = value;
}
}
// Get CSRF token
const csrfToken = formData.get('csrfmiddlewaretoken');
// Call agents API
const response = await fetch('/agents/api/execute/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
agent_slug: this.agentSlug,
input_data: inputData
}),
signal: AbortSignal.timeout(90000) // 90 second timeout
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(errorData.error || `API error: ${response.status}`);
}
const data = await response.json();
// Process successful execution
this.constructor.hideProcessing();
// Update wallet balance if fee was charged
if (data.fee_charged) {
const currentBalance = parseFloat(document.body.getAttribute('data-user-balance') || '0');
const newBalance = currentBalance - parseFloat(data.fee_charged);
// Update the wallet balance display
this.constructor.updateWalletBalance(newBalance);
// Update the data attribute for future calculations
document.body.setAttribute('data-user-balance', newBalance.toString());
}
// Display results
this.displayExecutionResults(data);
this.constructor.showToast('✅ Agent executed successfully!', 'success');
}
/**
* Deduct wallet balance for file upload (manual deduction)
*/
async deductBalanceForFileUpload() {
try {
// Call the wallet deduction API
const response = await fetch('/wallet/api/deduct/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: JSON.stringify({
amount: this.price,
description: `${this.agentSlug.replace('-', ' ')} execution`,
agent_slug: this.agentSlug
})
});
if (response.ok) {
const data = await response.json();
if (data.new_balance !== undefined) {
// Update wallet balance display
this.constructor.updateWalletBalance(data.new_balance);
document.body.setAttribute('data-user-balance', data.new_balance.toString());
}
}
} catch (error) {
console.error('Wallet deduction error:', error);
// Continue execution even if wallet update fails
}
}
/**
* Display results from file processing
*/
displayFileProcessingResults(data) {
const resultsContainer = document.getElementById('resultsContainer');
const resultsContent = document.getElementById('resultsContent');
if (!resultsContainer || !resultsContent) return;
let content = '';
// Handle different response formats from file processing
if (data && typeof data === 'object') {
if (data.sections) {
// Multi-section response
content = Object.entries(data.sections).map(([section, text]) => {
return `## ${section.replace('_', ' ').toUpperCase()}\n\n${text}`;
}).join('\n\n');
} else if (data.output || data.result || data.summary) {
content = data.output || data.result || data.summary;
} else if (data.error) {
content = `Error: ${data.error}`;
} else {
content = JSON.stringify(data, null, 2);
}
} else if (typeof data === 'string') {
content = data;
} else {
content = 'File processed successfully!';
}
// Clear and populate results securely
resultsContent.textContent = '';
this.renderSecureContent(resultsContent, content);
// Show results container
resultsContainer.style.display = 'block';
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
this.resetSubmitButton();
}
/**
* Display results from agent execution
*/
displayExecutionResults(executionData) {
const resultsContainer = document.getElementById('resultsContainer');
const resultsContent = document.getElementById('resultsContent');
if (!resultsContainer || !resultsContent) return;
let content = '';
// Handle different response formats
if (executionData.output_data && typeof executionData.output_data === 'object') {
// Handle N8N response formats
const output = executionData.output_data;
content = output.output || output.text || output.content || output.result || output.message || JSON.stringify(output, null, 2);
} else if (executionData.output_data && typeof executionData.output_data === 'string') {
content = executionData.output_data;
} else {
content = `Agent executed successfully!\n\nExecution ID: ${executionData.id}\nStatus: ${executionData.status}\nFee Charged: ${executionData.fee_charged} AED`;
}
// Clear and populate results securely
resultsContent.textContent = '';
this.renderSecureContent(resultsContent, content);
// Show results container
resultsContainer.style.display = 'block';
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
this.resetSubmitButton();
}
/**
* Secure content rendering without innerHTML to prevent XSS
*/
renderSecureContent(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 = 'results-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.textContent = line.substring(4);
} else if (line.startsWith('## ')) {
element = document.createElement('h2');
element.textContent = line.substring(3);
} else if (line.startsWith('# ')) {
element = document.createElement('h1');
element.textContent = line.substring(2);
} else {
// Handle regular text with basic formatting
element = document.createElement('span');
this.formatTextSecurely(element, line);
}
wrapper.appendChild(element);
// Add line break if not the last line
if (i < lines.length - 1) {
wrapper.appendChild(document.createElement('br'));
}
}
container.appendChild(wrapper);
}
/**
* Format text with basic styling while preventing XSS
*/
formatTextSecurely(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));
}
});
}
/**
* Initialize dynamic form validation based on form schema
*/
initializeDynamicFormValidation() {
const fields = document.querySelectorAll('#agentForm [name]');
fields.forEach(field => {
const fieldName = field.getAttribute('name');
if (fieldName && fieldName !== 'csrfmiddlewaretoken') {
field.addEventListener('blur', () => this.validateField(fieldName));
field.addEventListener('input', () => this.constructor.clearFieldError(fieldName));
}
});
}
validateField(fieldName) {
const field = document.getElementById(fieldName);
if (!field) return true;
const value = field.type === 'checkbox' ? field.checked : field.value.trim();
const required = field.hasAttribute('required');
// Basic required field validation
if (required && (!value || value === '')) {
this.constructor.showFieldError(fieldName, `${fieldName.replace('_', ' ')} is required`);
return false;
}
// Specific validation based on field type
if (field.type === 'textarea' && value && value.length < 10) {
this.constructor.showFieldError(fieldName, 'Please provide more detailed information (at least 10 characters)');
return false;
}
if (field.type === 'url' && value && !this.isValidURL(value)) {
this.constructor.showFieldError(fieldName, 'Please enter a valid URL');
return false;
}
this.constructor.clearFieldError(fieldName);
return true;
}
isValidURL(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}
isFormValid() {
const fields = document.querySelectorAll('#agentForm [name]');
let isValid = true;
fields.forEach(field => {
const fieldName = field.getAttribute('name');
if (fieldName && fieldName !== 'csrfmiddlewaretoken') {
if (!this.validateField(fieldName)) {
isValid = false;
}
}
});
return isValid;
}
/**
* Reset submit button to original state
*/
resetSubmitButton() {
const submitBtn = document.getElementById('generateBtn');
if (submitBtn) {
submitBtn.disabled = false;
const agentSlug = document.body.getAttribute('data-agent-slug') || 'agent';
const agentName = agentSlug.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase());
submitBtn.textContent = `🚀 Execute ${agentName} (${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, 'Results copied to clipboard!');
}
}
function downloadResults() {
const content = document.getElementById('resultsContent');
if (content) {
const text = content.textContent || '';
const agentSlug = document.body.getAttribute('data-agent-slug') || 'agent';
WorkflowsCore.downloadAsFile(text, `${agentSlug}-results.txt`, 'Results downloaded!');
}
}
function resetForm() {
const form = document.getElementById('agentForm');
if (form) {
// Reset form but preserve CSRF token
const csrfToken = form.querySelector('[name="csrfmiddlewaretoken"]').value;
form.reset();
form.querySelector('[name="csrfmiddlewaretoken"]').value = csrfToken;
}
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 = document.querySelectorAll('#agentForm [name]');
fields.forEach(field => {
const fieldName = field.getAttribute('name');
if (fieldName && fieldName !== 'csrfmiddlewaretoken') {
WorkflowsCore.clearFieldError(fieldName);
}
});
// Scroll back to form
const formSection = document.getElementById('agentForm');
if (formSection) {
formSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
// Initialize Agents Core when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
// Initialize processor (data attributes set by template)
window.agentsCore = new AgentsCore();
});