Medical-Validator / app /static /index.html
saifisvibin's picture
Add disclaimer for Check Quality Only button
27d96bd
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Medical Document Validator</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 40px;
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 32px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 16px;
}
.form-group {
margin-bottom: 25px;
}
label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 600;
font-size: 14px;
}
select,
input[type="file"] {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
select:focus,
input[type="file"]:focus {
outline: none;
border-color: #667eea;
}
select {
background: white;
cursor: pointer;
}
input[type="file"] {
cursor: pointer;
}
.file-info {
margin-top: 8px;
color: #666;
font-size: 14px;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 14px 32px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
width: 100%;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
}
.btn:active {
transform: translateY(0);
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.loading {
display: none;
text-align: center;
margin-top: 20px;
color: #667eea;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.results {
display: none;
margin-top: 30px;
padding: 20px;
border-radius: 8px;
background: #f8f9fa;
}
.status {
padding: 12px 20px;
border-radius: 8px;
margin-bottom: 20px;
font-weight: 600;
font-size: 18px;
}
.status.pass {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.fail {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.summary {
margin-bottom: 20px;
color: #333;
font-size: 16px;
}
.elements-list {
list-style: none;
}
.element-item {
background: white;
padding: 15px;
margin-bottom: 10px;
border-radius: 6px;
border-left: 4px solid #e0e0e0;
}
.element-item.present {
border-left-color: #28a745;
}
.element-item.missing {
border-left-color: #dc3545;
}
.element-item.optional {
border-left-color: #ffc107;
}
.element-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.element-label {
font-weight: 600;
color: #333;
}
.element-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.badge-present {
background: #d4edda;
color: #155724;
}
.badge-missing {
background: #f8d7da;
color: #721c24;
}
.badge-optional {
background: #fff3cd;
color: #856404;
}
.element-reason {
color: #666;
font-size: 14px;
margin-top: 8px;
}
.error {
display: none;
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
border: 1px solid #f5c6cb;
}
.templates-loading {
color: #666;
font-style: italic;
}
.debug-section {
margin-top: 20px;
padding: 15px;
background: #f0f0f0;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.debug-btn {
background: #6c757d;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
margin-top: 10px;
}
.debug-btn:hover {
background: #5a6268;
}
.debug-info {
margin-top: 15px;
padding: 15px;
background: white;
border-radius: 6px;
font-family: monospace;
font-size: 12px;
max-height: 400px;
overflow-y: auto;
}
.debug-info pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
/* Spell Check Styles */
.spell-check-section {
margin-top: 20px;
padding: 20px;
background: #fff9e6;
border-radius: 8px;
border-left: 4px solid #ffc107;
}
.spell-check-header {
font-weight: 600;
font-size: 18px;
color: #333;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.spell-error-item {
background: white;
padding: 15px;
margin-bottom: 10px;
border-radius: 6px;
border-left: 4px solid #dc3545;
}
.spell-error-word {
font-weight: 700;
color: #dc3545;
font-size: 16px;
margin-bottom: 8px;
}
.spell-error-context {
color: #666;
font-style: italic;
margin-bottom: 10px;
padding: 8px;
background: #f8f9fa;
border-radius: 4px;
font-size: 14px;
}
.spell-suggestions {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 8px;
}
.spell-suggestion {
background: #d4edda;
color: #155724;
padding: 4px 12px;
border-radius: 12px;
font-size: 13px;
font-weight: 500;
}
.spell-error-type {
display: inline-block;
padding: 3px 10px;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
margin-left: 10px;
}
.type-spelling {
background: #ffc107;
color: #856404;
}
.type-grammar {
background: #007bff;
color: white;
}
.type-formatting {
background: #6f42c1;
color: white;
}
.button-group {
display: flex;
gap: 15px;
margin-top: 20px;
}
.btn-secondary {
background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%);
color: white;
border: none;
padding: 14px 32px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
flex: 1;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-secondary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(108, 117, 125, 0.4);
}
.btn-secondary:active {
transform: translateY(0);
}
.btn-secondary:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.spell-check-no-errors {
background: #d4edda;
color: #155724;
padding: 15px;
border-radius: 6px;
text-align: center;
font-weight: 600;
}
/* Link Validation Styles */
.link-validation-section {
margin-top: 20px;
padding: 20px;
background: #e8f4fd;
border-radius: 8px;
border-left: 4px solid #007bff;
}
.link-validation-header {
font-weight: 600;
font-size: 18px;
color: #333;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.link-list {
list-style: none;
}
.link-item {
background: white;
padding: 12px;
margin-bottom: 8px;
border-radius: 6px;
border: 1px solid #dee2e6;
display: flex;
justify-content: space-between;
align-items: center;
}
.link-item.broken {
border-left: 4px solid #dc3545;
}
.link-item.valid {
border-left: 4px solid #28a745;
}
.link-item.warning {
border-left: 4px solid #ffc107;
}
.link-url {
font-family: monospace;
color: #0056b3;
word-break: break-all;
margin-right: 10px;
font-size: 14px;
}
.link-meta {
color: #666;
font-size: 12px;
margin-top: 4px;
}
.link-status-badge {
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
}
.status-valid {
background: #d4edda;
color: #155724;
}
.status-broken {
background: #f8d7da;
color: #721c24;
}
.status-warning {
background: #fff3cd;
color: #856404;
}
</style>
</head>
<body>
<div class="container">
<h1>Medical Document Validator</h1>
<p class="subtitle">Upload a document and select a template to validate against</p>
<!-- Project Selector -->
<div
style="background: #e8f4f8; padding: 15px; border-radius: 8px; margin-bottom: 30px; border-left: 4px solid #007bff;">
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
<label style="font-weight: 600; margin: 0;">📂 Current Project:</label>
<select id="currentProject"
style="flex: 1; min-width: 200px; padding: 8px; border: 1px solid #ccc; border-radius: 4px;">
<option value="">No Project (Not Saved)</option>
</select>
<button type="button" class="btn-secondary" id="createProjectBtn" style="white-space: nowrap;">
+ New Project
</button>
<button type="button" class="btn-secondary" id="viewProjectsBtn" style="white-space: nowrap;">
📋 View All
</button>
</div>
</div>
<!-- Create Project Modal -->
<div id="createProjectModal"
style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center;">
<div
style="background: white; padding: 30px; border-radius: 8px; max-width: 500px; width: 90%; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
<h2 style="margin-top: 0;">Create New Project</h2>
<div class="form-group">
<label for="projectName">Project Name: *</label>
<input type="text" id="projectName" placeholder="e.g., Cardiology Conference Q1 2025"
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div class="form-group">
<label for="projectDescription">Description (Optional):</label>
<textarea id="projectDescription" rows="3" placeholder="Brief description of this project..."
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; resize: vertical;"></textarea>
</div>
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
<button type="button" class="btn-secondary" id="cancelProjectBtn">Cancel</button>
<button type="button" class="btn" id="saveProjectBtn">Create Project</button>
</div>
</div>
</div>
<form id="validationForm">
<div class="form-group">
<label for="templateSelect">Select Template: <span style="color: #999; font-weight: 400;">(Optional for
spelling-only check)</span></label>
<select id="templateSelect" name="template">
<option value="">Loading templates...</option>
</select>
</div>
<div class="form-group">
<label for="fileInput">Upload Document:</label>
<input type="file" id="fileInput" name="file" accept=".pdf,.docx,.pptx" required>
<div class="file-info" id="fileInfo"></div>
</div>
<div class="form-group">
<label for="customPrompt">Custom Instructions (Optional):</label>
<textarea id="customPrompt" name="customPrompt" rows="3" maxlength="500"
placeholder="Enter any additional instructions to customize the validation (e.g., 'Focus on date format validation' or 'Pay special attention to logo placement')..."
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-family: inherit; font-size: 14px; resize: vertical;"></textarea>
<div style="text-align: right; font-size: 12px; color: #666; margin-top: 4px;">
<span id="charCount">0</span>/500 characters
</div>
</div>
<div class="button-group">
<button type="button" class="btn" id="validateBtn">📋 Validate Document</button>
<button type="button" class="btn-secondary" id="spellingOnlyBtn">✨ Check Quality Only</button>
</div>
<p style="font-size: 13px; color: #666; margin-top: 10px; font-style: italic;">
💡 If you want to check grammar and spelling only without template matching or verification, click
"Check Quality Only".
</p>
</form>
<div class="debug-section">
<h3 style="margin-bottom: 10px; font-size: 18px;">🔍 Debug: Image Extraction</h3>
<p style="color: #666; font-size: 14px; margin-bottom: 10px;">
Test image extraction without full validation. Check console logs for detailed information.
</p>
<button type="button" class="debug-btn" id="debugBtn">Extract Images (Debug)</button>
<div class="debug-info" id="debugInfo" style="display: none;"></div>
</div>
<!-- Document Comparison Section -->
<div class="comparison-section"
style="background: #f8f9fa; padding: 25px; border-radius: 8px; margin-top: 30px;">
<h3 style="margin-bottom: 15px; font-size: 20px; color: #333;">🔄 Compare Documents</h3>
<p style="color: #666; font-size: 14px; margin-bottom: 20px;">
Upload two versions of a document to see what changed (e.g., before and after edits).
</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;">
<div class="form-group">
<label for="compareFile1">📄 Original Document:</label>
<input type="file" id="compareFile1" accept=".pdf,.docx,.pptx">
<div class="file-info" id="compareFileInfo1"></div>
</div>
<div class="form-group">
<label for="compareFile2">📝 Modified Document:</label>
<input type="file" id="compareFile2" accept=".pdf,.docx,.pptx">
<div class="file-info" id="compareFileInfo2"></div>
</div>
</div>
<button type="button" class="btn" id="compareBtn" style="width: 100%;">
🔍 Compare Documents
</button>
</div>
<!-- Bulk Certificate Validation Section -->
<div class="bulk-validation-section"
style="background: #f0f8ff; padding: 25px; border-radius: 8px; margin-top: 30px;">
<h3 style="margin-bottom: 15px; font-size: 20px; color: #333;">📋 Bulk Certificate Validation</h3>
<p style="color: #666; font-size: 14px; margin-bottom: 20px;">
Upload an Excel list of names and multiple certificates to verify all attendees received their
certificates.
</p>
<!-- Step 1: Excel Upload -->
<div class="form-group" style="margin-bottom: 20px;">
<label for="excelFile">1️⃣ Upload Excel File with Names:</label>
<input type="file" id="excelFile" accept=".xlsx">
<div class="file-info" id="excelFileInfo"></div>
</div>
<!-- Step 2: Column Selection -->
<div class="form-group" style="margin-bottom: 20px; display: none;" id="columnSelectorGroup">
<label for="nameColumn">2️⃣ Select Column Containing Names:</label>
<select id="nameColumn" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
<option value="">Loading columns...</option>
</select>
<div style="margin-top: 8px; color: #666; font-size: 13px;">
Preview: <span id="namePreview" style="font-weight: 500;"></span>
</div>
</div>
<!-- Step 3: Certificates Upload -->
<div class="form-group" style="margin-bottom: 20px;">
<label for="certificateFiles">3️⃣ Upload Certificates (Max 150):</label>
<input type="file" id="certificateFiles" multiple accept=".pdf,.pptx"">
<div style=" margin-top: 8px;">
<span style="font-weight: 600; color: #007bff;" id="certCount">0</span>
<span style="color: #666;">/150 files selected</span>
</div>
</div>
<!-- Step 4: Validate Button -->
<button type="button" class="btn" id="bulkValidateBtn" style="width: 100%;" disabled>
✅ Validate All Certificates
</button>
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Validating document...</p>
</div>
<div class="error" id="error"></div>
<div class="results" id="results">
<div class="status" id="status"></div>
<div class="summary" id="summary"></div>
<ul class="elements-list" id="elementsList"></ul>
</div>
<!-- Comparison Results -->
<div class="results" id="comparisonResults" style="display: none;">
<h2 style="margin-bottom: 20px;">📊 Comparison Results</h2>
<div id="comparisonSummary" style="margin-bottom: 20px;"></div>
<div id="comparisonDetails"></div>
</div>
<!-- Bulk Validation Results -->
<div class="results" id="bulkResults" style="display: none;">
<h2 style="margin-bottom: 20px;">📊 Bulk Validation Results</h2>
<div id="bulkSummary" style="margin-bottom: 20px;"></div>
<button type="button" class="btn-secondary" id="downloadCSVBtn" style="margin-bottom: 20px;">
📥 Download CSV Report
</button>
<div id="bulkDetails"></div>
</div>
</div>
<script>
// Load templates on page load
async function loadTemplates() {
try {
const response = await fetch('/templates');
const templates = await response.json();
const select = document.getElementById('templateSelect');
select.innerHTML = '<option value="">-- Select a template --</option>';
templates.forEach(template => {
const option = document.createElement('option');
option.value = template.template_key;
option.textContent = template.friendly_name;
select.appendChild(option);
});
} catch (error) {
console.error('Error loading templates:', error);
document.getElementById('templateSelect').innerHTML =
'<option value="">Error loading templates</option>';
}
}
// Handle file input change
document.getElementById('fileInput').addEventListener('change', function (e) {
const file = e.target.files[0];
const fileInfo = document.getElementById('fileInfo');
if (file) {
fileInfo.textContent = `Selected: ${file.name} (${(file.size / 1024).toFixed(2)} KB)`;
} else {
fileInfo.textContent = '';
}
});
// Handle character count for custom prompt
document.getElementById('customPrompt').addEventListener('input', function () {
const count = this.value.length;
document.getElementById('charCount').textContent = count;
});
// Handle comparison file 1 input
document.getElementById('compareFile1').addEventListener('change', function (e) {
const file = e.target.files[0];
const fileInfo = document.getElementById('compareFileInfo1');
if (file) {
fileInfo.textContent = `${file.name} (${(file.size / 1024).toFixed(2)} KB)`;
} else {
fileInfo.textContent = '';
}
});
// Handle comparison file 2 input
document.getElementById('compareFile2').addEventListener('change', function (e) {
const file = e.target.files[0];
const fileInfo = document.getElementById('compareFileInfo2');
if (file) {
fileInfo.textContent = `${file.name} (${(file.size / 1024).toFixed(2)} KB)`;
} else {
fileInfo.textContent = '';
}
});
// Handle Compare Documents button
document.getElementById('compareBtn').addEventListener('click', async function () {
const file1 = document.getElementById('compareFile1').files[0];
const file2 = document.getElementById('compareFile2').files[0];
if (!file1 || !file2) {
showError('Please select both documents to compare');
return;
}
// Hide previous results
document.getElementById('results').style.display = 'none';
document.getElementById('comparisonResults').style.display = 'none';
document.getElementById('error').style.display = 'none';
document.getElementById('loading').querySelector('p').textContent = 'Comparing documents...';
document.getElementById('loading').style.display = 'block';
try {
const formData = new FormData();
formData.append('file1', file1);
formData.append('file2', file2);
const response = await fetch('/compare', {
method: 'POST',
body: formData
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || 'Comparison failed');
}
displayComparisonResults(data);
} catch (error) {
showError(error.message || 'An error occurred during comparison');
} finally {
document.getElementById('loading').style.display = 'none';
}
});
// Handle Validate Document button (Template + Spelling)
document.getElementById('validateBtn').addEventListener('click', async function () {
const templateKey = document.getElementById('templateSelect').value;
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!templateKey) {
showError('Please select a template for validation');
return;
}
if (!file) {
showError('Please select a file to upload');
return;
}
// Validate file type
const validExtensions = ['.pdf', '.docx', '.pptx'];
const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
if (!validExtensions.includes(fileExtension)) {
showError('Invalid file type. Please upload a PDF, DOCX, or PPTX file.');
return;
}
// Hide previous results and errors
document.getElementById('results').style.display = 'none';
document.getElementById('error').style.display = 'none';
document.getElementById('loading').style.display = 'block';
document.getElementById('validateBtn').disabled = true;
document.getElementById('spellingOnlyBtn').disabled = true;
try {
const formData = new FormData();
formData.append('file', file);
// Get custom prompt if provided
const customPrompt = document.getElementById('customPrompt').value.trim();
// Build URL - always include spell checking for validate mode
let url = `/validate?template_key=${encodeURIComponent(templateKey)}&check_spelling=true`;
if (customPrompt) {
url += `&custom_prompt=${encodeURIComponent(customPrompt)}`;
}
const response = await fetch(url, {
method: 'POST',
body: formData
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || 'Validation failed');
}
displayResults(data);
} catch (error) {
showError(error.message || 'An error occurred during validation');
} finally {
document.getElementById('loading').style.display = 'none';
document.getElementById('validateBtn').disabled = false;
document.getElementById('spellingOnlyBtn').disabled = false;
}
});
// Handle Check Spelling Only button
document.getElementById('spellingOnlyBtn').addEventListener('click', async function () {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
showError('Please select a file to upload');
return;
}
// Validate file type
const validExtensions = ['.pdf', '.docx', '.pptx'];
const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
if (!validExtensions.includes(fileExtension)) {
showError('Invalid file type. Please upload a PDF, DOCX, or PPTX file.');
return;
}
// Hide previous results and errors
document.getElementById('results').style.display = 'none';
document.getElementById('error').style.display = 'none';
document.getElementById('loading').style.display = 'block';
document.getElementById('validateBtn').disabled = true;
document.getElementById('spellingOnlyBtn').disabled = true;
try {
const formData = new FormData();
formData.append('file', file);
// Spelling-only mode endpoint
const url = `/validate/spelling-only`;
const response = await fetch(url, {
method: 'POST',
body: formData
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || 'Spell check failed');
}
displaySpellingOnlyResults(data);
} catch (error) {
showError(error.message || 'An error occurred during spell checking');
} finally {
document.getElementById('loading').style.display = 'none';
document.getElementById('validateBtn').disabled = false;
document.getElementById('spellingOnlyBtn').disabled = false;
}
});
function showError(message) {
const errorDiv = document.getElementById('error');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
document.getElementById('results').style.display = 'none';
}
function displayResults(data) {
const resultsDiv = document.getElementById('results');
const statusDiv = document.getElementById('status');
const summaryDiv = document.getElementById('summary');
const elementsList = document.getElementById('elementsList');
// Set status
statusDiv.textContent = data.status === 'PASS' ? '✓ Validation Passed' : '✗ Validation Failed';
statusDiv.className = `status ${data.status.toLowerCase()}`;
// Set summary
summaryDiv.textContent = data.summary || 'Validation completed';
// Clear and populate elements list
elementsList.innerHTML = '';
data.elements_report.forEach(element => {
const li = document.createElement('li');
li.className = `element-item ${element.is_present ? 'present' : 'missing'} ${!element.required ? 'optional' : ''}`;
const header = document.createElement('div');
header.className = 'element-header';
const label = document.createElement('span');
label.className = 'element-label';
label.textContent = element.label;
const badge = document.createElement('span');
badge.className = `element-badge ${element.is_present ? 'badge-present' : 'badge-missing'} ${!element.required ? 'badge-optional' : ''}`;
if (!element.required) {
badge.textContent = 'Optional';
} else {
badge.textContent = element.is_present ? 'Present' : 'Missing';
}
header.appendChild(label);
header.appendChild(badge);
const reason = document.createElement('div');
reason.className = 'element-reason';
reason.textContent = element.reason;
li.appendChild(header);
li.appendChild(reason);
elementsList.appendChild(li);
});
// Display spell check results if available
if (data.spell_check) {
displaySpellCheck(data.spell_check);
}
// Display link validation results if available
if (data.link_report) {
displayLinkReport(data.link_report);
}
resultsDiv.style.display = 'block';
document.getElementById('error').style.display = 'none';
}
function displaySpellCheck(spellCheck) {
const elementsList = document.getElementById('elementsList');
// Create spell check section
const spellSection = document.createElement('div');
spellSection.className = 'spell-check-section';
const header = document.createElement('div');
header.className = 'spell-check-header';
header.innerHTML = `✨ Quality & Spelling Check<span style="font-size: 14px; color: #666; font-weight: normal; margin-left: auto;">${spellCheck.summary}</span>`;
spellSection.appendChild(header);
if (spellCheck.total_errors === 0) {
const noErrors = document.createElement('div');
noErrors.className = 'spell-check-no-errors';
noErrors.textContent = '✓ No quality or spelling errors found!';
spellSection.appendChild(noErrors);
} else {
spellCheck.errors.forEach(error => {
const errorItem = document.createElement('div');
errorItem.className = 'spell-error-item';
const wordDiv = document.createElement('div');
wordDiv.className = 'spell-error-word';
wordDiv.innerHTML = `"${error.word}"<span class="spell-error-type type-${error.error_type}">${error.error_type}</span>`;
errorItem.appendChild(wordDiv);
if (error.context) {
const contextDiv = document.createElement('div');
contextDiv.className = 'spell-error-context';
contextDiv.textContent = `Context: "${error.context}"`;
errorItem.appendChild(contextDiv);
}
if (error.suggestions && error.suggestions.length > 0) {
const suggestionsLabel = document.createElement('div');
suggestionsLabel.textContent = 'Suggestions:';
suggestionsLabel.style.fontSize = '14px';
suggestionsLabel.style.marginTop = '8px';
suggestionsLabel.style.marginBottom = '6px';
suggestionsLabel.style.fontWeight = '600';
errorItem.appendChild(suggestionsLabel);
const suggestionsDiv = document.createElement('div');
suggestionsDiv.className = 'spell-suggestions';
error.suggestions.forEach(suggestion => {
const suggestionSpan = document.createElement('span');
suggestionSpan.className = 'spell-suggestion';
suggestionSpan.textContent = suggestion;
suggestionsDiv.appendChild(suggestionSpan);
});
errorItem.appendChild(suggestionsDiv);
}
spellSection.appendChild(errorItem);
});
}
elementsList.appendChild(spellSection);
}
function displayLinkReport(linkReport) {
const elementsList = document.getElementById('elementsList');
// Create link results section
const linkSection = document.createElement('div');
linkSection.className = 'link-validation-section';
const header = document.createElement('div');
header.className = 'link-validation-header';
header.innerHTML = `🔗 Link Validation <span style="font-size: 14px; color: #666; font-weight: normal; margin-left: auto;">${linkReport.length} link(s) checked</span>`;
linkSection.appendChild(header);
if (linkReport.length === 0) {
const noLinks = document.createElement('div');
noLinks.style.padding = '10px';
noLinks.style.color = '#666';
noLinks.style.fontStyle = 'italic';
noLinks.textContent = 'No links found in document.';
linkSection.appendChild(noLinks);
} else {
const list = document.createElement('ul');
list.className = 'link-list';
linkReport.forEach(link => {
const item = document.createElement('li');
let statusClass = 'valid';
if (link.status === 'broken') statusClass = 'broken';
if (link.status === 'warning') statusClass = 'warning';
item.className = `link-item ${statusClass}`;
const leftDiv = document.createElement('div');
leftDiv.style.flex = '1';
leftDiv.style.marginRight = '10px';
leftDiv.style.overflow = 'hidden';
const urlDiv = document.createElement('div');
urlDiv.className = 'link-url';
urlDiv.textContent = link.url;
leftDiv.appendChild(urlDiv);
const metaDiv = document.createElement('div');
metaDiv.className = 'link-meta';
const pageInfo = link.page !== 'Unknown' ? `Page: ${link.page}` : '';
metaDiv.textContent = `${pageInfo} ${link.message ? '• ' + link.message : ''}`;
leftDiv.appendChild(metaDiv);
const badge = document.createElement('span');
badge.className = `link-status-badge status-${statusClass}`;
badge.textContent = link.status.toUpperCase();
item.appendChild(leftDiv);
item.appendChild(badge);
list.appendChild(item);
});
linkSection.appendChild(list);
}
elementsList.appendChild(linkSection);
}
function displaySpellingOnlyResults(data) {
const resultsDiv = document.getElementById('results');
const statusDiv = document.getElementById('status');
const summaryDiv = document.getElementById('summary');
const elementsList = document.getElementById('elementsList');
// Set status for spelling-only mode
const hasErrors = data.spell_check && data.spell_check.total_errors > 0;
statusDiv.textContent = hasErrors ? '⚠️ Issues Found' : '✓ Text Quality OK';
statusDiv.className = `status ${hasErrors ? 'fail' : 'pass'}`;
// Set summary
summaryDiv.textContent = data.spell_check ? data.spell_check.summary : 'Spell check completed';
// Clear elements list
elementsList.innerHTML = '';
// Display spell check results
if (data.spell_check) {
displaySpellCheck(data.spell_check);
}
resultsDiv.style.display = 'block';
document.getElementById('error').style.display = 'none';
}
// Debug: Extract images
document.getElementById('debugBtn').addEventListener('click', async function () {
const templateKey = document.getElementById('templateSelect').value;
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const debugInfo = document.getElementById('debugInfo');
if (!templateKey) {
alert('Please select a template first');
return;
}
if (!file) {
alert('Please select a file first');
return;
}
debugInfo.style.display = 'block';
debugInfo.innerHTML = '<p>Extracting images...</p>';
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`/debug/extract-images?template_key=${encodeURIComponent(templateKey)}`, {
method: 'POST',
body: formData
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || 'Extraction failed');
}
// Format debug output
let output = '=== IMAGE EXTRACTION DEBUG ===\n\n';
output += `File: ${data.file_name}\n`;
output += `Size: ${(data.file_size_bytes / 1024).toFixed(2)} KB\n`;
output += `Text extracted: ${data.text_extracted ? 'Yes' : 'No'} (${data.text_length} chars)\n\n`;
output += `Images Found: ${data.images_found}\n`;
output += `Template Requires Visual Elements: ${data.template_requires_visual_elements ? 'Yes' : 'No'}\n\n`;
if (data.template_visual_elements.length > 0) {
output += 'Template Visual Elements:\n';
data.template_visual_elements.forEach(elem => {
output += ` - ${elem.label} (${elem.type}) - Required: ${elem.required}\n`;
});
output += '\n';
}
if (data.images.length > 0) {
output += 'Extracted Images:\n';
data.images.forEach((img, idx) => {
output += `\n${idx + 1}. ${img.id}\n`;
output += ` Path: ${img.file_path}\n`;
output += ` Exists: ${img.file_exists ? 'Yes' : 'No'}\n`;
output += ` Size: ${(img.file_size_bytes / 1024).toFixed(2)} KB\n`;
output += ` Dimensions: ${img.dimensions}\n`;
output += ` Mode: ${img.image_mode}\n`;
output += ` Role: ${img.role_hint}\n`;
output += ` Type: ${img.element_type}\n`;
});
} else {
output += '\n⚠️ No images were extracted from the document.\n';
output += 'This could mean:\n';
output += ' - The document has no embedded images\n';
output += ' - Images are in a format not supported\n';
output += ' - Images are embedded as external links\n';
}
debugInfo.innerHTML = '<pre>' + output + '</pre>';
} catch (error) {
debugInfo.innerHTML = '<pre style="color: red;">Error: ' + error.message + '</pre>';
}
});
// Function to display comparison results
function displayComparisonResults(data) {
const resultsDiv = document.getElementById('comparisonResults');
const summaryDiv = document.getElementById('comparisonSummary');
const detailsDiv = document.getElementById('comparisonDetails');
// Display summary
summaryDiv.innerHTML = `
<div class="summary" style="background: #f8f9fa; padding: 20px; border-radius: 8px;">
<h3 style="margin-bottom: 15px;">📝 Summary</h3>
<div style="white-space: pre-wrap; line-height: 1.6;">${data.summary || 'No summary available'}</div>
</div>
`;
// Display detailed changes
if (data.changes && data.changes.length > 0) {
let changesHTML = '<h3 style="margin: 20px 0 15px 0;">🔍 Detailed Changes</h3><ul class="elements-list">';
data.changes.forEach(change => {
const typeClass = change.type === 'addition' ? 'status-pass' :
change.type === 'deletion' ? 'status-fail' : 'status-warning';
const typeIcon = change.type === 'addition' ? '➕' :
change.type === 'deletion' ? '➖' : '🔄';
changesHTML += `
<li style="margin-bottom: 15px; padding: 15px; background: white; border-left: 4px solid ${change.type === 'addition' ? '#28a745' : change.type === 'deletion' ? '#dc3545' : '#ffc107'}; border-radius: 4px;">
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<span class="status-badge ${typeClass}" style="margin-right: 10px;">${typeIcon} ${change.type.toUpperCase()}</span>
${change.section ? `<strong>${change.section}</strong>` : ''}
</div>
<div style="color: #666; white-space: pre-wrap;">${change.description}</div>
</li>
`;
});
changesHTML += '</ul>';
detailsDiv.innerHTML = changesHTML;
} else {
detailsDiv.innerHTML = '<p style="color: #666; text-align: center; padding: 20px;">✅ No significant changes detected between the documents.</p>';
}
resultsDiv.style.display = 'block';
resultsDiv.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// Bulk Validation: Excel file handler
let excelFileData = null;
let excelColumns = [];
document.getElementById('excelFile').addEventListener('change', async function (e) {
const file = e.target.files[0];
const fileInfo = document.getElementById('excelFileInfo');
if (file) {
fileInfo.textContent = `${file.name} (${(file.size / 1024).toFixed(2)} KB)`;
// Read and parse Excel to get columns
try {
excelFileData = file;
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/excel-columns', {
method: 'POST',
body: formData
});
const data = await response.json();
if (response.ok) {
excelColumns = data.columns;
const nameColumnSelect = document.getElementById('nameColumn');
nameColumnSelect.innerHTML = '<option value="">-- Select Column --</option>';
excelColumns.forEach(col => {
const option = document.createElement('option');
option.value = col;
option.textContent = col;
nameColumnSelect.appendChild(option);
});
document.getElementById('columnSelectorGroup').style.display = 'block';
document.getElementById('namePreview').textContent = `${data.row_count} names found`;
}
} catch (error) {
showError('Failed to parse Excel file: ' + error.message);
}
} else {
fileInfo.textContent = '';
document.getElementById('columnSelectorGroup').style.display = 'none';
}
});
// Bulk Validation: Certificate files handler
document.getElementById('certificateFiles').addEventListener('change', function (e) {
const files = e.target.files;
const count = files.length;
document.getElementById('certCount').textContent = count;
if (count > 150) {
showError('Maximum 150 certificates allowed. Please reduce your selection.');
this.value = '';
document.getElementById('certCount').textContent = '0';
return;
}
checkBulkValidateReady();
});
// Bulk Validation: Column selection handler
document.getElementById('nameColumn').addEventListener('change', function () {
checkBulkValidateReady();
});
// Check if bulk validate button should be enabled
function checkBulkValidateReady() {
const excelFile = document.getElementById('excelFile').files[0];
const column = document.getElementById('nameColumn').value;
const certFiles = document.getElementById('certificateFiles').files;
const btn = document.getElementById('bulkValidateBtn');
btn.disabled = !(excelFile && column && certFiles.length > 0);
}
// Bulk Validation: Validate button handler
document.getElementById('bulkValidateBtn').addEventListener('click', async function () {
const excelFile = document.getElementById('excelFile').files[0];
const nameColumn = document.getElementById('nameColumn').value;
const certFiles = document.getElementById('certificateFiles').files;
if (!excelFile || !nameColumn || certFiles.length === 0) {
showError('Please complete all steps before validating');
return;
}
// Hide previous results
document.getElementById('results').style.display = 'none';
document.getElementById('comparisonResults').style.display = 'none';
document.getElementById('bulkResults').style.display = 'none';
document.getElementById('error').style.display = 'none';
document.getElementById('loading').querySelector('p').textContent = `Processing ${certFiles.length} certificates...`;
document.getElementById('loading').style.display = 'block';
this.disabled = true;
try {
const formData = new FormData();
formData.append('excel_file', excelFile);
formData.append('name_column', nameColumn);
for (let i = 0; i < certFiles.length; i++) {
formData.append('certificate_files', certFiles[i]);
}
const response = await fetch('/bulk-validate', {
method: 'POST',
body: formData
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || 'Bulk validation failed');
}
displayBulkResults(data);
} catch (error) {
showError(error.message || 'An error occurred during bulk validation');
} finally {
document.getElementById('loading').style.display = 'none';
this.disabled = false;
}
});
// Display bulk validation results
let bulkResultsData = null;
function displayBulkResults(data) {
bulkResultsData = data;
const resultsDiv = document.getElementById('bulkResults');
const summaryDiv = document.getElementById('bulkSummary');
const detailsDiv = document.getElementById('bulkDetails');
// Summary
summaryDiv.innerHTML = `
<div class="summary" style="background: #f8f9fa; padding: 20px; border-radius: 8px;">
<h3 style="margin-bottom: 15px;">📊 Summary</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px;">
<div style="text-align: center; padding: 15px; background: white; border-radius: 6px;">
<div style="font-size: 32px; font-weight: bold; color: #007bff;">${data.total_names}</div>
<div style="color: #666; font-size: 14px;">Total Names</div>
</div>
<div style="text-align: center; padding: 15px; background: white; border-radius: 6px;">
<div style="font-size: 32px; font-weight: bold; color: #6c757d;">${data.total_certificates}</div>
<div style="color: #666; font-size: 14px;">Certificates</div>
</div>
<div style="text-align: center; padding: 15px; background: white; border-radius: 6px;">
<div style="font-size: 32px; font-weight: bold; color: #28a745;">${data.exact_matches}</div>
<div style="color: #666; font-size: 14px;">✅ Exact</div>
</div>
<div style="text-align: center; padding: 15px; background: white; border-radius: 6px;">
<div style="font-size: 32px; font-weight: bold; color: #ffc107;">${data.fuzzy_matches}</div>
<div style="color: #666; font-size: 14px;">⚠️ Fuzzy</div>
</div>
<div style="text-align: center; padding: 15px; background: white; border-radius: 6px;">
<div style="font-size: 32px; font-weight: bold; color: #dc3545;">${data.missing}</div>
<div style="color: #666; font-size: 14px;">❌ Missing</div>
</div>
<div style="text-align: center; padding: 15px; background: white; border-radius: 6px;">
<div style="font-size: 32px; font-weight: bold; color: #17a2b8;">${data.extras}</div>
<div style="color: #666; font-size: 14px;">➕ Extra</div>
</div>
</div>
</div>
`;
// Details
let detailsHTML = '<h3 style="margin: 20px 0 15px 0;">📋 Detailed Results</h3><ul class="elements-list">';
data.details.forEach(item => {
const status = item.status;
const bgColor = status === 'exact_match' ? '#d4edda' :
status === 'fuzzy_match' ? '#fff3cd' :
status === 'missing' ? '#f8d7da' : '#d1ecf1';
const icon = status === 'exact_match' ? '✅' :
status === 'fuzzy_match' ? '⚠️' :
status === 'missing' ? '❌' : '➕';
const label = status === 'exact_match' ? 'EXACT MATCH' :
status === 'fuzzy_match' ? `FUZZY MATCH (${item.similarity}%)` :
status === 'missing' ? 'MISSING' : 'EXTRA';
detailsHTML += `
<li style="margin-bottom: 10px; padding: 12px; background: ${bgColor}; border-radius: 4px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<strong>${item.name}</strong>
${item.certificate_file ? `<div style="font-size: 12px; color: #666; margin-top: 4px;">📄 ${item.certificate_file}</div>` : ''}
</div>
<span style="font-size: 14px; font-weight: 600;">${icon} ${label}</span>
</div>
</li>
`;
});
detailsHTML += '</ul>';
detailsDiv.innerHTML = detailsHTML;
resultsDiv.style.display = 'block';
resultsDiv.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// CSV Download handler
document.getElementById('downloadCSVBtn').addEventListener('click', function () {
if (!bulkResultsData) return;
let csv = 'Name,Status,Certificate File,Match Type,Similarity\n';
bulkResultsData.details.forEach(item => {
const status = item.status === 'exact_match' ? 'Found' :
item.status === 'fuzzy_match' ? 'Found' :
item.status === 'missing' ? 'Missing' : 'Extra';
const matchType = item.status === 'exact_match' ? 'Exact' :
item.status === 'fuzzy_match' ? 'Fuzzy' : '-';
const similarity = item.similarity || '-';
const certFile = item.certificate_file || '-';
csv += `"${item.name}","${status}","${certFile}","${matchType}","${similarity}"\n`;
});
const blob = new Blob([csv], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'bulk_validation_results.csv';
a.click();
window.URL.revokeObjectURL(url);
});
// ==================== PROJECTS FUNCTIONALITY ====================
// Load projects list
async function loadProjects() {
try {
const response = await fetch('/projects');
const projects = await response.json();
const selector = document.getElementById('currentProject');
selector.innerHTML = '<option value="">No Project (Not Saved)</option>';
projects.forEach(project => {
const option = document.createElement('option');
option.value = project.id;
option.textContent = `${project.name} (${project.validation_count} validations)`;
selector.appendChild(option);
});
} catch (error) {
console.error('Failed to load projects:', error);
}
}
// Create project modal handlers
document.getElementById('createProjectBtn').addEventListener('click', function () {
document.getElementById('createProjectModal').style.display = 'flex';
document.getElementById('projectName').value = '';
document.getElementById('projectDescription').value = '';
});
document.getElementById('cancelProjectBtn').addEventListener('click', function () {
document.getElementById('createProjectModal').style.display = 'none';
});
document.getElementById('saveProjectBtn').addEventListener('click', async function () {
const name = document.getElementById('projectName').value.trim();
const description = document.getElementById('projectDescription').value.trim();
if (!name) {
showError('Project name is required');
return;
}
try {
const response = await fetch('/projects', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to create project');
}
const project = await response.json();
document.getElementById('createProjectModal').style.display = 'none';
await loadProjects();
document.getElementById('currentProject').value = project.id;
showError(''); // clear error
} catch (error) {
showError(error.message);
}
});
// View all projects
document.getElementById('viewProjectsBtn').addEventListener('click', function () {
// For now, just alert - can be enhanced later
alert('Projects view coming soon! For now, use the dropdown to select projects.');
});
// Load templates when page loads
loadProjects();
loadTemplates();
</script>
</body>
</html>