/* ═══════════════════════════════════════════════════
LexiSight — Frontend Script
═══════════════════════════════════════════════════ */
(() => {
// ── DOM refs ──
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('fileInput');
const browseBtn = document.getElementById('browseBtn');
const dropInner = document.getElementById('dropInner');
const previewWrap = document.getElementById('previewWrap');
const previewImg = document.getElementById('previewImg');
const previewMeta = document.getElementById('previewMeta');
const removeBtn = document.getElementById('removeBtn');
const groundTruth = document.getElementById('groundTruth');
const runBtn = document.getElementById('runBtn');
const btnLabel = document.getElementById('btnLabel');
const btnSpinner = document.getElementById('btnSpinner');
const errorBanner = document.getElementById('errorBanner');
const errorText = document.getElementById('errorText');
const resultsSection = document.getElementById('resultsSection');
// Results DOM
const textOutput = document.getElementById('textOutput');
const summaryOutput = document.getElementById('summaryOutput');
const headingsOutput = document.getElementById('headingsOutput');
const headingsEmpty = document.getElementById('headingsEmpty');
const copyTextBtn = document.getElementById('copyTextBtn');
const cardMetrics = document.getElementById('card-metrics');
const cerValue = document.getElementById('cerValue');
const werValue = document.getElementById('werValue');
const cerFill = document.getElementById('cerFill');
const werFill = document.getElementById('werFill');
let selectedFile = null;
// ── File Selection ──
browseBtn.addEventListener('click', (e) => {
e.stopPropagation();
fileInput.click();
});
dropzone.addEventListener('click', () => {
if (!selectedFile) fileInput.click();
});
fileInput.addEventListener('change', () => {
if (fileInput.files[0]) handleFile(fileInput.files[0]);
});
// ── Drag & Drop ──
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('drag-over');
});
dropzone.addEventListener('dragleave', (e) => {
if (!dropzone.contains(e.relatedTarget)) {
dropzone.classList.remove('drag-over');
}
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('drag-over');
const file = e.dataTransfer.files[0];
if (file) handleFile(file);
});
// ── Remove Image ──
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
clearFile();
});
function handleFile(file) {
const allowed = ['image/jpeg', 'image/png', 'image/webp', 'image/bmp', 'image/tiff'];
if (!allowed.includes(file.type)) {
showError(`File type "${file.type}" is not supported. Please use JPEG, PNG, WebP, BMP, or TIFF.`);
return;
}
if (file.size > 10 * 1024 * 1024) {
showError(`File is too large (${(file.size / 1024 / 1024).toFixed(1)} MB). Maximum allowed is 10 MB.`);
return;
}
selectedFile = file;
hideError();
// Show preview
const reader = new FileReader();
reader.onload = (e) => {
previewImg.src = e.target.result;
previewMeta.textContent = `${file.name} · ${(file.size / 1024).toFixed(0)} KB`;
dropInner.style.display = 'none';
previewWrap.style.display = 'flex';
};
reader.readAsDataURL(file);
runBtn.disabled = false;
}
function clearFile() {
selectedFile = null;
fileInput.value = '';
previewImg.src = '';
previewWrap.style.display = 'none';
dropInner.style.display = 'block';
runBtn.disabled = true;
hideError();
}
// ── Copy Button ──
copyTextBtn.addEventListener('click', () => {
const txt = textOutput.textContent;
if (!txt) return;
navigator.clipboard.writeText(txt).then(() => {
copyTextBtn.classList.add('copied');
copyTextBtn.innerHTML = `
Copied!`;
setTimeout(() => {
copyTextBtn.classList.remove('copied');
copyTextBtn.innerHTML = `
Copy`;
}, 2000);
});
});
// ── OCR Submit ──
runBtn.addEventListener('click', async () => {
if (!selectedFile) return;
setLoading(true);
hideError();
resultsSection.style.display = 'none';
const formData = new FormData();
formData.append('file', selectedFile);
const gt = groundTruth.value.trim();
if (gt) formData.append('ground_truth', gt);
try {
const res = await fetch('/ocr', {
method: 'POST',
body: formData
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.detail || `Server error ${res.status}`);
}
renderResults(data);
} catch (err) {
showError(err.message || 'An unexpected error occurred. Please try again.');
} finally {
setLoading(false);
}
});
// ── Render Results ──
function renderResults(data) {
// Text
textOutput.textContent = data.text || '(no text extracted)';
// Summary
summaryOutput.textContent = data.summary || 'No summary available.';
// Headings
headingsOutput.innerHTML = '';
if (data.headings && data.headings.length > 0) {
headingsEmpty.style.display = 'none';
data.headings.forEach((h) => {
const li = document.createElement('li');
li.textContent = h;
headingsOutput.appendChild(li);
});
} else {
headingsEmpty.style.display = 'block';
}
// Metrics
if (data.cer !== null && data.cer !== undefined) {
cardMetrics.style.display = 'block';
const cerPct = (data.cer * 100).toFixed(1);
const werPct = (data.wer * 100).toFixed(1);
cerValue.textContent = cerPct + '%';
werValue.textContent = werPct + '%';
// Animate bars after render
requestAnimationFrame(() => {
requestAnimationFrame(() => {
cerFill.style.width = Math.min(data.cer * 100, 100) + '%';
werFill.style.width = Math.min(data.wer * 100, 100) + '%';
});
});
} else {
cardMetrics.style.display = 'none';
}
// Show results
resultsSection.style.display = 'flex';
// Scroll to results
setTimeout(() => {
resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 100);
}
// ── UI Helpers ──
function setLoading(loading) {
runBtn.disabled = loading;
btnLabel.style.display = loading ? 'none' : 'flex';
btnSpinner.style.display = loading ? 'flex' : 'none';
}
function showError(msg) {
errorText.textContent = msg;
errorBanner.style.display = 'flex';
}
function hideError() {
errorBanner.style.display = 'none';
errorText.textContent = '';
}
})();