Spaces:
Sleeping
Sleeping
| <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> |