// Ensure Chart.js uses the right defaults for dark mode Chart.defaults.color = '#94A3B8'; Chart.defaults.borderColor = 'rgba(255, 255, 255, 0.1)'; // Global chart instances so we can destroy them before re-rendering let charts = {}; // API Configuration: Set this to your Hugging Face Space URL if deploying separately // For local development or combined deployment, leave it as an empty string const API_BASE_URL = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' ? '' : (window.BACKEND_URL || ''); document.addEventListener('DOMContentLoaded', () => { const form = document.getElementById('uploadForm'); const analyzeBtn = document.getElementById('analyzeBtn'); const btnText = analyzeBtn.querySelector('.btn-text'); const spinner = analyzeBtn.querySelector('.spinner'); const resultsArea = document.getElementById('resultsArea'); const inputSection = document.querySelector('.input-section'); // Tab switching logic const tabBtns = document.querySelectorAll('.tab-btn'); tabBtns.forEach(btn => { btn.addEventListener('click', () => { tabBtns.forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active')); btn.classList.add('active'); document.getElementById(btn.dataset.tab).classList.add('active'); }); }); // Example button logic const exampleBtns = document.querySelectorAll('.example-btn'); exampleBtns.forEach(btn => { btn.addEventListener('click', () => { const file = btn.dataset.file; const search = btn.dataset.search; // Populate search term document.getElementById('searchTerm').value = search; // Clear file input since we are using an example file document.getElementById('pdfFile').value = ''; // Submit form with example data submitAnalysis(null, search, file); }); }); form.addEventListener('submit', async (e) => { e.preventDefault(); const fileInput = document.getElementById('pdfFile'); const searchTerm = document.getElementById('searchTerm').value; if (!fileInput.files.length) { alert("Please upload a PDF file or choose an example."); return; } const file = fileInput.files[0]; submitAnalysis(file, searchTerm, null); }); async function submitAnalysis(file, searchTerm, exampleFile) { const formData = new FormData(); if (file) { formData.append('file', file); } else if (exampleFile) { formData.append('example_file', exampleFile); } formData.append('search_term', searchTerm); // UI Loading state analyzeBtn.disabled = true; btnText.textContent = 'Analyzing...'; spinner.classList.remove('hidden'); resultsArea.classList.add('hidden'); // Reset tabs to Summary tabBtns.forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active')); document.querySelector('[data-tab="summary"]').classList.add('active'); document.getElementById('summary').classList.add('active'); console.log("Starting analysis for:", { file: file?.name, searchTerm, exampleFile }); try { const response = await fetch(`${API_BASE_URL}/analyze`, { method: 'POST', body: formData }); const contentType = response.headers.get("content-type"); if (!response.ok) { if (contentType && contentType.includes("application/json")) { const errorData = await response.json(); throw new Error(errorData.detail || 'Analysis failed'); } else { const errorText = await response.text(); console.error("Backend Error (Non-JSON):", errorText); throw new Error(`Server Error (${response.status}). The backend might still be starting up or is misconfigured.`); } } if (!contentType || !contentType.includes("application/json")) { throw new Error("Invalid response from server. Expected JSON but received something else. Check if the Backend URL is correct."); } const data = await response.json(); console.log("Analysis data received:", data); try { renderResults(data); } catch (renderError) { console.error("Error in renderResults:", renderError); // Continue anyway to show the results area even if some charts fail } console.log("Transitioning UI: hiding input, showing results"); // Hide input section and show results inputSection.classList.add('hidden'); resultsArea.classList.remove('hidden'); window.scrollTo({ top: 0, behavior: 'smooth' }); } catch (error) { console.error("Analysis error:", error); alert('Error: ' + error.message); } finally { analyzeBtn.disabled = false; btnText.textContent = 'Analyze Manifesto'; spinner.classList.add('hidden'); } } }); // Show input form again (back button) function showInputForm() { document.querySelector('.input-section').classList.remove('hidden'); document.getElementById('resultsArea').classList.add('hidden'); window.scrollTo({ top: 0, behavior: 'smooth' }); } function renderResults(data) { // 1. Text Content (Markdown) document.getElementById('summaryContent').innerHTML = marked.parse(data.summary); document.getElementById('searchContent').innerHTML = marked.parse(data.search_result); // 2. Topics Grid const topicsContent = document.getElementById('topicsContent'); topicsContent.innerHTML = ''; // Sort topics by score const sortedTopics = Object.entries(data.topics) .filter(([key]) => key !== 'no_data' && key !== 'error' && key !== 'no_content' && key !== 'no_tokens') .sort((a, b) => b[1] - a[1]); sortedTopics.forEach(([topic, score]) => { const tag = document.createElement('div'); tag.className = 'topic-tag'; // Normalize score display const displayScore = (score * 100).toFixed(1); tag.innerHTML = `${topic}Relevance: ${displayScore}`; topicsContent.appendChild(tag); }); // Destroy existing charts Object.values(charts).forEach(chart => { try { chart.destroy(); } catch(e) {} }); charts = {}; // 3. Sentiment & Subjectivity Charts try { renderBarChart('sentimentChart', 'Polarity', data.sentiment.polarity, -1, 1, data.sentiment.polarity > 0 ? '#4CAF50' : data.sentiment.polarity < 0 ? '#F44336' : '#9E9E9E'); } catch (e) { console.error("Sentiment chart failed:", e); } try { renderBarChart('subjectivityChart', 'Subjectivity', data.sentiment.subjectivity, 0, 1, data.sentiment.subjectivity > 0.5 ? '#B667F1' : '#42A5F5'); } catch (e) { console.error("Subjectivity chart failed:", e); } // 4. Word Cloud try { renderWordCloud('wordCloudChart', data.word_cloud_freq); } catch (e) { console.error("Word cloud failed:", e); } // 5. Frequency Chart try { renderFrequencyChart('frequencyChart', sortedTopics); } catch (e) { console.error("Frequency chart failed:", e); } // 6. Dispersion Plot try { renderDispersionPlot('dispersionChart', data.dispersion, data.total_tokens); } catch (e) { console.error("Dispersion plot failed:", e); } } function renderBarChart(canvasId, label, value, min, max, color) { const ctx = document.getElementById(canvasId).getContext('2d'); charts[canvasId] = new Chart(ctx, { type: 'bar', data: { labels: [label], datasets: [{ label: 'Score', data: [value], backgroundColor: color, borderRadius: 5 }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { callbacks: { label: (ctx) => `Score: ${ctx.raw.toFixed(3)}` } } }, scales: { x: { min: min, max: max } } } }); } function renderWordCloud(canvasId, freqDict) { if (!freqDict || Object.keys(freqDict).length === 0) return; const ctx = document.getElementById(canvasId).getContext('2d'); // Format data for chartjs-wordcloud // Filter out any invalid entries and limit to top 50 for stability const filteredEntries = Object.entries(freqDict) .filter(([word, freq]) => word && freq > 0) .slice(0, 50); const words = filteredEntries.map(e => e[0]); const frequencies = filteredEntries.map(e => e[1]); if (words.length === 0) return; // Scale frequencies for better sizing const maxFreq = Math.max(...frequencies); const scaledFrequencies = frequencies.map(f => (f / maxFreq) * 40 + 10); // Min 10px, Max 50px charts[canvasId] = new Chart(ctx, { type: 'wordCloud', data: { labels: words, datasets: [{ label: 'Word Cloud', data: scaledFrequencies, color: () => `hsl(${Math.random() * 360}, 70%, 60%)` // Random vibrant colors }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } } }); } function renderFrequencyChart(canvasId, sortedTopics) { const ctx = document.getElementById(canvasId).getContext('2d'); // sortedTopics is an array of [word, score] const words = sortedTopics.slice(0, 15).map(item => item[0]); const scores = sortedTopics.slice(0, 15).map(item => item[1]); charts[canvasId] = new Chart(ctx, { type: 'bar', data: { labels: words, datasets: [{ label: 'Relevance Score', data: scores, backgroundColor: '#4F46E5', borderRadius: 4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true } } } }); } function renderDispersionPlot(canvasId, dispersionData, totalTokens) { const ctx = document.getElementById(canvasId).getContext('2d'); const datasets = []; const words = Object.keys(dispersionData); const colors = ['#4F46E5', '#F59E0B', '#10B981', '#EC4899', '#8B5CF6']; words.forEach((word, index) => { // Create scatter points const points = dispersionData[word].map(offset => ({ x: offset, y: index + 1 // Offset Y by word index })); datasets.push({ label: word, data: points, backgroundColor: colors[index % colors.length], pointRadius: 3, pointHoverRadius: 5, pointStyle: 'rect' // Use small rectangles like a barcode }); }); charts[canvasId] = new Chart(ctx, { type: 'scatter', data: { datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { tooltip: { callbacks: { label: (ctx) => `Word: ${ctx.dataset.label}, Position: ${ctx.raw.x}` } } }, scales: { x: { title: { display: true, text: 'Position in Text' }, min: 0, max: totalTokens > 0 ? totalTokens : undefined }, y: { title: { display: false }, min: 0, max: words.length + 1, ticks: { stepSize: 1, callback: function(value) { if (value > 0 && value <= words.length) { return words[value - 1]; } return ''; } } } } } }); }