// Application State
const AppState = {
currentPdf: null,
pdfs: [],
deviceInfo: null,
};
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
initializeTheme();
loadDeviceInfo();
initializeEventListeners();
loadPdfList();
});
// Theme Toggle
function initializeTheme() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.body.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
document.getElementById('themeToggle').addEventListener('click', function() {
const currentTheme = document.body.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.body.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme);
});
}
function updateThemeIcon(theme) {
const icon = document.getElementById('themeIcon');
if (icon) {
icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
}
}
// Load Device Info
async function loadDeviceInfo() {
try {
const response = await fetch('/api/device-info');
const data = await response.json();
AppState.deviceInfo = data;
updateDeviceStatus(data);
} catch (error) {
console.error('Error loading device info:', error);
updateDeviceStatus({ device: 'unknown', cuda_available: false });
}
}
function updateDeviceStatus(info) {
const badge = document.getElementById('deviceBadge');
const deviceName = document.getElementById('deviceName');
if (info.cuda_available) {
badge.textContent = 'GPU';
badge.className = 'badge bg-success';
deviceName.textContent = info.device_name || 'CUDA Device';
} else {
badge.textContent = 'CPU';
badge.className = 'badge bg-secondary';
deviceName.textContent = 'CPU Processing';
}
}
// Event Listeners
function initializeEventListeners() {
const uploadForm = document.getElementById('uploadForm');
uploadForm.addEventListener('submit', handleUpload);
}
// Handle File Upload
async function handleUpload(e) {
e.preventDefault();
const fileInput = document.getElementById('fileInput');
const files = fileInput.files;
if (files.length === 0) {
alert('Please select at least one PDF file');
return;
}
const extractionMode = document.querySelector('input[name="extractionMode"]:checked').value;
// Show processing section
document.getElementById('processingSection').style.display = 'block';
document.getElementById('resultsSection').style.display = 'none';
document.getElementById('emptyState').style.display = 'none';
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('files[]', files[i]);
}
formData.append('extraction_mode', extractionMode);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
// Hide processing section
document.getElementById('processingSection').style.display = 'none';
// Reload PDF list and show results
await loadPdfList();
// Show first PDF details if available
if (data.results && data.results.length > 0) {
const firstPdf = data.results[0];
if (!firstPdf.error) {
showPdfDetails(firstPdf.stem);
}
}
// Reset form
fileInput.value = '';
} catch (error) {
console.error('Upload error:', error);
alert('Error processing files: ' + error.message);
document.getElementById('processingSection').style.display = 'none';
}
}
// Load PDF List
async function loadPdfList() {
try {
const response = await fetch('/api/pdf-list');
const data = await response.json();
AppState.pdfs = data.pdfs || [];
renderPdfList();
if (AppState.pdfs.length > 0) {
document.getElementById('resultsSection').style.display = 'block';
document.getElementById('emptyState').style.display = 'none';
} else {
document.getElementById('resultsSection').style.display = 'none';
document.getElementById('emptyState').style.display = 'block';
}
} catch (error) {
console.error('Error loading PDF list:', error);
}
}
// Render PDF List
function renderPdfList() {
const pdfList = document.getElementById('pdfList');
pdfList.innerHTML = '';
if (AppState.pdfs.length === 0) {
pdfList.innerHTML = '
No PDFs processed yet
';
return;
}
AppState.pdfs.forEach((pdf, index) => {
const item = document.createElement('div');
item.className = `list-group-item d-flex align-items-center justify-content-between ${index === 0 && !AppState.currentPdf ? 'active' : ''} ${AppState.currentPdf === pdf.stem ? 'active' : ''}`;
const left = document.createElement('a');
left.href = '#';
left.className = 'flex-grow-1 text-decoration-none text-reset';
left.innerHTML = `
${pdf.stem}
`;
left.addEventListener('click', function(e) {
e.preventDefault();
// Update active state
document.querySelectorAll('#pdfList .list-group-item').forEach(i => i.classList.remove('active'));
item.classList.add('active');
showPdfDetails(pdf.stem);
});
const delBtn = document.createElement('button');
delBtn.className = 'btn btn-sm btn-outline-danger ms-3';
delBtn.innerHTML = '';
delBtn.title = `Delete "${pdf.stem}"`;
delBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const confirmed = confirm(`Delete processed outputs for "${pdf.stem}"? This cannot be undone.`);
if (!confirmed) return;
try {
// Use form-encoded POST to the body endpoint for widest compatibility
const resp = await fetch('/api/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
body: new URLSearchParams({ stem: pdf.stem }).toString()
});
const raw = await resp.text();
let res;
try { res = JSON.parse(raw); } catch (_) { res = null; }
if (!resp.ok || (res && res?.error)) {
throw new Error((res && res?.error) || raw || 'Delete failed');
}
// Refresh list and clear details if we deleted the active item
if (AppState.currentPdf === pdf.stem) {
AppState.currentPdf = null;
const details = document.getElementById('pdfDetails');
if (details) {
details.innerHTML = `
Deleted "${pdf.stem}" successfully.
`;
}
}
await loadPdfList();
} catch (err) {
console.error('Delete error:', err);
alert('Failed to delete: ' + (err?.message || err));
}
});
item.appendChild(left);
item.appendChild(delBtn);
pdfList.appendChild(item);
});
}
// Show PDF Details
async function showPdfDetails(pdfStem) {
AppState.currentPdf = pdfStem;
// Update active state in list
document.querySelectorAll('#pdfList .list-group-item').forEach((item, index) => {
item.classList.remove('active');
const pdfStemFromItem = AppState.pdfs[index]?.stem;
if (pdfStemFromItem === pdfStem) {
item.classList.add('active');
}
});
try {
const response = await fetch(`/api/pdf-details/${encodeURIComponent(pdfStem)}`);
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
renderPdfDetails(data);
} catch (error) {
console.error('Error loading PDF details:', error);
document.getElementById('pdfDetails').innerHTML = `
Error loading PDF details: ${error.message}
`;
}
}
// Render PDF Details
function renderPdfDetails(data) {
const container = document.getElementById('pdfDetails');
let html = `
${data.figures_count || 0}
Figures
${data.tables_count || 0}
Tables
${data.elements_count || 0}
Total Elements
${AppState.deviceInfo?.device === 'cuda' ? 'GPU' : 'CPU'}
Device
`;
// Figures Section
if (data.figure_images && data.figure_images.length > 0) {
html += `
`;
data.figure_images.forEach((imgPath, index) => {
const figure = data.figures[index] || {};
html += `
Figure ${index + 1}
${figure.page ? `
Page ${figure.page}` : ''}
`;
});
html += `
`;
}
// Tables Section
if (data.table_images && data.table_images.length > 0) {
html += `
`;
data.table_images.forEach((imgPath, index) => {
const table = data.tables[index] || {};
html += `
Table ${index + 1}
${table.page ? `
Page ${table.page}` : ''}
`;
});
html += `
`;
}
// Markdown Preview
if (data.markdown_path) {
html += `
`;
}
container.innerHTML = html;
// Load markdown preview if available
if (data.markdown_path) {
loadMarkdownPreview(data.markdown_path);
}
// Wire delete button
const deleteBtn = document.getElementById('deletePdfBtn');
if (deleteBtn) {
deleteBtn.addEventListener('click', async () => {
const confirmed = confirm(`Delete processed outputs for "${data.stem}"? This cannot be undone.`);
if (!confirmed) return;
try {
// Use form-encoded POST to the body endpoint for widest compatibility
const resp = await fetch('/api/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
body: new URLSearchParams({ stem: data.stem }).toString()
});
const raw = await resp.text();
let res;
try { res = JSON.parse(raw); } catch (_) { res = null; }
if (!resp.ok || (res && res.error)) {
throw new Error((res && res.error) || raw || 'Delete failed');
}
// Refresh list and clear details
await loadPdfList();
document.getElementById('pdfDetails').innerHTML = `
Deleted "${data.stem}" successfully.
`;
AppState.currentPdf = null;
} catch (err) {
console.error('Delete error:', err);
alert('Failed to delete: ' + (err?.message || err));
}
});
}
}
// Load Markdown Preview
async function loadMarkdownPreview(markdownPath) {
try {
const response = await fetch(`/output/${markdownPath}`);
const text = await response.text();
document.getElementById('markdownPreview').textContent = text;
} catch (error) {
console.error('Error loading markdown:', error);
document.getElementById('markdownPreview').textContent = 'Error loading markdown content';
}
}