manifesto-explainer / script.js
samarth70
Fix Groq completion token limit, replace decommissioned model, improve Gemini error logging, add gitignore
f07ec3c
// 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 = `<span class="topic-name">${topic}</span><span class="topic-score">Relevance: ${displayScore}</span>`;
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 '';
}
}
}
}
}
});
}