saifisvibin's picture
feat: Add core medical document validation module with text/image extraction and initial static UI page.
edca77e
<!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>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--primary: #1E3A5F;
--primary-light: #2D5A8A;
--accent: #00A878;
--accent-hover: #008F66;
--bg-main: #F0F4F8;
--bg-card: #FFFFFF;
--bg-sidebar: #1E3A5F;
--text-primary: #1F2937;
--text-secondary: #6B7280;
--text-muted: #9CA3AF;
--border: #E5E7EB;
--border-focus: #00A878;
--success: #10B981;
--success-bg: #D1FAE5;
--warning: #F59E0B;
--warning-bg: #FEF3C7;
--error: #EF4444;
--error-bg: #FEE2E2;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 16px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-main);
min-height: 100vh;
color: var(--text-primary);
display: flex;
}
/* Sidebar Navigation */
.sidebar {
width: 260px;
background: var(--bg-sidebar);
min-height: 100vh;
padding: 24px 16px;
position: fixed;
left: 0;
top: 0;
display: flex;
flex-direction: column;
}
.sidebar-logo {
color: white;
font-size: 20px;
font-weight: 700;
padding: 12px 16px;
margin-bottom: 32px;
display: flex;
align-items: center;
gap: 12px;
}
.sidebar-logo svg {
width: 32px;
height: 32px;
}
.sidebar-nav {
flex: 1;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
border-radius: var(--radius-md);
margin-bottom: 4px;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
font-size: 14px;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.nav-item.active {
background: var(--accent);
color: white;
}
.nav-item svg {
width: 20px;
height: 20px;
}
/* Main Content */
.main-content {
margin-left: 260px;
flex: 1;
padding: 32px;
min-height: 100vh;
}
/* Header */
.page-header {
margin-bottom: 32px;
}
.page-header h1 {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
}
.page-header p {
color: var(--text-secondary);
font-size: 15px;
}
/* Cards */
.card {
background: var(--bg-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
padding: 24px;
margin-bottom: 24px;
}
.card-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border);
}
.card-header h2 {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.card-icon {
width: 40px;
height: 40px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.card-icon.primary {
background: rgba(30, 58, 95, 0.1);
}
.card-icon.accent {
background: rgba(0, 168, 120, 0.1);
}
.card-icon.warning {
background: rgba(245, 158, 11, 0.1);
}
/* Form Elements */
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: var(--text-primary);
font-weight: 500;
font-size: 14px;
}
label .optional {
color: var(--text-muted);
font-weight: 400;
}
select,
input[type="text"],
input[type="file"],
textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--border);
border-radius: var(--radius-md);
font-size: 14px;
font-family: inherit;
transition: all 0.2s;
background: white;
}
select:focus,
input[type="text"]:focus,
textarea:focus {
outline: none;
border-color: var(--border-focus);
box-shadow: 0 0 0 3px rgba(0, 168, 120, 0.1);
}
select {
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%236B7280'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 20px;
padding-right: 40px;
}
/* File Upload */
.file-upload-wrapper {
border: 2px dashed var(--border);
border-radius: var(--radius-md);
padding: 32px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
background: #FAFBFC;
}
.file-upload-wrapper:hover {
border-color: var(--accent);
background: rgba(0, 168, 120, 0.02);
}
.file-upload-wrapper.dragover {
border-color: var(--accent);
background: rgba(0, 168, 120, 0.05);
}
.file-upload-icon {
font-size: 48px;
margin-bottom: 12px;
}
.file-upload-text {
font-size: 15px;
color: var(--text-secondary);
}
.file-upload-text strong {
color: var(--accent);
}
.file-info {
margin-top: 12px;
padding: 12px;
background: var(--success-bg);
border-radius: var(--radius-sm);
color: var(--success);
font-weight: 500;
font-size: 14px;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 24px;
border-radius: var(--radius-md);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
border: none;
font-family: inherit;
}
.btn-primary {
background: var(--accent);
color: white;
}
.btn-primary:hover {
background: var(--accent-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn-secondary {
background: var(--bg-main);
color: var(--text-primary);
border: 2px solid var(--border);
}
.btn-secondary:hover {
background: var(--border);
}
.btn-outline {
background: transparent;
color: var(--primary);
border: 2px solid var(--primary);
}
.btn-outline:hover {
background: var(--primary);
color: white;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
.btn-lg {
padding: 16px 32px;
font-size: 16px;
}
.btn-full {
width: 100%;
}
.button-group {
display: flex;
gap: 12px;
margin-top: 24px;
}
.button-group .btn {
flex: 1;
}
/* Loading */
.loading {
display: none;
text-align: center;
padding: 40px;
}
.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 Section Refined */
.results {
display: none;
margin-top: 32px;
animation: fadeIn 0.4s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.status {
display: flex;
align-items: center;
gap: 20px;
padding: 24px;
border-radius: var(--radius-lg);
margin-bottom: 32px;
border: 1px solid transparent;
box-shadow: var(--shadow-sm);
}
.status.pass {
background: #ECFDF5;
border-color: #A7F3D0;
color: #065F46;
}
.status.fail {
background: #FEF2F2;
border-color: #FECACA;
color: #991B1B;
}
.status-icon {
width: 56px;
height: 56px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.6);
font-size: 28px;
flex-shrink: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.status-content {
flex: 1;
}
.status-content h3 {
font-size: 20px;
font-weight: 700;
margin-bottom: 6px;
letter-spacing: -0.01em;
}
.status-content p {
font-size: 15px;
opacity: 0.9;
line-height: 1.5;
}
.elements-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
list-style: none;
margin-bottom: 32px;
}
.element-item {
background: white;
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 20px;
box-shadow: var(--shadow-sm);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
}
.element-item:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--primary-light);
}
.element-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6px;
}
.element-item.present::before {
background: var(--success);
}
.element-item.missing::before {
background: var(--error);
}
.element-item.optional::before {
background: var(--warning);
}
.element-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.element-label {
font-weight: 700;
font-size: 16px;
color: var(--text-primary);
padding-left: 12px;
line-height: 1.3;
}
.element-badge {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
padding: 6px 12px;
border-radius: 20px;
letter-spacing: 0.5px;
flex-shrink: 0;
}
.badge-present {
background: #E6FFFA;
color: #047481;
border: 1px solid #B2F5EA;
}
.badge-missing {
background: #FFF5F5;
color: #C53030;
border: 1px solid #FED7D7;
}
.badge-optional {
background: #FFFFF0;
color: #B7791F;
border: 1px solid #FEFCBF;
}
.element-reason {
color: var(--text-secondary);
font-size: 14px;
margin-top: auto;
line-height: 1.5;
padding-left: 12px;
border-top: 1px solid #F3F4F6;
padding-top: 12px;
}
.error {
display: none;
background: #FEF2F2;
color: #991B1B;
padding: 20px;
border-radius: var(--radius-md);
margin-top: 24px;
border: 1px solid #FECACA;
font-weight: 500;
text-align: center;
}
.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;
}
/* Loading animations */
@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
@keyframes dash {
0% {
stroke-dashoffset: 80;
}
50% {
stroke-dashoffset: 20;
}
100% {
stroke-dashoffset: 80;
}
}
@keyframes progress {
0% {
width: 5%;
}
50% {
width: 70%;
}
100% {
width: 95%;
}
}
</style>
</head>
<body>
<!-- Sidebar Navigation -->
<aside class="sidebar">
<div class="sidebar-logo">
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="8" fill="#00A878" />
<path d="M16 8L8 12v8l8 4 8-4v-8l-8-4z" stroke="white" stroke-width="2" fill="none" />
<path d="M16 16v8M8 12l8 4 8-4" stroke="white" stroke-width="2" />
</svg>
<span>DocValidator</span>
</div>
<nav class="sidebar-nav">
<div class="nav-item active" data-tab="validate">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Validate Document
</div>
<div class="nav-item" data-tab="compare">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"></path>
</svg>
Compare Documents
</div>
<div class="nav-item" data-tab="bulk">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10">
</path>
</svg>
Bulk Validation
</div>
</nav>
<div style="padding: 16px; border-top: 1px solid rgba(255,255,255,0.1); margin-top: auto;">
<div class="nav-item" id="logoutBtn">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1">
</path>
</svg>
Logout
</div>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<div class="page-header">
<h1>Document Validation</h1>
<p>Upload a document and select a template to validate against</p>
</div>
<!-- VALIDATE PAGE -->
<div id="validatePage" class="page-section">
<!-- Project Selector Card -->
<div class="card" style="padding: 16px;">
<div style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 20px;">📂</span>
<label style="font-weight: 600; margin: 0; white-space: nowrap;">Project:</label>
</div>
<select id="currentProject" style="flex: 1; min-width: 200px;">
<option value="">No Project (Not Saved)</option>
</select>
<button type="button" class="btn btn-secondary" id="createProjectBtn">+ New</button>
<button type="button" class="btn btn-secondary" id="viewProjectsBtn">View All</button>
</div>
</div>
<!-- SharePoint Integration -->
<div
style="background: #f3f6f9; padding: 15px; border-radius: 8px; margin-bottom: 30px; border-left: 4px solid #0078d4; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px;">
<div style="display: flex; align-items: center; gap: 10px;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5 4H19.5C20.6046 4 21.5 4.89543 21.5 6V20C21.5 21.1046 20.6046 22 19.5 22H12.5V4Z"
fill="#0078D4" />
<path d="M2.5 6C2.5 4.89543 3.39543 4 4.5 4H11.5V22H4.5C3.39543 22 2.5 21.1046 2.5 20V6Z"
fill="#50E6FF" fill-opacity="0.3" />
<path d="M11.5 4V13H7.5V7H11.5V4Z" fill="#0078D4" fill-opacity="0.5" />
</svg>
<div>
<strong style="display: block; color: #333;">Microsoft SharePoint / OneDrive</strong>
<span style="font-size: 12px; color: #666;">Import documents directly from cloud</span>
</div>
</div>
<div id="sharepointAuthSection">
<button type="button" class="btn-secondary" id="connectSharePointBtn"
style="border: 1px solid #0078d4; color: #0078d4; background: white;">
🔗 Connect Account
</button>
</div>
<div id="sharepointActionsSection" style="display: none;">
<span id="sharepointStatus"
style="font-size: 12px; color: #28a745; font-weight: 600; margin-right: 10px;">
Connected</span>
<button type="button" class="btn-secondary" id="browseSharePointBtn"
style="background: #0078d4; color: white; border: none;">
📂 Browse Files
</button>
<button type="button" class="btn-secondary" id="logoutSharePointBtn"
style="background: #eee; border: 1px solid #ccc; font-size: 12px; padding: 5px 10px;">
Disconnect
</button>
</div>
</div>
<!-- Validation Form Card -->
<div class="card" id="validateSection">
<div class="card-header">
<div class="card-icon accent"></div>
<h2>Validate Document</h2>
</div>
<form id="validationForm">
<div class="form-group">
<label for="templateSelect">Select Template <span class="optional">(Required for template
validation)</span></label>
<select id="templateSelect" name="template">
<option value="">-- Select a template --</option>
</select>
</div>
<div class="form-group">
<label for="fileInput">Upload Document</label>
<div class="file-upload-wrapper" id="dropZone">
<div class="file-upload-icon">📄</div>
<div class="file-upload-text">
<strong>Click to upload</strong> or drag and drop<br>
PDF, DOCX, or PPTX files
</div>
<input type="file" id="fileInput" name="file" accept=".pdf,.docx,.pptx" required
style="display: none;">
</div>
<div class="file-info" id="fileInfo" style="display: none;"></div>
</div>
<div class="form-group">
<label for="customPrompt">Custom Instructions <span class="optional">(Optional)</span></label>
<textarea id="customPrompt" name="customPrompt" rows="3" maxlength="500"
placeholder="e.g., 'Focus on date format validation' or 'Pay special attention to logo placement'..."></textarea>
<div style="text-align: right; font-size: 12px; color: var(--text-muted); margin-top: 4px;">
<span id="charCount">0</span>/500 characters
</div>
</div>
<div class="button-group">
<button type="button" class="btn btn-primary btn-lg" id="validateBtn">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Validate Document
</button>
<button type="button" class="btn btn-secondary btn-lg" id="spellingOnlyBtn">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z">
</path>
</svg>
Quality Check Only
</button>
</div>
<p style="font-size: 13px; color: var(--text-muted); margin-top: 12px; text-align: center;">
💡 Use "Quality Check Only" for grammar and spelling without template validation
</p>
</form>
</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>
</div>
<!-- COMPARE PAGE -->
<div id="comparePage" class="page-section" style="display: none;">
<!-- 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 class="error" id="compareError" style="margin-top: 15px;"></div>
<!-- Loading Indicator -->
<div id="compareLoading"
style="display: none; margin-top: 20px; text-align: center; padding: 40px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; color: white;">
<div style="margin-bottom: 20px;">
<svg width="60" height="60" viewBox="0 0 50 50" style="animation: rotate 2s linear infinite;">
<circle cx="25" cy="25" r="20" fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="4">
</circle>
<circle cx="25" cy="25" r="20" fill="none" stroke="white" stroke-width="4"
stroke-dasharray="80" stroke-dashoffset="60" stroke-linecap="round"
style="animation: dash 1.5s ease-in-out infinite;"></circle>
</svg>
</div>
<h3 style="margin: 0 0 10px 0; font-size: 18px; font-weight: 600;">Comparing Documents...</h3>
<p id="compareLoadingText" style="margin: 0; opacity: 0.9; font-size: 14px;">Extracting and
analyzing content</p>
<div
style="margin-top: 15px; background: rgba(255,255,255,0.2); border-radius: 8px; height: 6px; overflow: hidden;">
<div id="compareProgressBar"
style="height: 100%; background: white; width: 0%; transition: width 0.5s ease; animation: progress 3s ease-in-out infinite;">
</div>
</div>
</div>
</div>
<!-- Comparison Results inside Compare Page -->
<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>
</div>
<!-- BULK PAGE -->
<div id="bulkPage" class="page-section" style="display: none;">
<!-- 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>
<!-- Bulk Validation Results inside Bulk Page -->
<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>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Validating document...</p>
</div>
<div class="error" id="error"></div>
</main>
<script>
// Tab Navigation Logic
document.addEventListener('DOMContentLoaded', () => {
const navItems = document.querySelectorAll('.nav-item[data-tab]');
const pages = {
'validate': document.getElementById('validatePage'),
'compare': document.getElementById('comparePage'),
'bulk': document.getElementById('bulkPage')
};
const pageHeaderTitle = document.querySelector('.page-header h1');
const pageHeaderDesc = document.querySelector('.page-header p');
const pageInfo = {
'validate': { title: 'Document Validation', desc: 'Upload a document and select a template to validate against' },
'compare': { title: 'Compare Documents', desc: 'Upload two versions of a document to see differences' },
'bulk': { title: 'Bulk Certificate Validation', desc: 'Validate multiple certificates against an Excel list' }
};
navItems.forEach(item => {
item.addEventListener('click', () => {
const tabName = item.getAttribute('data-tab');
// Update Sidebar
navItems.forEach(nav => nav.classList.remove('active'));
item.classList.add('active');
// Update Pages
Object.values(pages).forEach(page => {
if (page) page.style.display = 'none';
});
if (pages[tabName]) {
pages[tabName].style.display = 'block';
// Update Header
if (pageInfo[tabName]) {
pageHeaderTitle.textContent = pageInfo[tabName].title;
pageHeaderDesc.textContent = pageInfo[tabName].desc;
}
}
// Clear and hide global error on tab switch
const errorDiv = document.getElementById('error');
errorDiv.style.display = 'none';
errorDiv.textContent = '';
});
});
// Initial load of templates
loadTemplates();
});
// 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 - updated for new drag-drop wrapper
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const fileInfo = document.getElementById('fileInfo');
// Click to open file dialog
dropZone.addEventListener('click', () => fileInput.click());
// Drag and drop handlers
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
if (e.dataTransfer.files.length) {
fileInput.files = e.dataTransfer.files;
updateFileInfo(e.dataTransfer.files[0]);
}
});
fileInput.addEventListener('change', function (e) {
if (e.target.files[0]) {
updateFileInfo(e.target.files[0]);
}
});
function updateFileInfo(file) {
fileInfo.style.display = 'block';
fileInfo.innerHTML = `✓ ${file.name} (${(file.size / 1024).toFixed(1)} KB)`;
dropZone.style.display = 'none';
}
// 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];
const compareError = document.getElementById('compareError');
const compareLoading = document.getElementById('compareLoading');
const loadingText = document.getElementById('compareLoadingText');
// Clear previous errors
compareError.style.display = 'none';
compareError.textContent = '';
if (!file1 || !file2) {
compareError.textContent = 'Please select both documents to compare';
compareError.style.display = 'block';
return;
}
// Hide previous results
document.getElementById('results').style.display = 'none';
document.getElementById('comparisonResults').style.display = 'none';
// Show loading indicator
compareLoading.style.display = 'block';
loadingText.textContent = 'Extracting text from documents...';
this.disabled = true;
try {
const formData = new FormData();
formData.append('file1', file1);
formData.append('file2', file2);
// Update status
loadingText.textContent = 'Analyzing differences with AI...';
const response = await fetch('/compare', {
method: 'POST',
body: formData
});
loadingText.textContent = 'Processing results...';
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || 'Comparison failed');
}
displayComparisonResults(data);
} catch (error) {
compareError.textContent = error.message || 'An error occurred during comparison';
compareError.style.display = 'block';
} finally {
compareLoading.style.display = 'none';
this.disabled = false;
}
});
// 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');
// 1. Status Section
const isPass = data.status === 'PASS';
statusDiv.className = `status ${data.status.toLowerCase()}`;
const iconSvg = isPass
? '<svg class="status-icon-svg" width="32" height="32" fill="none" stroke="#059669" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/></svg>'
: '<svg class="status-icon-svg" width="32" height="32" fill="none" stroke="#DC2626" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M6 18L18 6M6 6l12 12"/></svg>';
statusDiv.innerHTML = `
<div class="status-icon">
${iconSvg}
</div>
<div class="status-content">
<h3>${isPass ? 'Validation Passed' : 'Validation Failed'}</h3>
<p>${data.summary || 'Validation run completed.'}</p>
</div>
`;
// Hide separate summary div
summaryDiv.style.display = 'none';
// 2. Elements Grid
elementsList.innerHTML = '';
elementsList.className = 'elements-grid'; // Ensure grid class is used
data.elements_report.forEach(element => {
const li = document.createElement('li');
li.className = `element-item ${element.is_present ? 'present' : 'missing'} ${!element.required ? 'optional' : ''}`;
let badgeClass = element.is_present ? 'badge-present' : 'badge-missing';
let badgeText = element.is_present ? 'PRESENT' : 'MISSING';
if (!element.required) {
badgeClass = 'badge-optional';
badgeText = 'OPTIONAL';
}
li.innerHTML = `
<div class="element-header">
<span class="element-label">${element.label}</span>
<span class="element-badge ${badgeClass}">${badgeText}</span>
</div>
<div class="element-reason">${element.reason}</div>
`;
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 resultsDiv = document.getElementById('results');
// Remove existing spell check section
const existing = resultsDiv.querySelectorAll('.spell-check-section');
existing.forEach(el => el.remove());
// 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 = `
<div style="display:flex; align-items:center; gap:12px;">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
Quality & Spelling Check
</div>
<span style="font-size: 14px; color: var(--text-secondary); font-weight: normal;">${spellCheck.summary}</span>
`;
spellSection.appendChild(header);
if (spellCheck.total_errors === 0) {
const noErrors = document.createElement('div');
noErrors.className = 'status pass';
noErrors.style.marginBottom = '0';
noErrors.style.display = 'flex';
noErrors.innerHTML = `
<div style="font-size: 20px;">✓</div>
<div>No quality or spelling errors found!</div>
`;
spellSection.appendChild(noErrors);
} else {
const grid = document.createElement('div');
grid.className = 'spell-errors-grid';
spellCheck.errors.forEach(error => {
const errorItem = document.createElement('div');
errorItem.className = 'element-item'; // Reuse card style
errorItem.style.borderLeft = '4px solid var(--warning)'; // Distinctive
const wordDiv = document.createElement('div');
wordDiv.style.marginBottom = '8px';
wordDiv.innerHTML = `<span style="font-weight:700; font-size:16px; color:#1F2937;">"${error.word}"</span> <span class="element-badge badge-missing" style="font-size:10px; margin-left:8px;">${error.error_type}</span>`;
errorItem.appendChild(wordDiv);
if (error.context) {
const contextDiv = document.createElement('div');
contextDiv.className = 'element-reason';
contextDiv.style.borderTop = 'none';
contextDiv.style.paddingLeft = '0';
contextDiv.style.fontStyle = 'italic';
contextDiv.textContent = `Context: "${error.context}"`;
errorItem.appendChild(contextDiv);
}
if (error.suggestions && error.suggestions.length > 0) {
const suggestionsDiv = document.createElement('div');
suggestionsDiv.style.marginTop = '12px';
suggestionsDiv.style.display = 'flex';
suggestionsDiv.style.gap = '8px';
suggestionsDiv.style.flexWrap = 'wrap';
error.suggestions.forEach(suggestion => {
const badge = document.createElement('span');
badge.className = 'element-badge badge-present';
badge.textContent = suggestion;
suggestionsDiv.appendChild(badge);
});
errorItem.appendChild(suggestionsDiv);
}
grid.appendChild(errorItem);
});
spellSection.appendChild(grid);
}
resultsDiv.appendChild(spellSection);
}
function displayLinkReport(linkReport) {
const resultsDiv = document.getElementById('results');
// Remove existing
const existing = resultsDiv.querySelectorAll('.link-validation-section');
existing.forEach(el => el.remove());
// Create link results section
const linkSection = document.createElement('div');
linkSection.className = 'link-validation-section';
// Inline styles to match refined look (or could add to CSS block)
linkSection.style.background = 'white';
linkSection.style.border = '1px solid var(--border)';
linkSection.style.borderRadius = 'var(--radius-lg)';
linkSection.style.padding = '24px';
linkSection.style.marginTop = '32px';
linkSection.style.boxShadow = 'var(--shadow-sm)';
const header = document.createElement('div');
header.className = 'link-validation-header';
header.innerHTML = `
<div style="display:flex; align-items:center; gap:12px; margin-bottom:16px; font-size:18px; font-weight:700; color:var(--text-primary); border-bottom:2px solid var(--bg-main); padding-bottom:16px;">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path></svg>
Link Validation
<span style="font-size: 14px; color: var(--text-secondary); font-weight: normal; margin-left: auto;">${linkReport.length} link(s) checked</span>
</div>
`;
linkSection.appendChild(header);
if (linkReport.length === 0) {
const noLinks = document.createElement('div');
noLinks.style.padding = '10px';
noLinks.style.color = 'var(--text-secondary)';
noLinks.style.fontStyle = 'italic';
noLinks.textContent = 'No links found in document.';
linkSection.appendChild(noLinks);
} else {
const list = document.createElement('ul');
list.className = 'link-list';
list.style.listStyle = 'none';
linkReport.forEach(link => {
const item = document.createElement('li');
let statusColor = 'var(--success)';
let borderColor = 'var(--success-bg)';
let bgColor = '#F0FDF4';
if (link.status === 'broken') {
statusColor = 'var(--error)';
borderColor = 'var(--error-bg)';
bgColor = '#FEF2F2';
}
if (link.status === 'warning') {
statusColor = 'var(--warning)';
borderColor = 'var(--warning-bg)';
bgColor = '#FFFBEB';
}
item.style.display = 'flex';
item.style.marginBottom = '10px';
item.style.padding = '12px';
item.style.background = bgColor;
item.style.border = `1px solid ${borderColor}`;
item.style.borderRadius = 'var(--radius-md)';
item.style.alignItems = 'center';
const leftDiv = document.createElement('div');
leftDiv.style.flex = '1';
leftDiv.style.marginRight = '10px';
leftDiv.style.overflow = 'hidden';
leftDiv.style.textOverflow = 'ellipsis';
const urlLink = document.createElement('a');
urlLink.href = link.url;
urlLink.target = '_blank';
urlLink.textContent = link.url;
urlLink.style.color = 'var(--primary)';
urlLink.style.fontWeight = '500';
urlLink.style.textDecoration = 'none';
leftDiv.appendChild(urlLink);
const statusSpan = document.createElement('span');
statusSpan.style.fontWeight = '700';
statusSpan.style.color = statusColor;
statusSpan.style.textTransform = 'uppercase';
statusSpan.style.fontSize = '12px';
statusSpan.textContent = link.status;
item.appendChild(leftDiv);
item.appendChild(statusSpan);
list.appendChild(item);
});
linkSection.appendChild(list);
}
resultsDiv.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'); // unused but cleared
// 1. Status Section
const hasErrors = data.spell_check && data.spell_check.total_errors > 0;
statusDiv.className = `status ${hasErrors ? 'fail' : 'pass'}`;
const iconSvg = hasErrors
? '<svg class="status-icon-svg" width="32" height="32" fill="none" stroke="#DC2626" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>'
: '<svg class="status-icon-svg" width="32" height="32" fill="none" stroke="#059669" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>';
statusDiv.innerHTML = `
<div class="status-icon">
${iconSvg}
</div>
<div class="status-content">
<h3>${hasErrors ? 'Quality Issues Found' : 'Text Quality Passed'}</h3>
<p>${data.summary || (hasErrors ? 'Issues detected in document text.' : 'No spelling or grammar issues found.')}</p>
</div>
`;
// Hide summary, clear elements
summaryDiv.style.display = 'none';
elementsList.innerHTML = '';
elementsList.className = 'elements-grid'; // Ensure grid class just in case
// Display spell check results if available
if (data.spell_check) {
displaySpellCheck(data.spell_check);
}
resultsDiv.style.display = 'block';
document.getElementById('error').style.display = 'none';
}
// Debug: Extract images
const debugBtn = document.getElementById('debugBtn');
if (debugBtn) {
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
// Create project modal handlers
const createProjectBtn = document.getElementById('createProjectBtn');
if (createProjectBtn) {
createProjectBtn.addEventListener('click', function () {
document.getElementById('createProjectModal').style.display = 'flex';
document.getElementById('projectName').value = '';
document.getElementById('projectDescription').value = '';
});
}
const cancelProjectBtn = document.getElementById('cancelProjectBtn');
if (cancelProjectBtn) {
cancelProjectBtn.addEventListener('click', function () {
document.getElementById('createProjectModal').style.display = 'none';
});
}
const saveProjectBtn = document.getElementById('saveProjectBtn');
if (saveProjectBtn) {
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
const viewProjectsBtn = document.getElementById('viewProjectsBtn');
if (viewProjectsBtn) {
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.');
});
}
// ==================== SHAREPOINT INTEGRATION ====================
const spState = {
token: localStorage.getItem('sharepoint_token'),
currentDriveId: null,
currentFolderId: null,
breadcrumbs: [],
selectedFiles: new Set()
};
// Initialize UI based on auth state
function updateSharePointUI() {
const connected = localStorage.getItem('sp_connected') === 'true';
// FIX: Use correct ID from HTML (sharepointAuthSection, not sharepointConnectSection)
const connectDiv = document.getElementById('sharepointAuthSection');
const actionsDiv = document.getElementById('sharepointActionsSection');
if (connectDiv) connectDiv.style.display = connected ? 'none' : 'flex';
if (actionsDiv) actionsDiv.style.display = connected ? 'block' : 'none';
}
updateSharePointUI();
// Connect Button Handler
const connectSharePointBtn = document.getElementById('connectSharePointBtn');
console.log('SharePoint Button found:', !!connectSharePointBtn); // Debug
if (connectSharePointBtn) {
connectSharePointBtn.addEventListener('click', async () => {
console.log('Connect Account button clicked!'); // Debug
// Open popup immediately to avoid blocker
const width = 600;
const height = 700;
const left = (window.screen.width - width) / 2;
const top = (window.screen.height - height) / 2;
// Use unique name to ensure new window every time
const popup = window.open(
'about:blank',
`SharePointLogin_${Date.now()}`,
`width=${width},height=${height},top=${top},left=${left}`
);
console.log('Popup result:', popup); // Debug
if (!popup) {
showError('Popup blocked! Please allow popups for this site.');
return;
}
// Safer way to set content
try {
popup.document.body.innerHTML = '<h3>Connecting to Microsoft...</h3><p>Please wait while we redirect you.</p>';
} catch (e) {
// Ignore modification errors if cross-origin or closed
console.warn('Could not set popup content', e);
}
try {
const response = await fetch('/auth/sharepoint/login');
const data = await response.json();
console.log('Auth response:', data); // Debug
if (response.ok && data.auth_url) {
if (!popup.closed) {
popup.location.href = data.auth_url;
}
} else {
if (!popup.closed) popup.close();
showError('Failed to get login URL');
}
} catch (error) {
console.error('Auth error:', error); // Debug
if (!popup.closed) popup.close();
showError('Failed to start login: ' + error.message);
}
});
} else {
console.error('SharePoint button NOT FOUND in DOM!');
}
// Logout
const logoutSharePointBtn = document.getElementById('logoutSharePointBtn');
if (logoutSharePointBtn) {
logoutSharePointBtn.addEventListener('click', () => {
localStorage.removeItem('sp_connected');
updateSharePointUI();
});
}
// Browse
const browseSharePointBtn = document.getElementById('browseSharePointBtn');
if (browseSharePointBtn) {
browseSharePointBtn.addEventListener('click', async () => {
document.getElementById('sharepointModal').style.display = 'flex';
await loadSharePointItems();
});
}
// Close Modal
const closeSharePointModal = document.getElementById('closeSharePointModal');
if (closeSharePointModal) {
closeSharePointModal.addEventListener('click', () => {
document.getElementById('sharepointModal').style.display = 'none';
});
}
// Back Button
const spBackBtn = document.getElementById('spBackBtn');
if (spBackBtn) {
spBackBtn.addEventListener('click', async () => {
if (currentPath.length > 0) {
currentPath.pop(); // Remove current folder
const parentFolder = currentPath.length > 0 ? currentPath[currentPath.length - 1] : null;
await loadSharePointItems(parentFolder ? parentFolder.id : null);
}
});
}
// Import Button
const spImportBtn = document.getElementById('spImportBtn');
if (spImportBtn) {
spImportBtn.addEventListener('click', async () => {
const checkboxes = document.querySelectorAll('.sp-item-checkbox:checked');
if (checkboxes.length === 0) {
alert('Please select at least one file to import.');
return;
}
const fileIds = Array.from(checkboxes).map(cb => cb.value);
const btn = document.getElementById('spImportBtn');
btn.disabled = true;
btn.textContent = 'Importing...';
try {
// This endpoint would handle downloading from Graph API and processing
// For now, we simulate success or need to implement the backend logic
const response = await fetch('/sharepoint/download-and-validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_ids: fileIds })
});
if (response.ok) {
const result = await response.json();
document.getElementById('sharepointModal').style.display = 'none';
// Refresh validation results or show success
displayResults(result);
} else {
throw new Error('Import failed');
}
} catch (e) {
alert('Error importing files: ' + e.message);
} finally {
btn.disabled = false;
btn.textContent = 'Import Selected';
}
});
}
// Load Items (Folder level)
async function loadItems(driveId, folderId = null) {
showLoadingList();
try {
let url = `/sharepoint/items?drive_id=${driveId}&token=${spState.token}`;
if (folderId) url += `&folder_id=${folderId}`;
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to load items');
const items = await response.json();
renderList(items.map(item => ({
id: item.id,
name: item.name,
type: item.type || (item.folder ? 'folder' : 'file'),
icon: item.folder ? '📁' : (item.name.endsWith('.pdf') ? '📄' : '📝'),
size: item.size
})));
} catch (error) {
handleSPError(error);
}
}
function renderList(items) {
const list = document.getElementById('spFileList');
list.innerHTML = '';
if (items.length === 0) {
list.innerHTML = '<div style="text-align: center; color: #999; padding: 20px;">No items found</div>';
return;
}
items.forEach(item => {
const div = document.createElement('div');
div.style.padding = '10px';
div.style.borderBottom = '1px solid #eee';
div.style.display = 'flex';
div.style.alignItems = 'center';
div.style.cursor = 'pointer';
div.className = 'sp-item';
// Selectable logic
const isSelected = spState.selectedFiles.has(item.id);
const isSelectable = item.type === 'file' && (item.name.endsWith('.pdf') || item.name.endsWith('.pptx') || item.name.endsWith('.docx'));
div.style.backgroundColor = isSelected ? '#e8f0fe' : 'white';
div.innerHTML = `
<span style="font-size: 20px; margin-right: 10px;">${item.icon}</span>
<span style="flex: 1;">${item.name}</span>
${item.size ? `<span style="font-size: 12px; color: #999;">${formatSize(item.size)}</span>` : ''}
`;
div.onclick = () => {
if (item.type === 'drive') {
spState.currentDriveId = item.id;
spState.breadcrumbs.push({ name: item.name, id: item.id, type: 'drive' });
loadItems(item.id);
updateBreadcrumbs();
} else if (item.type === 'folder') {
spState.currentFolderId = item.id;
spState.breadcrumbs.push({ name: item.name, id: item.id, type: 'folder' });
loadItems(spState.currentDriveId, item.id);
updateBreadcrumbs();
} else if (isSelectable) {
if (spState.selectedFiles.has(item.id)) {
spState.selectedFiles.delete(item.id);
div.style.backgroundColor = 'white';
} else {
spState.selectedFiles.add(item.id);
div.style.backgroundColor = '#e8f0fe';
}
updateSelectionCount();
}
};
list.appendChild(div);
});
updateSelectionCount();
}
function updateBreadcrumbs() {
const container = document.getElementById('spBreadcrumbs');
container.innerHTML = spState.breadcrumbs.map((b, i) => {
const isLast = i === spState.breadcrumbs.length - 1;
return `<span class="${!isLast ? 'breadcrumb-link' : ''}" style="${!isLast ? 'cursor: pointer; color: #0078d4; text-decoration: underline;' : 'font-weight: 600;'}" onclick="${!isLast ? `navigateBreadcrumb(${i})` : ''}">${b.name}</span>`;
}).join(' > ');
document.getElementById('spBackBtn').disabled = spState.breadcrumbs.length <= 1;
document.getElementById('spBackBtn').onclick = () => navigateBreadcrumb(spState.breadcrumbs.length - 2);
}
window.navigateBreadcrumb = (index) => {
if (index < 0) return;
const target = spState.breadcrumbs[index];
spState.breadcrumbs = spState.breadcrumbs.slice(0, index + 1);
if (target.id === null) { // Home
loadDrives();
} else if (target.type === 'drive') {
spState.currentDriveId = target.id;
spState.currentFolderId = null;
loadItems(target.id);
} else {
spState.currentFolderId = target.id;
loadItems(spState.currentDriveId, target.id);
}
updateBreadcrumbs();
};
function updateSelectionCount() {
const count = spState.selectedFiles.size;
document.getElementById('spSelectionCount').textContent = `${count} files selected`;
document.getElementById('spImportBtn').disabled = count === 0;
}
function showLoadingList() {
document.getElementById('spFileList').innerHTML = '<div style="text-align: center; padding: 20px;">Loading...</div>';
}
function handleSPError(error) {
if (error.message.includes('401') || error.message.includes('token')) {
spState.token = null;
localStorage.removeItem('sharepoint_token');
updateSharePointUI();
document.getElementById('sharePointModal').style.display = 'none';
showError('Session expired. Please connect again.');
} else {
document.getElementById('spFileList').innerHTML = `<div style="text-align: center; color: red; padding: 20px;">Error: ${error.message}</div>`;
}
}
function formatSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
// App Logout
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', async () => {
try {
// Call backend logout if it exists, or just redirect
await fetch('/logout', { method: 'POST' });
window.location.href = '/login';
} catch (e) {
// Fallback
window.location.href = '/login';
}
});
}
// Load templates when page loads
loadProjects();
loadTemplates();
</script>
</body>
</html>