Spaces:
Running
Running
| <script> const mobileMenuBtn = document.getElementById('mobileMenuBtn'); const sidebar = document.getElementById('sidebar'); const workerCountInput = document.getElementById('workerCount'); const workerCountDisplay = document.getElementById('workerCountDisplay'); const uploadBtn = document.getElementById('uploadBtn'); const uploadModal = document.getElementById('uploadModal'); const closeUploadModalBtn = document.getElementById('closeUploadModal'); const fileUploadInput = document.getElementById('fileUpload'); const fileNameDisplay = document.getElementById('fileNameDisplay'); const confirmUploadBtn = document.getElementById('confirmUpload'); const uploadProgress = document.getElementById('uploadProgress'); const startAnalysisBtn = document.getElementById('startAnalysis'); const lastRunTimeSpan = document.getElementById('lastRunTime'); const completedCountSpan = document.getElementById('completedCount'); const errorCountSpan = document.getElementById('errorCount'); const processingCountSpan = document.getElementById('processingCount'); const progressPercentSpan = document.getElementById('progressPercent'); const progressBarDiv = document.getElementById('progressBar'); const sentimentCountSpan = document.getElementById('sentimentCount'); const topicCountSpan = document.getElementById('topicCount'); const progressCountSpan = document.getElementById('progressCount'); const summaryCountSpan = document.getElementById('summaryCount'); const sentimentStatusSpan = document.getElementById('sentimentStatus'); const topicStatusSpan = document.getElementById('topicStatus'); const progressStatusSpan = document.getElementById('progressStatus'); const summaryStatusSpan = document.getElementById('summaryStatus'); const resultsContainer = document.getElementById('conversationResults'); const showingCountSpan = document.getElementById('showingCount'); const totalCountSpan = document.getElementById('totalCount'); const analysisModal = document.getElementById('analysisModal'); const closeModalBtn = document.getElementById('closeModal'); const modalSummary = document.getElementById('modalSummary'); const successIndicatorsDiv = document.getElementById('successIndicators'); const struggleIndicatorsDiv = document.getElementById('struggleIndicators'); const mainTopicsDiv = document.getElementById('mainTopics'); const sentimentTrendSpan = document.getElementById('sentimentTrend'); const sentimentTrendIcon = sentimentTrendSpan.previousElementSibling.querySelector('i'); // Assuming the icon is the previous sibling const keyLearningPara = document.getElementById('keyLearning'); const downloadBtn = document.getElementById('downloadBtn'); const pauseBtn = document.getElementById('pauseBtn'); const stopBtn = document.getElementById('stopBtn'); const modelSelect = document.getElementById('modelSelect'); let statusInterval = null; // To hold the interval for status polling // Initial state update updateStatusDisplay(); // Call once on load // Mobile menu toggle mobileMenuBtn.addEventListener('click', function() { sidebar.classList.toggle('sidebar-open'); }); // Close sidebar on click outside (simple example, might need refinement) document.addEventListener('click', function(event) { if (!sidebar.contains(event.target) && !mobileMenuBtn.contains(event.target) && sidebar.classList.contains('sidebar-open') && window.innerWidth <= 768) { sidebar.classList.remove('sidebar-open'); } }); // Update worker count display (still useful for UI config) workerCountInput.addEventListener('input', function() { // Use input for live updates workerCountDisplay.textContent = this.value; // Note: Changing this *during* analysis won't change the actual worker count // A real app would need to signal the backend to resize the pool }); // File upload modal uploadBtn.addEventListener('click', function() { uploadModal.classList.remove('hidden'); }); closeUploadModalBtn.addEventListener('click', function() { uploadModal.classList.add('hidden'); resetUploadModal(); // Reset modal state on close }); // Reset upload modal UI function resetUploadModal() { fileUploadInput.value = ''; // Clear selected file fileNameDisplay.textContent = 'None'; confirmUploadBtn.disabled = true; uploadProgress.style.width = '0%'; } // File upload handling fileUploadInput.addEventListener('change', function(e) { const file = e.target.files[0]; if (file) { fileNameDisplay.textContent = file.name; confirmUploadBtn.disabled = false; } else { resetUploadModal(); } }); // Confirm Upload button (sends file to backend) confirmUploadBtn.addEventListener('click', async function() { const file = fileUploadInput.files[0]; if (!file) return; const formData = new FormData(); formData.append('file', file); // Simulate progress visually for the upload itself (optional, backend handles processing) // We won't do a real upload progress bar here as it's complex uploadProgress.style.width = '0%'; confirmUploadBtn.disabled = true; confirmUploadBtn.textContent = 'Uploading...'; try { const response = await fetch('/upload', { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Upload failed'); } const result = await response.json(); console.log('Upload successful:', result); uploadProgress.style.width = '100%'; alert(result.message); // Update total count based on files extracted totalCountSpan.textContent = result.file_count; showingCountSpan.textContent = 0; // No results shown yet // Reset analysis progress UI completedCountSpan.textContent = '0'; errorCountSpan.textContent = '0'; processingCountSpan.textContent = '0'; progressPercentSpan.textContent = '0'; progressBarDiv.style.width = '0%'; sentimentCountSpan.textContent = '0'; topicCountSpan.textContent = '0'; progressCountSpan.textContent = '0'; summaryCountSpan.textContent = '0'; // Clear previous results from display resultsContainer.innerHTML = ` <div class="text-center py-10 text-gray-400"> <i class="fas fa-comments text-4xl mb-2"></i> <p>Files uploaded. Start analysis to see results here.</p> </div> `; // Update agent statuses to 'Ready' or 'Idle' after upload updateAgentStatuses("Ready"); setTimeout(() => { uploadModal.classList.add('hidden'); resetUploadModal(); }, 500); // Give user a moment to read alert } catch (error) { console.error('Upload error:', error); alert('Upload failed: ' + error.message); uploadProgress.style.width = '0%'; // Reset progress on error confirmUploadBtn.disabled = false; confirmUploadBtn.textContent = 'Upload and Extract'; updateAgentStatuses("Idle"); // Stay idle on error } }); // Function to update agent status text and color function updateAgentStatuses(status) { const statusMapping = { "Idle": { text: "Idle", color: "gray" }, "Ready": { text: "Ready", color: "blue" }, // New status after upload "Active": { text: "Active", color: "blue" }, "Completed": { text: "Completed", color: "green" }, "Error": { text: "Error", color: "red" } // For individual agent errors }; const s = statusMapping[status] || statusMapping["Idle"]; sentimentStatusSpan.textContent = s.text; sentimentStatusSpan.className = `text-sm font-medium text-${s.color}-600`; topicStatusSpan.textContent = s.text; topicStatusSpan.className = `text-sm font-medium text-${s.color}-600`; progressStatusSpan.textContent = s.text; progressStatusSpan.className = `text-sm font-medium text-${s.color}-600`; summaryStatusSpan.textContent = s.text; summaryStatusSpan.className = `text-sm font-medium text-${s.color}-600`; } // Start Analysis button startAnalysisBtn.addEventListener('click', async function() { const model = modelSelect.value; // Disable button and indicate starting startAnalysisBtn.disabled = true; startAnalysisBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <span>Starting...</span>'; startAnalysisBtn.classList.add('opacity-50', 'cursor-not-allowed'); try { const response = await fetch('/start_analysis', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ model: model }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to start analysis'); } const result = await response.json(); console.log('Analysis start message:', result.message); // Update UI immediately to reflect starting lastRunTimeSpan.textContent = new Date().toLocaleString(); updateAgentStatuses("Active"); // Polling will update counts and progress // Start polling for status and results if (statusInterval) clearInterval(statusInterval); // Clear any existing interval statusInterval = setInterval(updateStatusDisplay, 2000); // Poll every 2 seconds // Enable pause/stop buttons pauseBtn.disabled = false; stopBtn.disabled = false; } catch (error) { console.error('Error starting analysis:', error); alert('Failed to start analysis: ' + error.message); // Re-enable button and reset text startAnalysisBtn.disabled = false; startAnalysisBtn.innerHTML = '<i class="fas fa-play"></i> <span>Start Analysis</span>'; startAnalysisBtn.classList.remove('opacity-50', 'cursor-not-allowed'); updateAgentStatuses("Idle"); } }); // Function to poll backend status and update UI async function updateStatusDisplay() { try { const statusResponse = await fetch('/status'); const status = await statusResponse.json(); completedCountSpan.textContent = status.completed_files; errorCountSpan.textContent = status.error_files; processingCountSpan.textContent = status.processing_files_count; progressPercentSpan.textContent = status.progress_percent; progressBarDiv.style.width = status.progress_percent + '%'; lastRunTimeSpan.textContent = status.last_run_time; totalCountSpan.textContent = status.total_files; // Ensure total is correct // Update agent counts sentimentCountSpan.textContent = status.agent_counts.sentiment; topicCountSpan.textContent = status.agent_counts.topic; progressCountSpan.textContent = status.agent_counts.progress; summaryCountSpan.textContent = status.agent_counts.summary; // Update agent statuses based on overall status updateAgentStatuses(status.status); // Uses the main status from backend // Update Start button based on status if (status.status === "Running") { startAnalysisBtn.disabled = true; startAnalysisBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <span>Analyzing...</span>'; startAnalysisBtn.classList.add('opacity-50', 'cursor-not-allowed'); startAnalysisBtn.classList.remove('bg-white', 'text-purple-700', 'hover:bg-purple-50'); startAnalysisBtn.classList.add('bg-gray-200', 'text-gray-700'); // Indicate disabled running state } else if (status.status === "Completed") { if (statusInterval) clearInterval(statusInterval); // Stop polling on completion startAnalysisBtn.disabled = false; startAnalysisBtn.innerHTML = '<i class="fas fa-check"></i> <span>Analysis Complete</span>'; startAnalysisBtn.classList.remove('opacity-50', 'cursor-not-allowed', 'bg-gray-200', 'text-gray-700', 'bg-white', 'text-purple-700', 'hover:bg-purple-50'); startAnalysisBtn.classList.add('bg-green-100', 'text-green-700', 'hover:bg-green-200'); // Indicate success // Disable pause/stop on completion pauseBtn.disabled = true; stopBtn.disabled = true; } else if (status.status === "Idle" || status.status === "Ready") { startAnalysisBtn.disabled = status.total_files === 0; // Disable if no files startAnalysisBtn.innerHTML = '<i class="fas fa-play"></i> <span>Start Analysis</span>'; startAnalysisBtn.classList.remove('opacity-50', 'cursor-not-allowed', 'bg-green-100', 'text-green-700', 'hover:bg-green-200', 'bg-gray-200', 'text-gray-700'); startAnalysisBtn.classList.add('bg-white', 'text-purple-700', 'hover:bg-purple-50'); // Ensure buttons are disabled if not running if (status.status === "Idle") { // Truly idle, before upload pauseBtn.disabled = true; stopBtn.disabled = true; } else { // Ready after upload pauseBtn.disabled = true; // Can't pause before starting stopBtn.disabled = false; // Can 'stop' to clear files } } // Fetch and display results list if analysis is running or completed if (status.status === "Running" || status.status === "Completed") { await updateResultsList(); // Charts would be updated here based on aggregated data from /status or a new endpoint } } catch (error) { console.error('Error fetching status:', error); // If status fetch fails repeatedly, stop polling and indicate error // For simplicity, we just log here. } } // Function to fetch and display the list of results async function updateResultsList() { try { const resultsResponse = await fetch('/results'); const results = await resultsResponse.json(); // Clear existing results, but keep the placeholder if no results yet if (results.length > 0) { const emptyMessage = resultsContainer.querySelector('.text-center.py-10.text-gray-400'); if (emptyMessage) { emptyMessage.remove(); } resultsContainer.innerHTML = ''; // Clear only if there are results to show results.forEach(result => { const card = document.createElement('div'); card.className = 'conversation-card bg-white border border-gray-200 rounded-lg overflow-hidden shadow-sm transition cursor-pointer fade-in'; // Determine sentiment class for badge let sentimentClass = 'bg-gray-100 text-gray-800'; if (result.sentiment.toLowerCase() === 'positive') { sentimentClass = 'bg-green-100 text-green-800'; } else if (result.sentiment.toLowerCase() === 'negative') { sentimentClass = 'bg-red-100 text-red-800'; } card.innerHTML = ` <div class="p-4 border-b border-gray-200"> <div class="flex justify-between items-start"> <div> <h4 class="font-medium text-gray-900 truncate">${result.filename}</h4> <p class="text-xs text-gray-500 mt-1">${result.date || 'N/A'}</p> </div> <span class="px-2 py-1 rounded-full text-xs font-medium ${sentimentClass}"> ${result.sentiment || 'N/A'} </span> </div> </div> <div class="p-4"> <p class="text-sm text-gray-600 line-clamp-2">${result.summary_snippet || 'No summary snippet available.'}</p> <div class="mt-3 flex flex-wrap gap-1"> ${(result.topics || []).map(topic => `<span class="inline-block bg-gray-100 px-2 py-1 rounded-md text-xs">${topic}</span>` ).join('') || '<span class="inline-block text-xs text-gray-500">No topics identified.</span>'} </div> </div> <div class="px-4 py-2 bg-gray-50 border-t border-gray-200 flex justify-between items-center"> <span class="text-xs text-gray-500">${result.exchanges || 'N/A'} exchanges</span> ${result.has_details ? ` <button class="text-blue-600 hover:text-blue-800 text-sm font-medium" data-filename="${result.filename}"> View Details </button> ` : `<span class="text-xs text-red-500">Analysis Failed</span>`} </div> `; // Add click handler to show modal (on button or card) if(result.has_details) { const detailButton = card.querySelector('button'); detailButton.addEventListener('click', function(e) { e.stopPropagation(); // Prevent card click from triggering showAnalysisModal(detailButton.dataset.filename); }); card.addEventListener('click', function() { showAnalysisModal(result.filename); }); } resultsContainer.appendChild(card); // Use appendChild to add in order }); showingCountSpan.textContent = results.length; // Update showing count // totalCountSpan should already be set by upload } else if (totalCountSpan.textContent > 0) { // Files uploaded, but no results yet (analysis not started or still processing first files) const emptyMessage = resultsContainer.querySelector('.text-center.py-10.text-gray-400'); if (!emptyMessage) { resultsContainer.innerHTML = ` <div class="text-center py-10 text-gray-400"> <i class="fas fa-comments text-4xl mb-2"></i> <p>Analysis in progress or no results yet. Results will appear here.</p> </div> `; } showingCountSpan.textContent = 0; } else { // No files uploaded const emptyMessage = resultsContainer.querySelector('.text-center.py-10.text-gray-400'); if (!emptyMessage) { resultsContainer.innerHTML = ` <div class="text-center py-10 text-gray-400"> <i class="fas fa-comments text-4xl mb-2"></i> <p>No analysis results yet. Start the analysis to see results here.</p> </div> `; } showingCountSpan.textContent = 0; } } catch (error) { console.error('Error fetching results list:', error); resultsContainer.innerHTML = ` <div class="text-center py-10 text-red-400"> <i class="fas fa-exclamation-circle text-4xl mb-2"></i> <p>Error loading results.</p> </div> `; } } // Show analysis modal with details fetched from backend async function showAnalysisModal(filename) { try { // Clear previous modal content and show loading modalSummary.textContent = 'Loading...'; successIndicatorsDiv.innerHTML = '<p class="text-gray-500">Loading...</p>'; struggleIndicatorsDiv.innerHTML = '<p class="text-gray-500">Loading...</p>'; mainTopicsDiv.innerHTML = '<p class="text-gray-500">Loading...</p>'; sentimentTrendSpan.textContent = 'Loading...'; sentimentTrendIcon.className = 'fas fa-spinner fa-spin text-gray-500 text-sm'; // Loading icon keyLearningPara.textContent = 'Loading...'; const response = await fetch(`/analyze/${encodeURIComponent(filename)}`); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to load analysis details'); } const details = await response.json(); console.log('Analysis details:', details); // Populate modal content modalSummary.textContent = details.summary || 'No summary available.'; // Success Indicators successIndicatorsDiv.innerHTML = ''; if (details.success_indicators && details.success_indicators.length > 0) { details.success_indicators.forEach(indicator => { const div = document.createElement('div'); div.className = 'bg-green-50 border border-green-100 p-3 rounded-lg'; div.innerHTML = `<p class="text-sm text-green-800">${indicator}</p>`; // Assuming indicator is the text itself successIndicatorsDiv.appendChild(div); }); } else { successIndicatorsDiv.innerHTML = '<p class="text-gray-500 text-sm">No specific success indicators identified.</p>'; } // Struggle Indicators struggleIndicatorsDiv.innerHTML = ''; if (details.struggle_indicators && details.struggle_indicators.length > 0) { details.struggle_indicators.forEach(indicator => { const div = document.createElement('div'); div.className = 'bg-red-50 border border-red-100 p-3 rounded-lg'; div.innerHTML = `<p class="text-sm text-red-800">${indicator}</p>`; // Assuming indicator is the text itself struggleIndicatorsDiv.appendChild(div); }); } else { struggleIndicatorsDiv.innerHTML = '<p class="text-gray-500 text-sm">No specific struggle indicators identified.</p>'; } // Main Topics mainTopicsDiv.innerHTML = ''; if (details.topics && details.topics.length > 0) { details.topics.forEach(topic => { const span = document.createElement('span'); span.className = 'inline-block bg-white px-2 py-1 rounded-md text-xs mr-1 mb-1'; span.textContent = topic; mainTopicsDiv.appendChild(span); }); } else { mainTopicsDiv.innerHTML = '<p class="text-gray-500 text-sm">No main topics identified.</p>'; } // Sentiment Trend (simplified mapping from main sentiment) const sentiment = details.sentiment?.label || 'N/A'; let sentimentTrendText = 'N/A'; let sentimentIconClass = 'fas fa-question text-gray-500'; let sentimentColorClass = 'text-gray-600'; if (sentiment.toLowerCase() === 'positive') { sentimentTrendText = 'Mostly Positive'; sentimentIconClass = 'fas fa-smile text-green-600'; sentimentColorClass = 'text-green-600'; } else if (sentiment.toLowerCase() === 'negative') { sentimentTrendText = 'Mostly Negative'; sentimentIconClass = 'fas fa-frown text-red-600'; sentimentColorClass = 'text-red-600'; } else if (sentiment.toLowerCase() === 'neutral') { sentimentTrendText = 'Neutral'; sentimentIconClass = 'fas fa-meh text-gray-500'; sentimentColorClass = 'text-gray-600'; } sentimentTrendSpan.textContent = sentimentTrendText; sentimentTrendSpan.className = `text-sm font-medium ${sentimentColorClass}`; sentimentTrendIcon.className = sentimentIconClass + ' text-sm'; // Key Learning keyLearningPara.textContent = details.key_learning || 'No key learning identified.'; // Store filename for download button in modal (if needed) // The current download button downloads ALL results, you'd need to add a new endpoint for single file download // Show modal analysisModal.classList.remove('hidden'); } catch (error) { console.error('Error fetching analysis details:', error); // Display error in modal if fetch fails modalSummary.textContent = `Error loading details: ${error.message}`; successIndicatorsDiv.innerHTML = ''; struggleIndicatorsDiv.innerHTML = ''; mainTopicsDiv.innerHTML = ''; sentimentTrendSpan.textContent = 'Error'; sentimentTrendIcon.className = 'fas fa-times-circle text-red-600 text-sm'; keyLearningPara.textContent = 'Could not load detailed analysis.'; analysisModal.classList.remove('hidden'); } } // Close modal closeModalBtn.addEventListener('click', function() { analysisModal.classList.add('hidden'); }); // Download results (calls backend endpoint) downloadBtn.addEventListener('click', function() { // The backend handles generating and sending the zip file window.location.href = '/download_results'; // This will trigger the download in the browser // Optional: Add visual feedback like a spinner or message alert('Generating and downloading results...'); // Simple feedback }); // Pause/stop buttons (call backend endpoints - functionality depends on backend implementation) pauseBtn.addEventListener('click', async function() { try { const response = await fetch('/pause_analysis', { method: 'POST' }); const result = await response.json(); console.log(result.message); alert(result.message); // UI might need updating to show paused state (backend status should handle this) } catch (error) { console.error('Error pausing analysis:', error); alert('Failed to pause analysis: ' + error.message); } }); stopBtn.addEventListener('click', async function() { const confirmStop = confirm("Are you sure you want to stop the analysis? Progress will be saved."); if (!confirmStop) return; try { const response = await fetch('/stop_analysis', { method: 'POST' }); const result = await response.json(); console.log(result.message); alert(result.message); // UI needs to update to indicate stopped state and potentially stop polling if (statusInterval) clearInterval(statusInterval); updateStatusDisplay(); // Final status update } catch (error) { console.error('Error stopping analysis:', error); alert('Failed to stop analysis: ' + error.message); } }); // Initial call to update status display on page load // This fetches the state in case the app was restarted or already had files document.addEventListener('DOMContentLoaded', () => { updateStatusDisplay(); }); // Helper function to update the chart areas with simple text/lists // This replaces the static HTML visualization simulation function updateVisualizations() { // This function is called by updateStatusDisplay when analysis is completed (or periodically) // Fetch aggregate data from the backend or use data from state.get_status() + results const status = state.get_status(); // Access state directly on frontend (not ideal) or fetch aggregate data // --- Sentiment Distribution (Simple Text Summary) --- const sentimentChartDiv = document.getElementById('sentimentChart'); const resultsList = state.get_results_list(); // Or fetch /results and process const sentimentCounts = resultsList.reduce((acc, res) => { const sentiment = res.sentiment || 'N/A'; acc[sentiment] = (acc[sentiment] || 0) + 1; return acc; }, {}); let sentimentHtml = '<h4 class="font-medium text-gray-900 mb-3">Sentiment Distribution</h4>'; if (resultsList.length === 0) { sentimentHtml += `<div class="text-center py-10 text-gray-400"> <i class="fas fa-chart-pie text-2xl mb-2"></i> <p class="text-sm">No sentiment data available yet</p> </div>`; } else { sentimentHtml += '<ul class="list-disc list-inside text-sm text-gray-700">'; for (const [sentiment, count] of Object.entries(sentimentCounts)) { let colorClass = 'text-gray-600'; if (sentiment.toLowerCase() === 'positive') colorClass = 'text-green-600'; else if (sentiment.toLowerCase() === 'negative') colorClass = 'text-red-600'; sentimentHtml += `<li class="${colorClass}">${sentiment}: ${count}</li>`; } sentimentHtml += '</ul>'; } sentimentChartDiv.innerHTML = sentimentHtml; // --- Top Topics (Simple List) --- const topicChartDiv = document.getElementById('topicChart'); const allTopics = resultsList.flatMap(res => res.topics || []).filter(topic => topic !== 'Topic extraction pipeline failed to load.'); const topicCounts = allTopics.reduce((acc, topic) => { acc[topic] = (acc[topic] || 0) + 1; return acc; }, {}); const sortedTopics = Object.entries(topicCounts).sort(([, a], [, b]) => b - a); let topicHtml = '<h4 class="font-medium text-gray-900 mb-3">Top Topics</h4>'; if (sortedTopics.length === 0) { topicHtml += `<div class="text-center py-10 text-gray-400"> <i class="fas fa-chart-bar text-2xl mb-2"></i> <p class="text-sm">No topic data available yet</p> </div>`; } else { topicHtml += '<ul class="list-disc list-inside text-sm text-gray-700">'; sortedTopics.slice(0, 5).forEach(([topic, count]) => { // Show top 5 topicHtml += `<li>${topic}: ${count}</li>`; }); topicHtml += '</ul>'; } topicChartDiv.innerHTML = topicHtml; } // Call update visualizations after results are updated, especially on completion // The updateStatusDisplay could call updateVisualizations periodically or on completion // For now, let's add a manual call example or call it when analysis is detected as completed. // A better approach is to fetch aggregate data from a backend endpoint dedicated to charts. // For this simplified version, let's update charts only when analysis completes. // (Modified the updateStatusDisplay to call updateVisualizations on "Completed") </script> |