diff --git a/data_analyzer/templates/data_analyzer/detail.html b/data_analyzer/templates/data_analyzer/detail.html index c57037a..edfbf94 100644 --- a/data_analyzer/templates/data_analyzer/detail.html +++ b/data_analyzer/templates/data_analyzer/detail.html @@ -687,26 +687,51 @@ } } + // Track polling and results to prevent duplicates + let resultsDisplayed = false; + let currentPollInterval = null; + // Poll for results function pollForResults(requestId) { let pollCount = 0; const maxPolls = 60; // 60 seconds maximum for data analysis + resultsDisplayed = false; // Reset flag - const pollInterval = setInterval(() => { + // Clear any existing polling + if (currentPollInterval) { + clearInterval(currentPollInterval); + currentPollInterval = null; + } + + currentPollInterval = setInterval(() => { pollCount++; fetch(`/agents/data-analyzer/result/${requestId}/`) - .then(response => response.json()) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return response.json(); + }) .then(result => { if (result.status === 'completed' || result.status === 'failed') { - clearInterval(pollInterval); + // Stop polling immediately + clearInterval(currentPollInterval); + currentPollInterval = null; + + // Reset UI document.getElementById('processingStatus').style.display = 'none'; document.getElementById('processButton').disabled = false; document.getElementById('processButton').innerHTML = '📊 Analyze Data (5.00 AED)'; - displayResults(result); + // Display results only once + if (!resultsDisplayed) { + resultsDisplayed = true; + displayResults(result); + } } else if (pollCount >= maxPolls) { - clearInterval(pollInterval); + clearInterval(currentPollInterval); + currentPollInterval = null; document.getElementById('processingStatus').style.display = 'none'; document.getElementById('processButton').disabled = false; document.getElementById('processButton').innerHTML = '📊 Analyze Data (5.00 AED)'; @@ -715,13 +740,12 @@ }) .catch(error => { console.error('Error polling results:', error); - if (pollCount >= maxPolls) { - clearInterval(pollInterval); - document.getElementById('processingStatus').style.display = 'none'; - document.getElementById('processButton').disabled = false; - document.getElementById('processButton').innerHTML = '📊 Analyze Data (5.00 AED)'; - showToast('❌ Network error - please try again', 'error'); - } + clearInterval(currentPollInterval); + currentPollInterval = null; + document.getElementById('processingStatus').style.display = 'none'; + document.getElementById('processButton').disabled = false; + document.getElementById('processButton').innerHTML = '📊 Analyze Data (5.00 AED)'; + showToast('❌ Network error during processing - please try again', 'error'); }); }, 1000); } @@ -729,10 +753,10 @@ function updateWalletBalance(newBalance) { // Update wallet balance display - const balanceElements = document.querySelectorAll('[data-wallet-balance]'); - balanceElements.forEach(element => { - element.textContent = `${newBalance.toFixed(2)} AED`; - }); + const balanceElement = document.getElementById('walletBalance'); + if (balanceElement) { + balanceElement.textContent = `${newBalance.toFixed(2)} AED`; + } window.currentWalletBalance = newBalance; } diff --git a/docs/agent-polling-guide.md b/docs/agent-polling-guide.md new file mode 100644 index 0000000..a6808ba --- /dev/null +++ b/docs/agent-polling-guide.md @@ -0,0 +1,320 @@ +# Agent Polling System Guide + +This document explains how to use the reusable polling system for NetCop AI agents. + +## Overview + +The agent polling system provides a standardized way to handle asynchronous requests in agent templates, with proper cleanup, error handling, and user feedback. + +## Key Features + +- **Automatic cleanup**: Prevents memory leaks and duplicate polling +- **Error handling**: Handles network errors and timeouts gracefully +- **Duplicate prevention**: Ensures results are displayed only once +- **Progressive feedback**: Shows status steps for better UX +- **Reusable utilities**: Common functions for wallet updates, toasts, etc. + +## Basic Usage + +### 1. Include the Script + +Add to your agent template's `extra_css` block: + +```html +{% block extra_js %} + + +{% endblock %} +``` + +### 2. Set Up Polling + +```javascript +// For agents that use async polling +function startPolling(requestId) { + const poller = window.pollingManager.createPoller('myAgent', { + requestId: requestId, + statusUrl: `/agents/my-agent/status/${requestId}/`, + maxPolls: 30, + pollInterval: 1000, + onComplete: (result) => { + AgentUtils.resetUI({ + processingStatusId: 'processingStatus', + processButtonId: 'processButton', + resultsId: 'results', + buttonText: '🔄 Generate Again (5.00 AED)' + }); + displayResults(result); + }, + onError: (error) => { + AgentUtils.resetUI({ + processingStatusId: 'processingStatus', + processButtonId: 'processButton', + buttonText: '🔄 Try Again (5.00 AED)' + }); + AgentUtils.showToast('❌ Network error - please try again', 'error'); + }, + onTimeout: () => { + AgentUtils.resetUI({ + processingStatusId: 'processingStatus', + processButtonId: 'processButton', + buttonText: '🔄 Try Again (5.00 AED)' + }); + AgentUtils.showToast('❌ Processing timeout - please try again', 'error'); + } + }); + + poller.start(); +} +``` + +### 3. Handle Form Submission + +```javascript +document.getElementById('myForm').addEventListener('submit', function(e) { + e.preventDefault(); + + // Validation + if (!isFormValid()) { + AgentUtils.showToast('Please fill in all required fields', 'error'); + return; + } + + // Authentication check + if (!isAuthenticated) { + window.location.href = loginUrl; + return; + } + + // Balance check + if (userBalance < requiredAmount) { + AgentUtils.showToast(`Insufficient balance! You need ${requiredAmount} AED.`, 'error'); + setTimeout(() => window.location.href = walletUrl, 2000); + return; + } + + // Clear any existing polling + window.pollingManager.stopAll(); + + // Show processing status + AgentUtils.showProcessing({ + processingStatusId: 'processingStatus', + processButtonId: 'processButton', + resultsId: 'results', + processingText: '⏳ Processing...' + }); + + // Start status steps + const stepper = new StatusStepper([ + 'Analyzing request...', + 'Processing data...', + 'Generating results...', + 'Finalizing output...' + ], 'statusText'); + stepper.start(); + + // Submit form + const formData = new FormData(this); + + fetch(submitUrl, { + method: 'POST', + body: formData, + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }) + .then(response => response.json()) + .then(result => { + stepper.stop(); + + if (result.success && result.request_id) { + // Start polling for async agents + startPolling(result.request_id); + } else { + // Handle immediate response + AgentUtils.resetUI({ + processingStatusId: 'processingStatus', + processButtonId: 'processButton', + buttonText: '🔄 Try Again (5.00 AED)' + }); + + if (result.error) { + AgentUtils.showToast(`❌ ${result.error}`, 'error'); + } else { + displayResults(result); + } + } + }) + .catch(error => { + stepper.stop(); + AgentUtils.resetUI({ + processingStatusId: 'processingStatus', + processButtonId: 'processButton', + buttonText: '🔄 Try Again (5.00 AED)' + }); + AgentUtils.showToast('❌ Network error - please try again', 'error'); + }); +}); +``` + +### 4. Reset Function + +```javascript +function resetForm() { + // Stop all polling + window.pollingManager.stopAll(); + + // Reset form + document.getElementById('myForm').reset(); + + // Reset UI + AgentUtils.resetUI({ + processingStatusId: 'processingStatus', + processButtonId: 'processButton', + resultsId: 'results', + buttonText: '🚀 Generate (5.00 AED)' + }); + + AgentUtils.showToast('Form reset! Ready for another request.', 'success'); +} +``` + +## API Reference + +### AgentPoller Class + +```javascript +const poller = new AgentPoller({ + requestId: 'string', // Request ID to poll + statusUrl: 'string', // Status endpoint URL + maxPolls: 30, // Maximum poll attempts + pollInterval: 1000, // Poll interval in ms + onComplete: function(result) {}, // Success callback + onError: function(error) {}, // Error callback + onTimeout: function() {} // Timeout callback +}); +``` + +### PollingManager + +```javascript +// Create and start a poller +const poller = window.pollingManager.createPoller('pollerId', config); +poller.start(); + +// Stop specific poller +window.pollingManager.stopPoller('pollerId'); + +// Stop all pollers +window.pollingManager.stopAll(); +``` + +### AgentUtils + +```javascript +// Update wallet balance +AgentUtils.updateWalletBalance(150.00); + +// Reset UI elements +AgentUtils.resetUI({ + processingStatusId: 'processingStatus', + processButtonId: 'processButton', + resultsId: 'results', + buttonText: 'Process Again' +}); + +// Show processing state +AgentUtils.showProcessing({ + processingStatusId: 'processingStatus', + processButtonId: 'processButton', + resultsId: 'results', + processingText: '⏳ Working...' +}); + +// Show toast notification +AgentUtils.showToast('Success message', 'success'); +AgentUtils.showToast('Error message', 'error'); +``` + +### StatusStepper + +```javascript +const stepper = new StatusStepper([ + 'Step 1...', + 'Step 2...', + 'Step 3...' +], 'statusTextElementId', 800); // 800ms interval + +stepper.start(); +stepper.stop(); +``` + +## Migration Guide + +### Converting Existing Agents + +1. **Include the script** in your template +2. **Replace polling logic** with `AgentPoller` +3. **Use `AgentUtils`** for common operations +4. **Add proper cleanup** in reset functions +5. **Use `StatusStepper`** for better UX + +### Before (old way): + +```javascript +// Old polling code with potential issues +let pollInterval = setInterval(() => { + fetch(statusUrl) + .then(response => response.json()) + .then(result => { + if (result.status === 'completed') { + clearInterval(pollInterval); + displayResults(result); + } + }); +}, 1000); +``` + +### After (new way): + +```javascript +// New robust polling +const poller = window.pollingManager.createPoller('agent', { + requestId: requestId, + statusUrl: statusUrl, + onComplete: displayResults, + onError: handleError, + onTimeout: handleTimeout +}); +poller.start(); +``` + +## Best Practices + +1. **Always stop existing polling** before starting new requests +2. **Use unique poller IDs** for different agents/features +3. **Provide clear error messages** to users +4. **Set appropriate timeouts** based on expected processing time +5. **Clean up resources** in reset functions +6. **Use progressive status steps** for better UX +7. **Prevent duplicate submissions** with proper state management + +## Troubleshooting + +### Common Issues + +1. **Multiple polling instances**: Use `pollingManager.stopAll()` before starting new requests +2. **Memory leaks**: Always call `stop()` or use the manager's cleanup methods +3. **Duplicate results**: The system prevents this automatically +4. **Network errors**: Handled automatically with proper user feedback + +### Debug Mode + +Enable debug logging: + +```javascript +// In development +window.agentPollingDebug = true; +``` + +This will log polling activities to the console for debugging. \ No newline at end of file diff --git a/job_posting_generator/templates/job_posting_generator/detail.html b/job_posting_generator/templates/job_posting_generator/detail.html index 4d333da..6d6cd70 100644 --- a/job_posting_generator/templates/job_posting_generator/detail.html +++ b/job_posting_generator/templates/job_posting_generator/detail.html @@ -4,82 +4,116 @@ {% block title %}Job Posting Generator Agent - NetCop AI Hub{% endblock %} {% block extra_css %} + + + + + + {% endblock %} @@ -317,42 +473,60 @@
')
+ .replace(/\n/g, '
');
+ }
+
+ // Display job posting results with markdown formatting
function displayResults(result) {
const resultsContainer = document.getElementById('jobResults');
const contentContainer = document.getElementById('jobContent');
if (result.success && result.status === 'completed') {
- contentContainer.textContent = result.content || result.job_posting_content || result.output_text || 'Job posting generated successfully!';
+ const content = result.content || result.job_posting_content || result.output_text || 'Job posting generated successfully!';
+
+ // Parse and display as HTML with markdown formatting
+ const formattedContent = parseMarkdown(content);
+ contentContainer.innerHTML = '
' + formattedContent + '
'; + resultsContainer.style.display = 'block'; // Update wallet balance if provided @@ -559,39 +842,65 @@ } } + // Track if results have been displayed to prevent duplicates + let resultsDisplayed = false; + let currentPollInterval = null; + // Poll for results function pollForResults(requestId) { let pollCount = 0; const maxPolls = 30; // 30 seconds maximum + resultsDisplayed = false; // Reset flag - const pollInterval = setInterval(() => { + // Clear any existing polling + if (currentPollInterval) { + clearInterval(currentPollInterval); + } + + currentPollInterval = setInterval(() => { pollCount++; fetch(`/agents/job-posting-generator/status/${requestId}/`) .then(response => response.json()) .then(result => { if (result.status === 'completed' || result.status === 'failed') { - clearInterval(pollInterval); - document.getElementById('processingStatus').style.display = 'none'; - document.getElementById('processButton').disabled = false; - document.getElementById('processButton').innerHTML = '💼 Generate Job Posting (4.00 AED)'; + // Stop polling immediately + clearInterval(currentPollInterval); + currentPollInterval = null; - displayResults(result); - } else if (pollCount >= maxPolls) { - clearInterval(pollInterval); + // Reset UI document.getElementById('processingStatus').style.display = 'none'; - document.getElementById('processButton').disabled = false; - document.getElementById('processButton').innerHTML = '💼 Generate Job Posting (4.00 AED)'; + const processButton = document.getElementById('processButton'); + processButton.disabled = false; + processButton.classList.remove('loading'); + processButton.innerHTML = '💼 Generate Job Posting (4.00 AED)'; + + // Display results only once + if (!resultsDisplayed) { + resultsDisplayed = true; + displayResults(result); + } + } else if (pollCount >= maxPolls) { + clearInterval(currentPollInterval); + currentPollInterval = null; + document.getElementById('processingStatus').style.display = 'none'; + const processButton = document.getElementById('processButton'); + processButton.disabled = false; + processButton.classList.remove('loading'); + processButton.innerHTML = '💼 Generate Job Posting (4.00 AED)'; showToast('❌ Processing timeout - please try again', 'error'); } }) .catch(error => { console.error('Error polling results:', error); if (pollCount >= maxPolls) { - clearInterval(pollInterval); + clearInterval(currentPollInterval); + currentPollInterval = null; document.getElementById('processingStatus').style.display = 'none'; - document.getElementById('processButton').disabled = false; - document.getElementById('processButton').innerHTML = '💼 Generate Job Posting (4.00 AED)'; + const processButton = document.getElementById('processButton'); + processButton.disabled = false; + processButton.classList.remove('loading'); + processButton.innerHTML = '💼 Generate Job Posting (4.00 AED)'; showToast('❌ Network error - please try again', 'error'); } }); @@ -607,6 +916,12 @@ return; } + // Prevent multiple submissions + const processButton = document.getElementById('processButton'); + if (processButton.disabled) { + return; // Already processing + } + // Check user authentication {% if not user.is_authenticated %} window.location.href = "{% url 'authentication:login' %}"; @@ -623,10 +938,18 @@ return; } - // Show processing status with steps + // Clear any existing polling and reset flags + if (currentPollInterval) { + clearInterval(currentPollInterval); + currentPollInterval = null; + } + resultsDisplayed = false; + + // Show processing status with enhanced loading document.getElementById('processingStatus').style.display = 'block'; - document.getElementById('processButton').disabled = true; - document.getElementById('processButton').innerHTML = '⏳ Processing...'; + processButton.disabled = true; + processButton.classList.add('loading'); + processButton.innerHTML = '⏳ Processing...'; document.getElementById('jobResults').style.display = 'none'; const steps = [ @@ -666,8 +989,10 @@ } else { // Handle immediate response document.getElementById('processingStatus').style.display = 'none'; - document.getElementById('processButton').disabled = false; - document.getElementById('processButton').innerHTML = '💼 Generate Job Posting (4.00 AED)'; + const processButton = document.getElementById('processButton'); + processButton.disabled = false; + processButton.classList.remove('loading'); + processButton.innerHTML = '💼 Generate Job Posting (4.00 AED)'; if (result.error) { showToast(`❌ ${result.error}`, 'error'); @@ -680,8 +1005,10 @@ clearInterval(stepInterval); console.error('Error:', error); document.getElementById('processingStatus').style.display = 'none'; - document.getElementById('processButton').disabled = false; - document.getElementById('processButton').innerHTML = '💼 Generate Job Posting (4.00 AED)'; + const processButton = document.getElementById('processButton'); + processButton.disabled = false; + processButton.classList.remove('loading'); + processButton.innerHTML = '💼 Generate Job Posting (4.00 AED)'; showToast('❌ Network error - please try again', 'error'); }); }); diff --git a/social_ads_generator/templates/social_ads_generator/detail.html b/social_ads_generator/templates/social_ads_generator/detail.html index 358b0ff..aca550e 100644 --- a/social_ads_generator/templates/social_ads_generator/detail.html +++ b/social_ads_generator/templates/social_ads_generator/detail.html @@ -473,6 +473,14 @@ // Reset form for creating another ad function resetForm() { + // Clear any active polling + if (currentPollInterval) { + clearInterval(currentPollInterval); + currentPollInterval = null; + } + resultsDisplayed = false; + + // Reset form and UI document.getElementById('socialAdsForm').reset(); document.getElementById('adResults').style.display = 'none'; document.getElementById('processingStatus').style.display = 'none'; @@ -532,26 +540,51 @@ } } + // Track polling and results to prevent duplicates + let resultsDisplayed = false; + let currentPollInterval = null; + // Poll for results function pollForResults(requestId) { let pollCount = 0; const maxPolls = 30; // 30 seconds maximum + resultsDisplayed = false; // Reset flag - const pollInterval = setInterval(() => { + // Clear any existing polling + if (currentPollInterval) { + clearInterval(currentPollInterval); + currentPollInterval = null; + } + + currentPollInterval = setInterval(() => { pollCount++; fetch(`/agents/social-ads-generator/status/${requestId}/`) - .then(response => response.json()) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return response.json(); + }) .then(result => { if (result.status === 'completed' || result.status === 'failed') { - clearInterval(pollInterval); + // Stop polling immediately + clearInterval(currentPollInterval); + currentPollInterval = null; + + // Reset UI document.getElementById('processingStatus').style.display = 'none'; document.getElementById('processButton').disabled = false; document.getElementById('processButton').innerHTML = '📢 Generate Social Ads (7.00 AED)'; - displayResults(result); + // Display results only once + if (!resultsDisplayed) { + resultsDisplayed = true; + displayResults(result); + } } else if (pollCount >= maxPolls) { - clearInterval(pollInterval); + clearInterval(currentPollInterval); + currentPollInterval = null; document.getElementById('processingStatus').style.display = 'none'; document.getElementById('processButton').disabled = false; document.getElementById('processButton').innerHTML = '📢 Generate Social Ads (7.00 AED)'; @@ -560,13 +593,12 @@ }) .catch(error => { console.error('Error polling results:', error); - if (pollCount >= maxPolls) { - clearInterval(pollInterval); - document.getElementById('processingStatus').style.display = 'none'; - document.getElementById('processButton').disabled = false; - document.getElementById('processButton').innerHTML = '📢 Generate Social Ads (7.00 AED)'; - showToast('❌ Network error - please try again', 'error'); - } + clearInterval(currentPollInterval); + currentPollInterval = null; + document.getElementById('processingStatus').style.display = 'none'; + document.getElementById('processButton').disabled = false; + document.getElementById('processButton').innerHTML = '📢 Generate Social Ads (7.00 AED)'; + showToast('❌ Network error during processing - please try again', 'error'); }); }, 1000); } @@ -596,6 +628,13 @@ return; } + // Clear any existing polling and reset flags + if (currentPollInterval) { + clearInterval(currentPollInterval); + currentPollInterval = null; + } + resultsDisplayed = false; + // Show processing status with steps document.getElementById('processingStatus').style.display = 'block'; document.getElementById('processButton').disabled = true; diff --git a/static/css/themes.css b/static/css/themes.css new file mode 100644 index 0000000..3bc59f1 --- /dev/null +++ b/static/css/themes.css @@ -0,0 +1,231 @@ +/* Lightweight Theme System for Job Posting Generator */ +/* No frameworks - pure CSS variables for clean theme switching */ + +/* Base styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + line-height: 1.6; + background-color: var(--bg-color); + color: var(--text-color); + transition: all 0.3s ease; +} + +/* Theme 1: Black & White (Grayscale Minimal) */ +.theme-black-white { + --bg-color: #ffffff; + --text-color: #1a1a1a; + --primary-color: #000000; + --card-bg: #f8f9fa; + --border-color: #e0e0e0; + --hover-color: #f0f0f0; + --accent-color: #666666; +} + +/* Theme 2: Blue (Calm and Modern) */ +.theme-blue { + --bg-color: #f8fafc; + --text-color: #1e293b; + --primary-color: #3b82f6; + --card-bg: #ffffff; + --border-color: #e2e8f0; + --hover-color: #f1f5f9; + --accent-color: #64748b; +} + +/* Theme 3: Orange (Vibrant and Friendly) */ +.theme-orange { + --bg-color: #fffbf7; + --text-color: #1c1917; + --primary-color: #ea580c; + --card-bg: #ffffff; + --border-color: #fed7aa; + --hover-color: #fff7ed; + --accent-color: #a3a3a3; +} + +/* Component Styles Using CSS Variables */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +.header { + background-color: var(--card-bg); + border-bottom: 1px solid var(--border-color); + padding: 1rem 0; + margin-bottom: 2rem; +} + +.header h1 { + color: var(--primary-color); + font-size: 2rem; + font-weight: 700; + text-align: center; +} + +.card { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 1.5rem; + margin-bottom: 1.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; +} + +.card:hover { + background-color: var(--hover-color); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.button { + background-color: var(--primary-color); + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + display: inline-block; +} + +.button:hover { + opacity: 0.9; + transform: translateY(-1px); +} + +.button-secondary { + background-color: var(--card-bg); + color: var(--primary-color); + border: 1px solid var(--border-color); +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-label { + display: block; + color: var(--text-color); + font-weight: 600; + margin-bottom: 0.5rem; +} + +.form-input, +.form-textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid var(--border-color); + border-radius: 6px; + background-color: var(--card-bg); + color: var(--text-color); + font-size: 1rem; + transition: all 0.2s ease; +} + +.form-input:focus, +.form-textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.theme-selector { + position: fixed; + top: 20px; + right: 20px; + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 1rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + z-index: 1000; +} + +.theme-selector h3 { + margin-bottom: 0.5rem; + color: var(--text-color); + font-size: 0.9rem; +} + +.theme-buttons { + display: flex; + gap: 0.5rem; +} + +.theme-btn { + width: 30px; + height: 30px; + border: 2px solid var(--border-color); + border-radius: 50%; + cursor: pointer; + transition: all 0.2s ease; +} + +.theme-btn.black-white { + background: linear-gradient(45deg, #000 50%, #fff 50%); +} + +.theme-btn.blue { + background: #3b82f6; +} + +.theme-btn.orange { + background: #ea580c; +} + +.theme-btn:hover { + transform: scale(1.1); + border-color: var(--primary-color); +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.badge { + display: inline-block; + padding: 0.25rem 0.75rem; + background-color: var(--primary-color); + color: white; + border-radius: 12px; + font-size: 0.8rem; + font-weight: 600; +} + +.text-accent { + color: var(--accent-color); +} + +.text-primary { + color: var(--primary-color); +} + +@media (max-width: 768px) { + .container { + padding: 1rem; + } + + .theme-selector { + position: relative; + top: auto; + right: auto; + margin-bottom: 2rem; + } + + .grid { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/static/js/agent-polling.js b/static/js/agent-polling.js new file mode 100644 index 0000000..3d78ad2 --- /dev/null +++ b/static/js/agent-polling.js @@ -0,0 +1,243 @@ +/** + * Reusable polling system for NetCop AI agents + * Handles async request polling with proper cleanup and error handling + */ + +class AgentPoller { + constructor(config) { + this.requestId = config.requestId; + this.statusUrl = config.statusUrl; + this.maxPolls = config.maxPolls || 30; + this.pollInterval = config.pollInterval || 1000; + this.onComplete = config.onComplete; + this.onError = config.onError; + this.onTimeout = config.onTimeout; + + // Internal state + this.pollCount = 0; + this.currentInterval = null; + this.isPolling = false; + this.resultsDisplayed = false; + } + + start() { + if (this.isPolling) { + console.warn('Poller is already running'); + return; + } + + this.isPolling = true; + this.pollCount = 0; + this.resultsDisplayed = false; + + // Clear any existing interval + this.stop(); + + this.currentInterval = setInterval(() => { + this.pollCount++; + + fetch(this.statusUrl) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return response.json(); + }) + .then(result => { + if (result.status === 'completed' || result.status === 'failed') { + this.stop(); + + // Display results only once + if (!this.resultsDisplayed) { + this.resultsDisplayed = true; + if (this.onComplete) { + this.onComplete(result); + } + } + } else if (this.pollCount >= this.maxPolls) { + this.stop(); + if (this.onTimeout) { + this.onTimeout(); + } + } + }) + .catch(error => { + console.error('Error polling results:', error); + this.stop(); + if (this.onError) { + this.onError(error); + } + }); + }, this.pollInterval); + } + + stop() { + if (this.currentInterval) { + clearInterval(this.currentInterval); + this.currentInterval = null; + } + this.isPolling = false; + } + + isRunning() { + return this.isPolling; + } +} + +/** + * Global polling manager to handle multiple pollers + */ +class PollingManager { + constructor() { + this.pollers = new Map(); + } + + createPoller(id, config) { + // Stop existing poller if any + this.stopPoller(id); + + const poller = new AgentPoller(config); + this.pollers.set(id, poller); + return poller; + } + + stopPoller(id) { + const poller = this.pollers.get(id); + if (poller) { + poller.stop(); + this.pollers.delete(id); + } + } + + stopAll() { + this.pollers.forEach(poller => poller.stop()); + this.pollers.clear(); + } +} + +// Global instance +window.pollingManager = new PollingManager(); + +/** + * Utility functions for common agent UI operations + */ +window.AgentUtils = { + // Update wallet balance display + updateWalletBalance(newBalance) { + const balanceElement = document.querySelector('[data-wallet-balance]') || + document.getElementById('walletBalance'); + if (balanceElement) { + balanceElement.textContent = `${newBalance.toFixed(2)} AED`; + } + window.currentWalletBalance = newBalance; + }, + + // Reset UI to initial state + resetUI(config) { + const elements = { + processingStatus: document.getElementById(config.processingStatusId || 'processingStatus'), + processButton: document.getElementById(config.processButtonId || 'processButton'), + results: document.getElementById(config.resultsId) + }; + + if (elements.processingStatus) { + elements.processingStatus.style.display = 'none'; + } + + if (elements.processButton) { + elements.processButton.disabled = false; + elements.processButton.innerHTML = config.buttonText || 'Process'; + } + + if (elements.results) { + elements.results.style.display = 'none'; + } + }, + + // Show processing status + showProcessing(config) { + const elements = { + processingStatus: document.getElementById(config.processingStatusId || 'processingStatus'), + processButton: document.getElementById(config.processButtonId || 'processButton'), + results: document.getElementById(config.resultsId) + }; + + if (elements.processingStatus) { + elements.processingStatus.style.display = 'block'; + } + + if (elements.processButton) { + elements.processButton.disabled = true; + elements.processButton.innerHTML = config.processingText || '⏳ Processing...'; + } + + if (elements.results) { + elements.results.style.display = 'none'; + } + }, + + // Show toast notification + showToast(message, type = 'info') { + // Prevent duplicate toasts + const existingToast = document.querySelector('.agent-toast'); + if (existingToast) { + existingToast.remove(); + } + + const toast = document.createElement('div'); + toast.className = 'agent-toast'; + toast.style.cssText = ` + position: fixed; + top: 16px; + right: 16px; + padding: 8px 12px; + border-radius: 4px; + color: white; + font-size: 13px; + z-index: 1000; + max-width: 300px; + font-weight: 500; + ${type === 'success' ? 'background: #10b981;' : 'background: #ef4444;'} + `; + toast.textContent = message; + document.body.appendChild(toast); + + setTimeout(() => { + if (toast.parentNode) { + toast.remove(); + } + }, 2000); + } +}; + +/** + * Progressive status steps for better UX + */ +window.StatusStepper = class { + constructor(steps, statusTextElementId, interval = 800) { + this.steps = steps; + this.statusTextElement = document.getElementById(statusTextElementId); + this.interval = interval; + this.currentStep = 0; + this.stepInterval = null; + } + + start() { + this.currentStep = 0; + this.stepInterval = setInterval(() => { + if (this.currentStep < this.steps.length && this.statusTextElement) { + this.statusTextElement.textContent = this.steps[this.currentStep]; + this.currentStep++; + } else { + this.stop(); + } + }, this.interval); + } + + stop() { + if (this.stepInterval) { + clearInterval(this.stepInterval); + this.stepInterval = null; + } + } +}; \ No newline at end of file diff --git a/weather_reporter/templates/weather_reporter/detail.html b/weather_reporter/templates/weather_reporter/detail.html index e8280a5..a9a92e7 100644 --- a/weather_reporter/templates/weather_reporter/detail.html +++ b/weather_reporter/templates/weather_reporter/detail.html @@ -476,8 +476,15 @@ return 'No weather data available'; } + // Track processing state to prevent duplicates + let isProcessing = false; + // Reset form for creating another report function resetForm() { + // Reset processing state + isProcessing = false; + + // Reset form and UI document.getElementById('weatherForm').reset(); document.getElementById('weatherResults').style.display = 'none'; document.getElementById('processingStatus').style.display = 'none'; @@ -593,6 +600,11 @@ return; } + // Prevent duplicate submissions + if (isProcessing) { + return; + } + // Check user authentication {% if not user.is_authenticated %} window.location.href = "{% url 'authentication:login' %}"; @@ -609,6 +621,9 @@ return; } + // Set processing state + isProcessing = true; + // Show processing status with steps document.getElementById('processingStatus').style.display = 'block'; document.getElementById('processButton').disabled = true; @@ -646,6 +661,10 @@ .then(response => response.json()) .then(result => { clearInterval(stepInterval); + + // Reset processing state + isProcessing = false; + // Handle immediate response (API-based agent) document.getElementById('processingStatus').style.display = 'none'; document.getElementById('processButton').disabled = false; @@ -662,6 +681,10 @@ .catch(error => { clearInterval(stepInterval); console.error('Error:', error); + + // Reset processing state + isProcessing = false; + document.getElementById('processingStatus').style.display = 'none'; document.getElementById('processButton').disabled = false; document.getElementById('processButton').innerHTML = '🌤️ Get Weather Report (2.00 AED)';