Spaces:
Sleeping
Sleeping
feat: Add core medical document validation module with text/image extraction and initial static UI page.
edca77e
| <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 ; | |
| } | |
| .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> |