From 8cc9bb0609a40bab4ef92f18f9858b463dab2795 Mon Sep 17 00:00:00 2001
From: Claude Basic Information
-
-
+ Job Requirements
-
-
+ Generated Job Posting
- Generated Job Posting
+ $1
')
+ .replace(/^## (.*$)/gm, '$1
')
+ .replace(/^# (.*$)/gm, '$1
')
+ // Bold
+ .replace(/\*\*(.*?)\*\*/g, '$1')
+ // Italic
+ .replace(/\*(.*?)\*/g, '$1')
+ // Lists
+ .replace(/^- (.*$)/gm, '$1
')
+ // Line breaks
+ .replace(/\n\n/g, '
')
+ .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)';