saifisvibin commited on
Commit
edca77e
·
1 Parent(s): 5c56bc9

feat: Add core medical document validation module with text/image extraction and initial static UI page.

Browse files
Files changed (2) hide show
  1. app/static/index.html +868 -577
  2. app/validator.py +84 -17
app/static/index.html CHANGED
@@ -379,112 +379,190 @@
379
  }
380
  }
381
 
 
382
  .results {
383
  display: none;
384
- margin-top: 30px;
385
- padding: 20px;
386
- border-radius: 8px;
387
- background: #f8f9fa;
 
 
 
 
 
 
 
 
 
 
388
  }
389
 
390
  .status {
391
- padding: 12px 20px;
392
- border-radius: 8px;
393
- margin-bottom: 20px;
394
- font-weight: 600;
395
- font-size: 18px;
 
 
 
396
  }
397
 
398
  .status.pass {
399
- background: #d4edda;
400
- color: #155724;
401
- border: 1px solid #c3e6cb;
402
  }
403
 
404
  .status.fail {
405
- background: #f8d7da;
406
- color: #721c24;
407
- border: 1px solid #f5c6cb;
408
  }
409
 
410
- .summary {
411
- margin-bottom: 20px;
412
- color: #333;
413
- font-size: 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  }
415
 
416
- .elements-list {
 
 
 
417
  list-style: none;
 
418
  }
419
 
420
  .element-item {
421
  background: white;
422
- padding: 15px;
423
- margin-bottom: 10px;
424
- border-radius: 6px;
425
- border-left: 4px solid #e0e0e0;
 
 
 
 
 
 
 
 
 
 
 
 
426
  }
427
 
428
- .element-item.present {
429
- border-left-color: #28a745;
 
 
 
 
 
 
 
 
 
430
  }
431
 
432
- .element-item.missing {
433
- border-left-color: #dc3545;
434
  }
435
 
436
- .element-item.optional {
437
- border-left-color: #ffc107;
438
  }
439
 
440
  .element-header {
441
  display: flex;
442
  justify-content: space-between;
443
- align-items: center;
444
- margin-bottom: 8px;
445
  }
446
 
447
  .element-label {
448
- font-weight: 600;
449
- color: #333;
 
 
 
450
  }
451
 
452
  .element-badge {
453
- padding: 4px 12px;
454
- border-radius: 12px;
455
- font-size: 12px;
456
- font-weight: 600;
 
 
 
457
  }
458
 
459
  .badge-present {
460
- background: #d4edda;
461
- color: #155724;
 
462
  }
463
 
464
  .badge-missing {
465
- background: #f8d7da;
466
- color: #721c24;
 
467
  }
468
 
469
  .badge-optional {
470
- background: #fff3cd;
471
- color: #856404;
 
472
  }
473
 
474
  .element-reason {
475
- color: #666;
476
  font-size: 14px;
477
- margin-top: 8px;
 
 
 
 
478
  }
479
 
480
  .error {
481
  display: none;
482
- background: #f8d7da;
483
- color: #721c24;
484
- padding: 15px;
485
- border-radius: 8px;
486
- margin-top: 20px;
487
- border: 1px solid #f5c6cb;
 
 
488
  }
489
 
490
  .templates-loading {
@@ -741,6 +819,41 @@
741
  background: #fff3cd;
742
  color: #856404;
743
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
744
  </style>
745
  </head>
746
 
@@ -800,252 +913,257 @@
800
  <p>Upload a document and select a template to validate against</p>
801
  </div>
802
 
803
- <!-- Project Selector Card -->
804
- <div class="card" style="padding: 16px;">
805
- <div style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap;">
806
- <div style="display: flex; align-items: center; gap: 8px;">
807
- <span style="font-size: 20px;">📂</span>
808
- <label style="font-weight: 600; margin: 0; white-space: nowrap;">Project:</label>
 
 
 
 
 
 
 
 
809
  </div>
810
- <select id="currentProject" style="flex: 1; min-width: 200px;">
811
- <option value="">No Project (Not Saved)</option>
812
- </select>
813
- <button type="button" class="btn btn-secondary" id="createProjectBtn">+ New</button>
814
- <button type="button" class="btn btn-secondary" id="viewProjectsBtn">View All</button>
815
  </div>
816
- </div>
817
 
818
- <!-- SharePoint Integration -->
819
- <div
820
- 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;">
821
- <div style="display: flex; align-items: center; gap: 10px;">
822
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
823
- <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"
824
- fill="#0078D4" />
825
- <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"
826
- fill="#50E6FF" fill-opacity="0.3" />
827
- <path d="M11.5 4V13H7.5V7H11.5V4Z" fill="#0078D4" fill-opacity="0.5" />
828
- </svg>
829
- <div>
830
- <strong style="display: block; color: #333;">Microsoft SharePoint / OneDrive</strong>
831
- <span style="font-size: 12px; color: #666;">Import documents directly from cloud</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
832
  </div>
833
  </div>
834
- <div id="sharepointAuthSection">
835
- <button type="button" class="btn-secondary" id="connectSharePointBtn"
836
- style="border: 1px solid #0078d4; color: #0078d4; background: white;">
837
- 🔗 Connect Account
838
- </button>
839
- </div>
840
- <div id="sharepointActionsSection" style="display: none;">
841
- <span id="sharepointStatus"
842
- style="font-size: 12px; color: #28a745; font-weight: 600; margin-right: 10px;">✓ Connected</span>
843
- <button type="button" class="btn-secondary" id="browseSharePointBtn"
844
- style="background: #0078d4; color: white; border: none;">
845
- 📂 Browse Files
846
- </button>
847
- <button type="button" class="btn-secondary" id="logoutSharePointBtn"
848
- style="background: #eee; border: 1px solid #ccc; font-size: 12px; padding: 5px 10px;">
849
- Disconnect
850
- </button>
851
- </div>
852
- </div>
853
 
854
- <!-- SharePoint File Browser Modal -->
855
- <div id="sharePointModal"
856
- style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 2000; align-items: center; justify-content: center;">
857
- <div
858
- style="background: white; padding: 0; border-radius: 8px; max-width: 800px; width: 90%; height: 80vh; display: flex; flex-direction: column; box-shadow: 0 10px 25px rgba(0,0,0,0.2);">
859
- <div
860
- style="padding: 20px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center;">
861
- <h2 style="margin: 0; font-size: 20px;">📂 Select Documents</h2>
862
- <button type="button" id="closeSharePointModal"
863
- style="background: none; border: none; font-size: 24px; cursor: pointer;">&times;</button>
864
  </div>
865
 
866
- <div
867
- style="padding: 10px 20px; background: #f8f9fa; border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 10px;">
868
- <button id="spBackBtn" disabled style="padding: 5px 10px; cursor: pointer;">⬅ Back</button>
869
- <div id="spBreadcrumbs"
870
- style="font-size: 14px; color: #666; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;">
871
- Home</div>
872
- </div>
 
873
 
874
- <div id="spFileList" style="flex: 1; overflow-y: auto; padding: 20px;">
875
- <!-- Drives/Files populated here -->
876
- <div style="text-align: center; color: #999;">Loading...</div>
877
- </div>
 
 
 
 
 
 
 
 
 
878
 
879
- <div style="padding: 20px; border-top: 1px solid #eee; background: #fcfcfc; text-align: right;">
880
- <span id="spSelectionCount" style="margin-right: 15px; color: #666; font-size: 14px;">0 files
881
- selected</span>
882
- <button type="button" class="btn" id="spImportBtn" disabled>Import & Validate</button>
883
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
884
  </div>
885
- </div>
886
 
887
- <!-- Create Project Modal -->
888
- <div id="createProjectModal"
889
- style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center;">
890
- <div
891
- style="background: white; padding: 30px; border-radius: 8px; max-width: 500px; width: 90%; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
892
- <h2 style="margin-top: 0;">Create New Project</h2>
893
- <div class="form-group">
894
- <label for="projectName">Project Name: *</label>
895
- <input type="text" id="projectName" placeholder="e.g., Cardiology Conference Q1 2025"
896
- style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
897
- </div>
898
- <div class="form-group">
899
- <label for="projectDescription">Description (Optional):</label>
900
- <textarea id="projectDescription" rows="3" placeholder="Brief description of this project..."
901
- style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; resize: vertical;"></textarea>
902
- </div>
903
- <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
904
- <button type="button" class="btn-secondary" id="cancelProjectBtn">Cancel</button>
905
- <button type="button" class="btn" id="saveProjectBtn">Create Project</button>
906
- </div>
907
  </div>
908
- </div>
909
 
910
- <!-- Validation Form Card -->
911
- <div class="card" id="validateSection">
912
- <div class="card-header">
913
- <div class="card-icon accent">✓</div>
914
- <h2>Validate Document</h2>
 
915
  </div>
 
916
 
917
- <form id="validationForm">
918
- <div class="form-group">
919
- <label for="templateSelect">Select Template <span class="optional">(Required for template
920
- validation)</span></label>
921
- <select id="templateSelect" name="template">
922
- <option value="">-- Select a template --</option>
923
- </select>
924
- </div>
925
 
926
- <div class="form-group">
927
- <label for="fileInput">Upload Document</label>
928
- <div class="file-upload-wrapper" id="dropZone">
929
- <div class="file-upload-icon">📄</div>
930
- <div class="file-upload-text">
931
- <strong>Click to upload</strong> or drag and drop<br>
932
- PDF, DOCX, or PPTX files
933
- </div>
934
- <input type="file" id="fileInput" name="file" accept=".pdf,.docx,.pptx" required
935
- style="display: none;">
 
 
 
 
 
 
936
  </div>
937
- <div class="file-info" id="fileInfo" style="display: none;"></div>
938
- </div>
939
 
940
- <div class="form-group">
941
- <label for="customPrompt">Custom Instructions <span class="optional">(Optional)</span></label>
942
- <textarea id="customPrompt" name="customPrompt" rows="3" maxlength="500"
943
- placeholder="e.g., 'Focus on date format validation' or 'Pay special attention to logo placement'..."></textarea>
944
- <div style="text-align: right; font-size: 12px; color: var(--text-muted); margin-top: 4px;">
945
- <span id="charCount">0</span>/500 characters
946
  </div>
947
  </div>
948
 
949
- <div class="button-group">
950
- <button type="button" class="btn btn-primary btn-lg" id="validateBtn">
951
- <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
952
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
953
- d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
954
- </svg>
955
- Validate Document
956
- </button>
957
- <button type="button" class="btn btn-secondary btn-lg" id="spellingOnlyBtn">
958
- <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
959
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
960
- 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">
961
- </path>
 
 
 
962
  </svg>
963
- Quality Check Only
964
- </button>
 
 
 
 
 
 
 
 
965
  </div>
966
- <p style="font-size: 13px; color: var(--text-muted); margin-top: 12px; text-align: center;">
967
- 💡 Use "Quality Check Only" for grammar and spelling without template validation
968
- </p>
969
- </form>
970
- </div>
971
 
972
- <div class="debug-section">
973
- <h3 style="margin-bottom: 10px; font-size: 18px;">🔍 Debug: Image Extraction</h3>
974
- <p style="color: #666; font-size: 14px; margin-bottom: 10px;">
975
- Test image extraction without full validation. Check console logs for detailed information.
976
- </p>
977
- <button type="button" class="debug-btn" id="debugBtn">Extract Images (Debug)</button>
978
- <div class="debug-info" id="debugInfo" style="display: none;"></div>
979
  </div>
980
 
981
- <!-- Document Comparison Section -->
982
- <div class="comparison-section"
983
- style="background: #f8f9fa; padding: 25px; border-radius: 8px; margin-top: 30px;">
984
- <h3 style="margin-bottom: 15px; font-size: 20px; color: #333;">🔄 Compare Documents</h3>
985
- <p style="color: #666; font-size: 14px; margin-bottom: 20px;">
986
- Upload two versions of a document to see what changed (e.g., before and after edits).
987
- </p>
988
-
989
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;">
990
- <div class="form-group">
991
- <label for="compareFile1">📄 Original Document:</label>
992
- <input type="file" id="compareFile1" accept=".pdf,.docx,.pptx">
993
- <div class="file-info" id="compareFileInfo1"></div>
994
- </div>
995
 
996
- <div class="form-group">
997
- <label for="compareFile2">📝 Modified Document:</label>
998
- <input type="file" id="compareFile2" accept=".pdf,.docx,.pptx">
999
- <div class="file-info" id="compareFileInfo2"></div>
 
1000
  </div>
1001
- </div>
1002
-
1003
- <button type="button" class="btn" id="compareBtn" style="width: 100%;">
1004
- 🔍 Compare Documents
1005
- </button>
1006
- </div>
1007
 
1008
- <!-- Bulk Certificate Validation Section -->
1009
- <div class="bulk-validation-section"
1010
- style="background: #f0f8ff; padding: 25px; border-radius: 8px; margin-top: 30px;">
1011
- <h3 style="margin-bottom: 15px; font-size: 20px; color: #333;">📋 Bulk Certificate Validation</h3>
1012
- <p style="color: #666; font-size: 14px; margin-bottom: 20px;">
1013
- Upload an Excel list of names and multiple certificates to verify all attendees received their
1014
- certificates.
1015
- </p>
1016
-
1017
- <!-- Step 1: Excel Upload -->
1018
- <div class="form-group" style="margin-bottom: 20px;">
1019
- <label for="excelFile">1️⃣ Upload Excel File with Names:</label>
1020
- <input type="file" id="excelFile" accept=".xlsx">
1021
- <div class="file-info" id="excelFileInfo"></div>
1022
- </div>
1023
 
1024
- <!-- Step 2: Column Selection -->
1025
- <div class="form-group" style="margin-bottom: 20px; display: none;" id="columnSelectorGroup">
1026
- <label for="nameColumn">2️⃣ Select Column Containing Names:</label>
1027
- <select id="nameColumn" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
1028
- <option value="">Loading columns...</option>
1029
- </select>
1030
- <div style="margin-top: 8px; color: #666; font-size: 13px;">
1031
- Preview: <span id="namePreview" style="font-weight: 500;"></span>
1032
  </div>
1033
- </div>
1034
 
1035
- <!-- Step 3: Certificates Upload -->
1036
- <div class="form-group" style="margin-bottom: 20px;">
1037
- <label for="certificateFiles">3️⃣ Upload Certificates (Max 150):</label>
1038
- <input type="file" id="certificateFiles" multiple accept=".pdf,.pptx"">
1039
- <div style=" margin-top: 8px;">
1040
- <span style="font-weight: 600; color: #007bff;" id="certCount">0</span>
1041
- <span style="color: #666;">/150 files selected</span>
1042
  </div>
1043
- </div>
1044
 
1045
- <!-- Step 4: Validate Button -->
1046
- <button type="button" class="btn" id="bulkValidateBtn" style="width: 100%;" disabled>
1047
- Validate All Certificates
1048
- </button>
 
 
 
 
 
1049
  </div>
1050
 
1051
  <div class="loading" id="loading">
@@ -1055,31 +1173,60 @@
1055
 
1056
  <div class="error" id="error"></div>
1057
 
1058
- <div class="results" id="results">
1059
- <div class="status" id="status"></div>
1060
- <div class="summary" id="summary"></div>
1061
- <ul class="elements-list" id="elementsList"></ul>
1062
- </div>
1063
-
1064
- <!-- Comparison Results -->
1065
- <div class="results" id="comparisonResults" style="display: none;">
1066
- <h2 style="margin-bottom: 20px;">📊 Comparison Results</h2>
1067
- <div id="comparisonSummary" style="margin-bottom: 20px;"></div>
1068
- <div id="comparisonDetails"></div>
1069
- </div>
1070
 
1071
- <!-- Bulk Validation Results -->
1072
- <div class="results" id="bulkResults" style="display: none;">
1073
- <h2 style="margin-bottom: 20px;">📊 Bulk Validation Results</h2>
1074
- <div id="bulkSummary" style="margin-bottom: 20px;"></div>
1075
- <button type="button" class="btn-secondary" id="downloadCSVBtn" style="margin-bottom: 20px;">
1076
- 📥 Download CSV Report
1077
- </button>
1078
- <div id="bulkDetails"></div>
1079
- </div>
1080
  </main>
1081
 
1082
  <script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1083
  // Load templates on page load
1084
  async function loadTemplates() {
1085
  try {
@@ -1173,29 +1320,44 @@
1173
  document.getElementById('compareBtn').addEventListener('click', async function () {
1174
  const file1 = document.getElementById('compareFile1').files[0];
1175
  const file2 = document.getElementById('compareFile2').files[0];
 
 
 
 
 
 
 
1176
 
1177
  if (!file1 || !file2) {
1178
- showError('Please select both documents to compare');
 
1179
  return;
1180
  }
1181
 
1182
  // Hide previous results
1183
  document.getElementById('results').style.display = 'none';
1184
  document.getElementById('comparisonResults').style.display = 'none';
1185
- document.getElementById('error').style.display = 'none';
1186
- document.getElementById('loading').querySelector('p').textContent = 'Comparing documents...';
1187
- document.getElementById('loading').style.display = 'block';
 
 
1188
 
1189
  try {
1190
  const formData = new FormData();
1191
  formData.append('file1', file1);
1192
  formData.append('file2', file2);
1193
 
 
 
 
1194
  const response = await fetch('/compare', {
1195
  method: 'POST',
1196
  body: formData
1197
  });
1198
 
 
 
1199
  const data = await response.json();
1200
 
1201
  if (!response.ok) {
@@ -1204,9 +1366,11 @@
1204
 
1205
  displayComparisonResults(data);
1206
  } catch (error) {
1207
- showError(error.message || 'An error occurred during comparison');
 
1208
  } finally {
1209
- document.getElementById('loading').style.display = 'none';
 
1210
  }
1211
  });
1212
 
@@ -1342,43 +1506,49 @@
1342
  const summaryDiv = document.getElementById('summary');
1343
  const elementsList = document.getElementById('elementsList');
1344
 
1345
- // Set status
1346
- statusDiv.textContent = data.status === 'PASS' ? '✓ Validation Passed' : '✗ Validation Failed';
1347
  statusDiv.className = `status ${data.status.toLowerCase()}`;
1348
 
1349
- // Set summary
1350
- summaryDiv.textContent = data.summary || 'Validation completed';
 
1351
 
1352
- // Clear and populate elements list
 
 
 
 
 
 
 
 
 
 
 
 
 
1353
  elementsList.innerHTML = '';
 
 
1354
  data.elements_report.forEach(element => {
1355
  const li = document.createElement('li');
1356
  li.className = `element-item ${element.is_present ? 'present' : 'missing'} ${!element.required ? 'optional' : ''}`;
1357
 
1358
- const header = document.createElement('div');
1359
- header.className = 'element-header';
1360
-
1361
- const label = document.createElement('span');
1362
- label.className = 'element-label';
1363
- label.textContent = element.label;
1364
-
1365
- const badge = document.createElement('span');
1366
- badge.className = `element-badge ${element.is_present ? 'badge-present' : 'badge-missing'} ${!element.required ? 'badge-optional' : ''}`;
1367
  if (!element.required) {
1368
- badge.textContent = 'Optional';
1369
- } else {
1370
- badge.textContent = element.is_present ? 'Present' : 'Missing';
1371
  }
1372
 
1373
- header.appendChild(label);
1374
- header.appendChild(badge);
1375
-
1376
- const reason = document.createElement('div');
1377
- reason.className = 'element-reason';
1378
- reason.textContent = element.reason;
1379
-
1380
- li.appendChild(header);
1381
- li.appendChild(reason);
1382
  elementsList.appendChild(li);
1383
  });
1384
 
@@ -1397,7 +1567,11 @@
1397
  }
1398
 
1399
  function displaySpellCheck(spellCheck) {
1400
- const elementsList = document.getElementById('elementsList');
 
 
 
 
1401
 
1402
  // Create spell check section
1403
  const spellSection = document.createElement('div');
@@ -1405,137 +1579,201 @@
1405
 
1406
  const header = document.createElement('div');
1407
  header.className = 'spell-check-header';
1408
- header.innerHTML = `✨ Quality & Spelling Check<span style="font-size: 14px; color: #666; font-weight: normal; margin-left: auto;">${spellCheck.summary}</span>`;
 
 
 
 
 
 
1409
  spellSection.appendChild(header);
1410
 
1411
  if (spellCheck.total_errors === 0) {
1412
  const noErrors = document.createElement('div');
1413
- noErrors.className = 'spell-check-no-errors';
1414
- noErrors.textContent = '✓ No quality or spelling errors found!';
 
 
 
 
 
1415
  spellSection.appendChild(noErrors);
1416
  } else {
 
 
 
1417
  spellCheck.errors.forEach(error => {
1418
  const errorItem = document.createElement('div');
1419
- errorItem.className = 'spell-error-item';
 
1420
 
1421
  const wordDiv = document.createElement('div');
1422
- wordDiv.className = 'spell-error-word';
1423
- wordDiv.innerHTML = `"${error.word}"<span class="spell-error-type type-${error.error_type}">${error.error_type}</span>`;
1424
  errorItem.appendChild(wordDiv);
1425
 
1426
  if (error.context) {
1427
  const contextDiv = document.createElement('div');
1428
- contextDiv.className = 'spell-error-context';
 
 
 
1429
  contextDiv.textContent = `Context: "${error.context}"`;
1430
  errorItem.appendChild(contextDiv);
1431
  }
1432
 
1433
  if (error.suggestions && error.suggestions.length > 0) {
1434
- const suggestionsLabel = document.createElement('div');
1435
- suggestionsLabel.textContent = 'Suggestions:';
1436
- suggestionsLabel.style.fontSize = '14px';
1437
- suggestionsLabel.style.marginTop = '8px';
1438
- suggestionsLabel.style.marginBottom = '6px';
1439
- suggestionsLabel.style.fontWeight = '600';
1440
- errorItem.appendChild(suggestionsLabel);
1441
-
1442
  const suggestionsDiv = document.createElement('div');
1443
- suggestionsDiv.className = 'spell-suggestions';
 
 
 
 
1444
  error.suggestions.forEach(suggestion => {
1445
- const suggestionSpan = document.createElement('span');
1446
- suggestionSpan.className = 'spell-suggestion';
1447
- suggestionSpan.textContent = suggestion;
1448
- suggestionsDiv.appendChild(suggestionSpan);
1449
  });
1450
  errorItem.appendChild(suggestionsDiv);
1451
  }
1452
 
1453
- spellSection.appendChild(errorItem);
1454
  });
 
1455
  }
1456
 
1457
- elementsList.appendChild(spellSection);
1458
  }
1459
 
1460
  function displayLinkReport(linkReport) {
1461
- const elementsList = document.getElementById('elementsList');
 
 
 
 
1462
 
1463
  // Create link results section
1464
  const linkSection = document.createElement('div');
1465
  linkSection.className = 'link-validation-section';
 
 
 
 
 
 
 
1466
 
1467
  const header = document.createElement('div');
1468
  header.className = 'link-validation-header';
1469
- header.innerHTML = `🔗 Link Validation <span style="font-size: 14px; color: #666; font-weight: normal; margin-left: auto;">${linkReport.length} link(s) checked</span>`;
 
 
 
 
 
 
1470
  linkSection.appendChild(header);
1471
 
1472
  if (linkReport.length === 0) {
1473
  const noLinks = document.createElement('div');
1474
  noLinks.style.padding = '10px';
1475
- noLinks.style.color = '#666';
1476
  noLinks.style.fontStyle = 'italic';
1477
  noLinks.textContent = 'No links found in document.';
1478
  linkSection.appendChild(noLinks);
1479
  } else {
1480
  const list = document.createElement('ul');
1481
  list.className = 'link-list';
 
1482
 
1483
  linkReport.forEach(link => {
1484
  const item = document.createElement('li');
1485
- let statusClass = 'valid';
1486
- if (link.status === 'broken') statusClass = 'broken';
1487
- if (link.status === 'warning') statusClass = 'warning';
 
 
 
 
 
 
 
 
 
 
 
1488
 
1489
- item.className = `link-item ${statusClass}`;
 
 
 
 
 
 
1490
 
1491
  const leftDiv = document.createElement('div');
1492
  leftDiv.style.flex = '1';
1493
  leftDiv.style.marginRight = '10px';
1494
  leftDiv.style.overflow = 'hidden';
1495
-
1496
- const urlDiv = document.createElement('div');
1497
- urlDiv.className = 'link-url';
1498
- urlDiv.textContent = link.url;
1499
- leftDiv.appendChild(urlDiv);
1500
-
1501
- const metaDiv = document.createElement('div');
1502
- metaDiv.className = 'link-meta';
1503
- const pageInfo = link.page !== 'Unknown' ? `Page: ${link.page}` : '';
1504
- metaDiv.textContent = `${pageInfo} ${link.message ? '• ' + link.message : ''}`;
1505
- leftDiv.appendChild(metaDiv);
1506
-
1507
- const badge = document.createElement('span');
1508
- badge.className = `link-status-badge status-${statusClass}`;
1509
- badge.textContent = link.status.toUpperCase();
 
 
1510
 
1511
  item.appendChild(leftDiv);
1512
- item.appendChild(badge);
1513
  list.appendChild(item);
1514
  });
1515
  linkSection.appendChild(list);
1516
  }
1517
 
1518
- elementsList.appendChild(linkSection);
1519
  }
1520
 
1521
  function displaySpellingOnlyResults(data) {
1522
  const resultsDiv = document.getElementById('results');
1523
  const statusDiv = document.getElementById('status');
1524
  const summaryDiv = document.getElementById('summary');
1525
- const elementsList = document.getElementById('elementsList');
1526
 
1527
- // Set status for spelling-only mode
1528
  const hasErrors = data.spell_check && data.spell_check.total_errors > 0;
1529
- statusDiv.textContent = hasErrors ? '⚠️ Issues Found' : '✓ Text Quality OK';
1530
  statusDiv.className = `status ${hasErrors ? 'fail' : 'pass'}`;
1531
 
1532
- // Set summary
1533
- summaryDiv.textContent = data.spell_check ? data.spell_check.summary : 'Spell check completed';
 
 
 
 
 
 
 
 
 
 
 
1534
 
1535
- // Clear elements list
 
1536
  elementsList.innerHTML = '';
 
1537
 
1538
- // Display spell check results
1539
  if (data.spell_check) {
1540
  displaySpellCheck(data.spell_check);
1541
  }
@@ -1545,81 +1783,85 @@
1545
  }
1546
 
1547
  // Debug: Extract images
1548
- document.getElementById('debugBtn').addEventListener('click', async function () {
1549
- const templateKey = document.getElementById('templateSelect').value;
1550
- const fileInput = document.getElementById('fileInput');
1551
- const file = fileInput.files[0];
1552
- const debugInfo = document.getElementById('debugInfo');
 
 
 
 
 
 
 
1553
 
1554
- if (!templateKey) {
1555
- alert('Please select a template first');
1556
- return;
1557
- }
1558
 
1559
- if (!file) {
1560
- alert('Please select a file first');
1561
- return;
1562
- }
1563
 
1564
- debugInfo.style.display = 'block';
1565
- debugInfo.innerHTML = '<p>Extracting images...</p>';
 
1566
 
1567
- try {
1568
- const formData = new FormData();
1569
- formData.append('file', file);
 
1570
 
1571
- const response = await fetch(`/debug/extract-images?template_key=${encodeURIComponent(templateKey)}`, {
1572
- method: 'POST',
1573
- body: formData
1574
- });
1575
 
1576
- const data = await response.json();
 
 
1577
 
1578
- if (!response.ok) {
1579
- throw new Error(data.detail || 'Extraction failed');
1580
- }
 
 
 
 
 
 
 
 
 
 
 
 
1581
 
1582
- // Format debug output
1583
- let output = '=== IMAGE EXTRACTION DEBUG ===\n\n';
1584
- output += `File: ${data.file_name}\n`;
1585
- output += `Size: ${(data.file_size_bytes / 1024).toFixed(2)} KB\n`;
1586
- output += `Text extracted: ${data.text_extracted ? 'Yes' : 'No'} (${data.text_length} chars)\n\n`;
1587
- output += `Images Found: ${data.images_found}\n`;
1588
- output += `Template Requires Visual Elements: ${data.template_requires_visual_elements ? 'Yes' : 'No'}\n\n`;
1589
-
1590
- if (data.template_visual_elements.length > 0) {
1591
- output += 'Template Visual Elements:\n';
1592
- data.template_visual_elements.forEach(elem => {
1593
- output += ` - ${elem.label} (${elem.type}) - Required: ${elem.required}\n`;
1594
- });
1595
- output += '\n';
1596
- }
 
 
 
 
1597
 
1598
- if (data.images.length > 0) {
1599
- output += 'Extracted Images:\n';
1600
- data.images.forEach((img, idx) => {
1601
- output += `\n${idx + 1}. ${img.id}\n`;
1602
- output += ` Path: ${img.file_path}\n`;
1603
- output += ` Exists: ${img.file_exists ? 'Yes' : 'No'}\n`;
1604
- output += ` Size: ${(img.file_size_bytes / 1024).toFixed(2)} KB\n`;
1605
- output += ` Dimensions: ${img.dimensions}\n`;
1606
- output += ` Mode: ${img.image_mode}\n`;
1607
- output += ` Role: ${img.role_hint}\n`;
1608
- output += ` Type: ${img.element_type}\n`;
1609
- });
1610
- } else {
1611
- output += '\n⚠️ No images were extracted from the document.\n';
1612
- output += 'This could mean:\n';
1613
- output += ' - The document has no embedded images\n';
1614
- output += ' - Images are in a format not supported\n';
1615
- output += ' - Images are embedded as external links\n';
1616
  }
 
 
1617
 
1618
- debugInfo.innerHTML = '<pre>' + output + '</pre>';
1619
- } catch (error) {
1620
- debugInfo.innerHTML = '<pre style="color: red;">Error: ' + error.message + '</pre>';
1621
- }
1622
- });
1623
 
1624
  // Function to display comparison results
1625
  function displayComparisonResults(data) {
@@ -1921,52 +2163,65 @@
1921
  }
1922
 
1923
  // Create project modal handlers
1924
- document.getElementById('createProjectBtn').addEventListener('click', function () {
1925
- document.getElementById('createProjectModal').style.display = 'flex';
1926
- document.getElementById('projectName').value = '';
1927
- document.getElementById('projectDescription').value = '';
1928
- });
 
 
 
 
1929
 
1930
- document.getElementById('cancelProjectBtn').addEventListener('click', function () {
1931
- document.getElementById('createProjectModal').style.display = 'none';
1932
- });
 
 
 
1933
 
1934
- document.getElementById('saveProjectBtn').addEventListener('click', async function () {
1935
- const name = document.getElementById('projectName').value.trim();
1936
- const description = document.getElementById('projectDescription').value.trim();
 
 
1937
 
1938
- if (!name) {
1939
- showError('Project name is required');
1940
- return;
1941
- }
1942
 
1943
- try {
1944
- const response = await fetch('/projects', {
1945
- method: 'POST',
1946
- headers: { 'Content-Type': 'application/json' },
1947
- body: JSON.stringify({ name, description })
1948
- });
1949
 
1950
- if (!response.ok) {
1951
- const error = await response.json();
1952
- throw new Error(error.detail || 'Failed to create project');
1953
- }
1954
 
1955
- const project = await response.json();
1956
- document.getElementById('createProjectModal').style.display = 'none';
1957
- await loadProjects();
1958
- document.getElementById('currentProject').value = project.id;
1959
- showError(''); // clear error
1960
- } catch (error) {
1961
- showError(error.message);
1962
- }
1963
- });
 
1964
 
1965
  // View all projects
1966
- document.getElementById('viewProjectsBtn').addEventListener('click', function () {
1967
- // For now, just alert - can be enhanced later
1968
- alert('Projects view coming soon! For now, use the dropdown to select projects.');
1969
- });
 
 
 
1970
 
1971
 
1972
  // ==================== SHAREPOINT INTEGRATION ====================
@@ -1978,89 +2233,152 @@
1978
  breadcrumbs: [],
1979
  selectedFiles: new Set()
1980
  };
1981
-
1982
  // Initialize UI based on auth state
1983
  function updateSharePointUI() {
1984
- if (spState.token) {
1985
- document.getElementById('sharepointAuthSection').style.display = 'none';
1986
- document.getElementById('sharepointActionsSection').style.display = 'block';
1987
- } else {
1988
- document.getElementById('sharepointAuthSection').style.display = 'block';
1989
- document.getElementById('sharepointActionsSection').style.display = 'none';
1990
- }
1991
  }
1992
  updateSharePointUI();
1993
 
1994
  // Connect Button Handler
1995
- document.getElementById('connectSharePointBtn').addEventListener('click', async () => {
1996
- try {
1997
- const response = await fetch('/auth/sharepoint/login');
1998
- const data = await response.json();
1999
-
2000
- // Open popup
2001
  const width = 600;
2002
  const height = 700;
2003
  const left = (window.screen.width - width) / 2;
2004
  const top = (window.screen.height - height) / 2;
2005
 
2006
- window.open(
2007
- data.auth_url,
2008
- 'SharePointLogin',
 
2009
  `width=${width},height=${height},top=${top},left=${left}`
2010
  );
2011
- } catch (error) {
2012
- showError('Failed to start login: ' + error.message);
2013
- }
2014
- });
2015
 
2016
- // Listen for auth message from popup
2017
- window.addEventListener('message', (event) => {
2018
- if (event.data.type === 'SHAREPOINT_AUTH') {
2019
- spState.token = event.data.token;
2020
- localStorage.setItem('sharepoint_token', spState.token);
2021
- updateSharePointUI();
2022
- showError(''); // Clear errors
2023
- }
2024
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2025
 
2026
  // Logout
2027
- document.getElementById('logoutSharePointBtn').addEventListener('click', () => {
2028
- spState.token = null;
2029
- localStorage.removeItem('sharepoint_token');
2030
- updateSharePointUI();
2031
- });
 
 
2032
 
2033
- // Browse Button
2034
- document.getElementById('browseSharePointBtn').addEventListener('click', () => {
2035
- document.getElementById('sharePointModal').style.display = 'flex';
2036
- loadDrives();
2037
- });
 
 
 
2038
 
2039
- document.getElementById('closeSharePointModal').addEventListener('click', () => {
2040
- document.getElementById('sharePointModal').style.display = 'none';
2041
- });
 
 
 
 
2042
 
2043
- // Load Drives (Root level)
2044
- async function loadDrives() {
2045
- showLoadingList();
2046
- try {
2047
- const response = await fetch(`/sharepoint/drives?token=${spState.token}`);
2048
- if (!response.ok) throw new Error('Failed to load drives');
2049
- const drives = await response.json();
2050
-
2051
- spState.currentDriveId = null;
2052
- spState.breadcrumbs = [{ name: 'Home', id: null }];
2053
- updateBreadcrumbs();
2054
-
2055
- renderList(drives.map(drive => ({
2056
- id: drive.id,
2057
- name: drive.name,
2058
- type: 'drive',
2059
- icon: '💽'
2060
- })));
2061
- } catch (error) {
2062
- handleSPError(error);
2063
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2064
  }
2065
 
2066
  // Load Items (Folder level)
@@ -2203,48 +2521,21 @@
2203
  return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
2204
  }
2205
 
2206
- // Import Button
2207
- document.getElementById('spImportBtn').addEventListener('click', async () => {
2208
- const fileIds = Array.from(spState.selectedFiles);
2209
- const btn = document.getElementById('spImportBtn');
2210
- btn.disabled = true;
2211
- btn.textContent = 'Importing...';
2212
-
2213
- try {
2214
- // Determine current project
2215
- const projectId = document.getElementById('currentProject').value || null;
2216
-
2217
- const response = await fetch('/sharepoint/download-and-validate', {
2218
- method: 'POST',
2219
- headers: { 'Content-Type': 'application/json' },
2220
- body: JSON.stringify({
2221
- drive_id: spState.currentDriveId,
2222
- file_ids: fileIds,
2223
- token: spState.token,
2224
- project_id: projectId ? parseInt(projectId) : null
2225
- })
2226
- });
2227
-
2228
- if (!response.ok) throw new Error('Import failed');
2229
-
2230
- const result = await response.json();
2231
- document.getElementById('sharePointModal').style.display = 'none';
2232
- spState.selectedFiles.clear();
2233
- updateSelectionCount();
2234
-
2235
- // Show success or redirect to results
2236
- showStatus('Successfully imported files! Check validation results below.', 'pass');
2237
-
2238
- // Trigger refresh of validations (if implemented) or just alert
2239
- alert('Files imported successfully! (Validation logic to be connected fully in next step)');
2240
 
2241
- } catch (error) {
2242
- showError('Import failed: ' + error.message);
2243
- } finally {
2244
- btn.disabled = false;
2245
- btn.textContent = 'Import & Validate';
2246
- }
2247
- });
2248
 
2249
  // Load templates when page loads
2250
  loadProjects();
 
379
  }
380
  }
381
 
382
+ /* Results Section Refined */
383
  .results {
384
  display: none;
385
+ margin-top: 32px;
386
+ animation: fadeIn 0.4s ease-out;
387
+ }
388
+
389
+ @keyframes fadeIn {
390
+ from {
391
+ opacity: 0;
392
+ transform: translateY(10px);
393
+ }
394
+
395
+ to {
396
+ opacity: 1;
397
+ transform: translateY(0);
398
+ }
399
  }
400
 
401
  .status {
402
+ display: flex;
403
+ align-items: center;
404
+ gap: 20px;
405
+ padding: 24px;
406
+ border-radius: var(--radius-lg);
407
+ margin-bottom: 32px;
408
+ border: 1px solid transparent;
409
+ box-shadow: var(--shadow-sm);
410
  }
411
 
412
  .status.pass {
413
+ background: #ECFDF5;
414
+ border-color: #A7F3D0;
415
+ color: #065F46;
416
  }
417
 
418
  .status.fail {
419
+ background: #FEF2F2;
420
+ border-color: #FECACA;
421
+ color: #991B1B;
422
  }
423
 
424
+ .status-icon {
425
+ width: 56px;
426
+ height: 56px;
427
+ border-radius: 50%;
428
+ display: flex;
429
+ align-items: center;
430
+ justify-content: center;
431
+ background: rgba(255, 255, 255, 0.6);
432
+ font-size: 28px;
433
+ flex-shrink: 0;
434
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
435
+ }
436
+
437
+ .status-content {
438
+ flex: 1;
439
+ }
440
+
441
+ .status-content h3 {
442
+ font-size: 20px;
443
+ font-weight: 700;
444
+ margin-bottom: 6px;
445
+ letter-spacing: -0.01em;
446
+ }
447
+
448
+ .status-content p {
449
+ font-size: 15px;
450
+ opacity: 0.9;
451
+ line-height: 1.5;
452
  }
453
 
454
+ .elements-grid {
455
+ display: grid;
456
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
457
+ gap: 20px;
458
  list-style: none;
459
+ margin-bottom: 32px;
460
  }
461
 
462
  .element-item {
463
  background: white;
464
+ border: 1px solid var(--border);
465
+ border-radius: var(--radius-md);
466
+ padding: 20px;
467
+ box-shadow: var(--shadow-sm);
468
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
469
+ position: relative;
470
+ overflow: hidden;
471
+ display: flex;
472
+ flex-direction: column;
473
+ height: 100%;
474
+ }
475
+
476
+ .element-item:hover {
477
+ transform: translateY(-4px);
478
+ box-shadow: var(--shadow-lg);
479
+ border-color: var(--primary-light);
480
  }
481
 
482
+ .element-item::before {
483
+ content: '';
484
+ position: absolute;
485
+ left: 0;
486
+ top: 0;
487
+ bottom: 0;
488
+ width: 6px;
489
+ }
490
+
491
+ .element-item.present::before {
492
+ background: var(--success);
493
  }
494
 
495
+ .element-item.missing::before {
496
+ background: var(--error);
497
  }
498
 
499
+ .element-item.optional::before {
500
+ background: var(--warning);
501
  }
502
 
503
  .element-header {
504
  display: flex;
505
  justify-content: space-between;
506
+ align-items: flex-start;
507
+ margin-bottom: 16px;
508
  }
509
 
510
  .element-label {
511
+ font-weight: 700;
512
+ font-size: 16px;
513
+ color: var(--text-primary);
514
+ padding-left: 12px;
515
+ line-height: 1.3;
516
  }
517
 
518
  .element-badge {
519
+ font-size: 11px;
520
+ font-weight: 700;
521
+ text-transform: uppercase;
522
+ padding: 6px 12px;
523
+ border-radius: 20px;
524
+ letter-spacing: 0.5px;
525
+ flex-shrink: 0;
526
  }
527
 
528
  .badge-present {
529
+ background: #E6FFFA;
530
+ color: #047481;
531
+ border: 1px solid #B2F5EA;
532
  }
533
 
534
  .badge-missing {
535
+ background: #FFF5F5;
536
+ color: #C53030;
537
+ border: 1px solid #FED7D7;
538
  }
539
 
540
  .badge-optional {
541
+ background: #FFFFF0;
542
+ color: #B7791F;
543
+ border: 1px solid #FEFCBF;
544
  }
545
 
546
  .element-reason {
547
+ color: var(--text-secondary);
548
  font-size: 14px;
549
+ margin-top: auto;
550
+ line-height: 1.5;
551
+ padding-left: 12px;
552
+ border-top: 1px solid #F3F4F6;
553
+ padding-top: 12px;
554
  }
555
 
556
  .error {
557
  display: none;
558
+ background: #FEF2F2;
559
+ color: #991B1B;
560
+ padding: 20px;
561
+ border-radius: var(--radius-md);
562
+ margin-top: 24px;
563
+ border: 1px solid #FECACA;
564
+ font-weight: 500;
565
+ text-align: center;
566
  }
567
 
568
  .templates-loading {
 
819
  background: #fff3cd;
820
  color: #856404;
821
  }
822
+
823
+ /* Loading animations */
824
+ @keyframes rotate {
825
+ 100% {
826
+ transform: rotate(360deg);
827
+ }
828
+ }
829
+
830
+ @keyframes dash {
831
+ 0% {
832
+ stroke-dashoffset: 80;
833
+ }
834
+
835
+ 50% {
836
+ stroke-dashoffset: 20;
837
+ }
838
+
839
+ 100% {
840
+ stroke-dashoffset: 80;
841
+ }
842
+ }
843
+
844
+ @keyframes progress {
845
+ 0% {
846
+ width: 5%;
847
+ }
848
+
849
+ 50% {
850
+ width: 70%;
851
+ }
852
+
853
+ 100% {
854
+ width: 95%;
855
+ }
856
+ }
857
  </style>
858
  </head>
859
 
 
913
  <p>Upload a document and select a template to validate against</p>
914
  </div>
915
 
916
+ <!-- VALIDATE PAGE -->
917
+ <div id="validatePage" class="page-section">
918
+ <!-- Project Selector Card -->
919
+ <div class="card" style="padding: 16px;">
920
+ <div style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap;">
921
+ <div style="display: flex; align-items: center; gap: 8px;">
922
+ <span style="font-size: 20px;">📂</span>
923
+ <label style="font-weight: 600; margin: 0; white-space: nowrap;">Project:</label>
924
+ </div>
925
+ <select id="currentProject" style="flex: 1; min-width: 200px;">
926
+ <option value="">No Project (Not Saved)</option>
927
+ </select>
928
+ <button type="button" class="btn btn-secondary" id="createProjectBtn">+ New</button>
929
+ <button type="button" class="btn btn-secondary" id="viewProjectsBtn">View All</button>
930
  </div>
 
 
 
 
 
931
  </div>
 
932
 
933
+ <!-- SharePoint Integration -->
934
+ <div
935
+ 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;">
936
+ <div style="display: flex; align-items: center; gap: 10px;">
937
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
938
+ <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"
939
+ fill="#0078D4" />
940
+ <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"
941
+ fill="#50E6FF" fill-opacity="0.3" />
942
+ <path d="M11.5 4V13H7.5V7H11.5V4Z" fill="#0078D4" fill-opacity="0.5" />
943
+ </svg>
944
+ <div>
945
+ <strong style="display: block; color: #333;">Microsoft SharePoint / OneDrive</strong>
946
+ <span style="font-size: 12px; color: #666;">Import documents directly from cloud</span>
947
+ </div>
948
+ </div>
949
+ <div id="sharepointAuthSection">
950
+ <button type="button" class="btn-secondary" id="connectSharePointBtn"
951
+ style="border: 1px solid #0078d4; color: #0078d4; background: white;">
952
+ 🔗 Connect Account
953
+ </button>
954
+ </div>
955
+ <div id="sharepointActionsSection" style="display: none;">
956
+ <span id="sharepointStatus"
957
+ style="font-size: 12px; color: #28a745; font-weight: 600; margin-right: 10px;">✓
958
+ Connected</span>
959
+ <button type="button" class="btn-secondary" id="browseSharePointBtn"
960
+ style="background: #0078d4; color: white; border: none;">
961
+ 📂 Browse Files
962
+ </button>
963
+ <button type="button" class="btn-secondary" id="logoutSharePointBtn"
964
+ style="background: #eee; border: 1px solid #ccc; font-size: 12px; padding: 5px 10px;">
965
+ Disconnect
966
+ </button>
967
  </div>
968
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
969
 
970
+ <!-- Validation Form Card -->
971
+ <div class="card" id="validateSection">
972
+ <div class="card-header">
973
+ <div class="card-icon accent">✓</div>
974
+ <h2>Validate Document</h2>
 
 
 
 
 
975
  </div>
976
 
977
+ <form id="validationForm">
978
+ <div class="form-group">
979
+ <label for="templateSelect">Select Template <span class="optional">(Required for template
980
+ validation)</span></label>
981
+ <select id="templateSelect" name="template">
982
+ <option value="">-- Select a template --</option>
983
+ </select>
984
+ </div>
985
 
986
+ <div class="form-group">
987
+ <label for="fileInput">Upload Document</label>
988
+ <div class="file-upload-wrapper" id="dropZone">
989
+ <div class="file-upload-icon">📄</div>
990
+ <div class="file-upload-text">
991
+ <strong>Click to upload</strong> or drag and drop<br>
992
+ PDF, DOCX, or PPTX files
993
+ </div>
994
+ <input type="file" id="fileInput" name="file" accept=".pdf,.docx,.pptx" required
995
+ style="display: none;">
996
+ </div>
997
+ <div class="file-info" id="fileInfo" style="display: none;"></div>
998
+ </div>
999
 
1000
+ <div class="form-group">
1001
+ <label for="customPrompt">Custom Instructions <span class="optional">(Optional)</span></label>
1002
+ <textarea id="customPrompt" name="customPrompt" rows="3" maxlength="500"
1003
+ placeholder="e.g., 'Focus on date format validation' or 'Pay special attention to logo placement'..."></textarea>
1004
+ <div style="text-align: right; font-size: 12px; color: var(--text-muted); margin-top: 4px;">
1005
+ <span id="charCount">0</span>/500 characters
1006
+ </div>
1007
+ </div>
1008
+
1009
+ <div class="button-group">
1010
+ <button type="button" class="btn btn-primary btn-lg" id="validateBtn">
1011
+ <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1012
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
1013
+ d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
1014
+ </svg>
1015
+ Validate Document
1016
+ </button>
1017
+ <button type="button" class="btn btn-secondary btn-lg" id="spellingOnlyBtn">
1018
+ <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1019
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
1020
+ 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">
1021
+ </path>
1022
+ </svg>
1023
+ Quality Check Only
1024
+ </button>
1025
+ </div>
1026
+ <p style="font-size: 13px; color: var(--text-muted); margin-top: 12px; text-align: center;">
1027
+ 💡 Use "Quality Check Only" for grammar and spelling without template validation
1028
+ </p>
1029
+ </form>
1030
  </div>
 
1031
 
1032
+ <div class="loading" id="loading">
1033
+ <div class="spinner"></div>
1034
+ <p>Validating document...</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1035
  </div>
 
1036
 
1037
+ <div class="error" id="error"></div>
1038
+
1039
+ <div class="results" id="results">
1040
+ <div class="status" id="status"></div>
1041
+ <div class="summary" id="summary"></div>
1042
+ <ul class="elements-list" id="elementsList"></ul>
1043
  </div>
1044
+ </div>
1045
 
 
 
 
 
 
 
 
 
1046
 
1047
+
1048
+ <!-- COMPARE PAGE -->
1049
+ <div id="comparePage" class="page-section" style="display: none;">
1050
+ <!-- Document Comparison Section -->
1051
+ <div class="comparison-section"
1052
+ style="background: #f8f9fa; padding: 25px; border-radius: 8px; margin-top: 30px;">
1053
+ <h3 style="margin-bottom: 15px; font-size: 20px; color: #333;">🔄 Compare Documents</h3>
1054
+ <p style="color: #666; font-size: 14px; margin-bottom: 20px;">
1055
+ Upload two versions of a document to see what changed (e.g., before and after edits).
1056
+ </p>
1057
+
1058
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;">
1059
+ <div class="form-group">
1060
+ <label for="compareFile1">📄 Original Document:</label>
1061
+ <input type="file" id="compareFile1" accept=".pdf,.docx,.pptx">
1062
+ <div class="file-info" id="compareFileInfo1"></div>
1063
  </div>
 
 
1064
 
1065
+ <div class="form-group">
1066
+ <label for="compareFile2">📝 Modified Document:</label>
1067
+ <input type="file" id="compareFile2" accept=".pdf,.docx,.pptx">
1068
+ <div class="file-info" id="compareFileInfo2"></div>
 
 
1069
  </div>
1070
  </div>
1071
 
1072
+ <button type="button" class="btn" id="compareBtn" style="width: 100%;">
1073
+ 🔍 Compare Documents
1074
+ </button>
1075
+
1076
+ <div class="error" id="compareError" style="margin-top: 15px;"></div>
1077
+
1078
+ <!-- Loading Indicator -->
1079
+ <div id="compareLoading"
1080
+ style="display: none; margin-top: 20px; text-align: center; padding: 40px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; color: white;">
1081
+ <div style="margin-bottom: 20px;">
1082
+ <svg width="60" height="60" viewBox="0 0 50 50" style="animation: rotate 2s linear infinite;">
1083
+ <circle cx="25" cy="25" r="20" fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="4">
1084
+ </circle>
1085
+ <circle cx="25" cy="25" r="20" fill="none" stroke="white" stroke-width="4"
1086
+ stroke-dasharray="80" stroke-dashoffset="60" stroke-linecap="round"
1087
+ style="animation: dash 1.5s ease-in-out infinite;"></circle>
1088
  </svg>
1089
+ </div>
1090
+ <h3 style="margin: 0 0 10px 0; font-size: 18px; font-weight: 600;">Comparing Documents...</h3>
1091
+ <p id="compareLoadingText" style="margin: 0; opacity: 0.9; font-size: 14px;">Extracting and
1092
+ analyzing content</p>
1093
+ <div
1094
+ style="margin-top: 15px; background: rgba(255,255,255,0.2); border-radius: 8px; height: 6px; overflow: hidden;">
1095
+ <div id="compareProgressBar"
1096
+ style="height: 100%; background: white; width: 0%; transition: width 0.5s ease; animation: progress 3s ease-in-out infinite;">
1097
+ </div>
1098
+ </div>
1099
  </div>
1100
+ </div>
 
 
 
 
1101
 
1102
+ <!-- Comparison Results inside Compare Page -->
1103
+ <div class="results" id="comparisonResults" style="display: none;">
1104
+ <h2 style="margin-bottom: 20px;">📊 Comparison Results</h2>
1105
+ <div id="comparisonSummary" style="margin-bottom: 20px;"></div>
1106
+ <div id="comparisonDetails"></div>
1107
+ </div>
 
1108
  </div>
1109
 
1110
+ <!-- BULK PAGE -->
1111
+ <div id="bulkPage" class="page-section" style="display: none;">
1112
+ <!-- Bulk Certificate Validation Section -->
1113
+ <div class="bulk-validation-section"
1114
+ style="background: #f0f8ff; padding: 25px; border-radius: 8px; margin-top: 30px;">
1115
+ <h3 style="margin-bottom: 15px; font-size: 20px; color: #333;">📋 Bulk Certificate Validation
1116
+ </h3>
1117
+ <p style="color: #666; font-size: 14px; margin-bottom: 20px;">
1118
+ Upload an Excel list of names and multiple certificates to verify all attendees received
1119
+ their
1120
+ certificates.
1121
+ </p>
 
 
1122
 
1123
+ <!-- Step 1: Excel Upload -->
1124
+ <div class="form-group" style="margin-bottom: 20px;">
1125
+ <label for="excelFile">1️⃣ Upload Excel File with Names:</label>
1126
+ <input type="file" id="excelFile" accept=".xlsx">
1127
+ <div class="file-info" id="excelFileInfo"></div>
1128
  </div>
 
 
 
 
 
 
1129
 
1130
+ <!-- Step 2: Column Selection -->
1131
+ <div class="form-group" style="margin-bottom: 20px; display: none;" id="columnSelectorGroup">
1132
+ <label for="nameColumn">2️⃣ Select Column Containing Names:</label>
1133
+ <select id="nameColumn"
1134
+ style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
1135
+ <option value="">Loading columns...</option>
1136
+ </select>
1137
+ <div style="margin-top: 8px; color: #666; font-size: 13px;">
1138
+ Preview: <span id="namePreview" style="font-weight: 500;"></span>
1139
+ </div>
1140
+ </div>
 
 
 
 
1141
 
1142
+ <!-- Step 3: Certificates Upload -->
1143
+ <div class="form-group" style="margin-bottom: 20px;">
1144
+ <label for="certificateFiles">3️⃣ Upload Certificates (Max 150):</label>
1145
+ <input type="file" id="certificateFiles" multiple accept=".pdf,.pptx">
1146
+ <div style=" margin-top: 8px;">
1147
+ <span style="font-weight: 600; color: #007bff;" id="certCount">0</span>
1148
+ <span style="color: #666;">/150 files selected</span>
1149
+ </div>
1150
  </div>
 
1151
 
1152
+ <!-- Step 4: Validate Button -->
1153
+ <button type="button" class="btn" id="bulkValidateBtn" style="width: 100%;" disabled>
1154
+ Validate All Certificates
1155
+ </button>
 
 
 
1156
  </div>
 
1157
 
1158
+ <!-- Bulk Validation Results inside Bulk Page -->
1159
+ <div class="results" id="bulkResults" style="display: none;">
1160
+ <h2 style="margin-bottom: 20px;">📊 Bulk Validation Results</h2>
1161
+ <div id="bulkSummary" style="margin-bottom: 20px;"></div>
1162
+ <button type="button" class="btn-secondary" id="downloadCSVBtn" style="margin-bottom: 20px;">
1163
+ 📥 Download CSV Report
1164
+ </button>
1165
+ <div id="bulkDetails"></div>
1166
+ </div>
1167
  </div>
1168
 
1169
  <div class="loading" id="loading">
 
1173
 
1174
  <div class="error" id="error"></div>
1175
 
 
 
 
 
 
 
 
 
 
 
 
 
1176
 
 
 
 
 
 
 
 
 
 
1177
  </main>
1178
 
1179
  <script>
1180
+ // Tab Navigation Logic
1181
+ document.addEventListener('DOMContentLoaded', () => {
1182
+ const navItems = document.querySelectorAll('.nav-item[data-tab]');
1183
+ const pages = {
1184
+ 'validate': document.getElementById('validatePage'),
1185
+ 'compare': document.getElementById('comparePage'),
1186
+ 'bulk': document.getElementById('bulkPage')
1187
+ };
1188
+ const pageHeaderTitle = document.querySelector('.page-header h1');
1189
+ const pageHeaderDesc = document.querySelector('.page-header p');
1190
+
1191
+ const pageInfo = {
1192
+ 'validate': { title: 'Document Validation', desc: 'Upload a document and select a template to validate against' },
1193
+ 'compare': { title: 'Compare Documents', desc: 'Upload two versions of a document to see differences' },
1194
+ 'bulk': { title: 'Bulk Certificate Validation', desc: 'Validate multiple certificates against an Excel list' }
1195
+ };
1196
+
1197
+ navItems.forEach(item => {
1198
+ item.addEventListener('click', () => {
1199
+ const tabName = item.getAttribute('data-tab');
1200
+
1201
+ // Update Sidebar
1202
+ navItems.forEach(nav => nav.classList.remove('active'));
1203
+ item.classList.add('active');
1204
+
1205
+ // Update Pages
1206
+ Object.values(pages).forEach(page => {
1207
+ if (page) page.style.display = 'none';
1208
+ });
1209
+ if (pages[tabName]) {
1210
+ pages[tabName].style.display = 'block';
1211
+
1212
+ // Update Header
1213
+ if (pageInfo[tabName]) {
1214
+ pageHeaderTitle.textContent = pageInfo[tabName].title;
1215
+ pageHeaderDesc.textContent = pageInfo[tabName].desc;
1216
+ }
1217
+ }
1218
+
1219
+ // Clear and hide global error on tab switch
1220
+ const errorDiv = document.getElementById('error');
1221
+ errorDiv.style.display = 'none';
1222
+ errorDiv.textContent = '';
1223
+ });
1224
+ });
1225
+
1226
+ // Initial load of templates
1227
+ loadTemplates();
1228
+ });
1229
+
1230
  // Load templates on page load
1231
  async function loadTemplates() {
1232
  try {
 
1320
  document.getElementById('compareBtn').addEventListener('click', async function () {
1321
  const file1 = document.getElementById('compareFile1').files[0];
1322
  const file2 = document.getElementById('compareFile2').files[0];
1323
+ const compareError = document.getElementById('compareError');
1324
+ const compareLoading = document.getElementById('compareLoading');
1325
+ const loadingText = document.getElementById('compareLoadingText');
1326
+
1327
+ // Clear previous errors
1328
+ compareError.style.display = 'none';
1329
+ compareError.textContent = '';
1330
 
1331
  if (!file1 || !file2) {
1332
+ compareError.textContent = 'Please select both documents to compare';
1333
+ compareError.style.display = 'block';
1334
  return;
1335
  }
1336
 
1337
  // Hide previous results
1338
  document.getElementById('results').style.display = 'none';
1339
  document.getElementById('comparisonResults').style.display = 'none';
1340
+
1341
+ // Show loading indicator
1342
+ compareLoading.style.display = 'block';
1343
+ loadingText.textContent = 'Extracting text from documents...';
1344
+ this.disabled = true;
1345
 
1346
  try {
1347
  const formData = new FormData();
1348
  formData.append('file1', file1);
1349
  formData.append('file2', file2);
1350
 
1351
+ // Update status
1352
+ loadingText.textContent = 'Analyzing differences with AI...';
1353
+
1354
  const response = await fetch('/compare', {
1355
  method: 'POST',
1356
  body: formData
1357
  });
1358
 
1359
+ loadingText.textContent = 'Processing results...';
1360
+
1361
  const data = await response.json();
1362
 
1363
  if (!response.ok) {
 
1366
 
1367
  displayComparisonResults(data);
1368
  } catch (error) {
1369
+ compareError.textContent = error.message || 'An error occurred during comparison';
1370
+ compareError.style.display = 'block';
1371
  } finally {
1372
+ compareLoading.style.display = 'none';
1373
+ this.disabled = false;
1374
  }
1375
  });
1376
 
 
1506
  const summaryDiv = document.getElementById('summary');
1507
  const elementsList = document.getElementById('elementsList');
1508
 
1509
+ // 1. Status Section
1510
+ const isPass = data.status === 'PASS';
1511
  statusDiv.className = `status ${data.status.toLowerCase()}`;
1512
 
1513
+ const iconSvg = isPass
1514
+ ? '<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>'
1515
+ : '<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>';
1516
 
1517
+ statusDiv.innerHTML = `
1518
+ <div class="status-icon">
1519
+ ${iconSvg}
1520
+ </div>
1521
+ <div class="status-content">
1522
+ <h3>${isPass ? 'Validation Passed' : 'Validation Failed'}</h3>
1523
+ <p>${data.summary || 'Validation run completed.'}</p>
1524
+ </div>
1525
+ `;
1526
+
1527
+ // Hide separate summary div
1528
+ summaryDiv.style.display = 'none';
1529
+
1530
+ // 2. Elements Grid
1531
  elementsList.innerHTML = '';
1532
+ elementsList.className = 'elements-grid'; // Ensure grid class is used
1533
+
1534
  data.elements_report.forEach(element => {
1535
  const li = document.createElement('li');
1536
  li.className = `element-item ${element.is_present ? 'present' : 'missing'} ${!element.required ? 'optional' : ''}`;
1537
 
1538
+ let badgeClass = element.is_present ? 'badge-present' : 'badge-missing';
1539
+ let badgeText = element.is_present ? 'PRESENT' : 'MISSING';
 
 
 
 
 
 
 
1540
  if (!element.required) {
1541
+ badgeClass = 'badge-optional';
1542
+ badgeText = 'OPTIONAL';
 
1543
  }
1544
 
1545
+ li.innerHTML = `
1546
+ <div class="element-header">
1547
+ <span class="element-label">${element.label}</span>
1548
+ <span class="element-badge ${badgeClass}">${badgeText}</span>
1549
+ </div>
1550
+ <div class="element-reason">${element.reason}</div>
1551
+ `;
 
 
1552
  elementsList.appendChild(li);
1553
  });
1554
 
 
1567
  }
1568
 
1569
  function displaySpellCheck(spellCheck) {
1570
+ const resultsDiv = document.getElementById('results');
1571
+
1572
+ // Remove existing spell check section
1573
+ const existing = resultsDiv.querySelectorAll('.spell-check-section');
1574
+ existing.forEach(el => el.remove());
1575
 
1576
  // Create spell check section
1577
  const spellSection = document.createElement('div');
 
1579
 
1580
  const header = document.createElement('div');
1581
  header.className = 'spell-check-header';
1582
+ header.innerHTML = `
1583
+ <div style="display:flex; align-items:center; gap:12px;">
1584
+ <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>
1585
+ Quality & Spelling Check
1586
+ </div>
1587
+ <span style="font-size: 14px; color: var(--text-secondary); font-weight: normal;">${spellCheck.summary}</span>
1588
+ `;
1589
  spellSection.appendChild(header);
1590
 
1591
  if (spellCheck.total_errors === 0) {
1592
  const noErrors = document.createElement('div');
1593
+ noErrors.className = 'status pass';
1594
+ noErrors.style.marginBottom = '0';
1595
+ noErrors.style.display = 'flex';
1596
+ noErrors.innerHTML = `
1597
+ <div style="font-size: 20px;">✓</div>
1598
+ <div>No quality or spelling errors found!</div>
1599
+ `;
1600
  spellSection.appendChild(noErrors);
1601
  } else {
1602
+ const grid = document.createElement('div');
1603
+ grid.className = 'spell-errors-grid';
1604
+
1605
  spellCheck.errors.forEach(error => {
1606
  const errorItem = document.createElement('div');
1607
+ errorItem.className = 'element-item'; // Reuse card style
1608
+ errorItem.style.borderLeft = '4px solid var(--warning)'; // Distinctive
1609
 
1610
  const wordDiv = document.createElement('div');
1611
+ wordDiv.style.marginBottom = '8px';
1612
+ 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>`;
1613
  errorItem.appendChild(wordDiv);
1614
 
1615
  if (error.context) {
1616
  const contextDiv = document.createElement('div');
1617
+ contextDiv.className = 'element-reason';
1618
+ contextDiv.style.borderTop = 'none';
1619
+ contextDiv.style.paddingLeft = '0';
1620
+ contextDiv.style.fontStyle = 'italic';
1621
  contextDiv.textContent = `Context: "${error.context}"`;
1622
  errorItem.appendChild(contextDiv);
1623
  }
1624
 
1625
  if (error.suggestions && error.suggestions.length > 0) {
 
 
 
 
 
 
 
 
1626
  const suggestionsDiv = document.createElement('div');
1627
+ suggestionsDiv.style.marginTop = '12px';
1628
+ suggestionsDiv.style.display = 'flex';
1629
+ suggestionsDiv.style.gap = '8px';
1630
+ suggestionsDiv.style.flexWrap = 'wrap';
1631
+
1632
  error.suggestions.forEach(suggestion => {
1633
+ const badge = document.createElement('span');
1634
+ badge.className = 'element-badge badge-present';
1635
+ badge.textContent = suggestion;
1636
+ suggestionsDiv.appendChild(badge);
1637
  });
1638
  errorItem.appendChild(suggestionsDiv);
1639
  }
1640
 
1641
+ grid.appendChild(errorItem);
1642
  });
1643
+ spellSection.appendChild(grid);
1644
  }
1645
 
1646
+ resultsDiv.appendChild(spellSection);
1647
  }
1648
 
1649
  function displayLinkReport(linkReport) {
1650
+ const resultsDiv = document.getElementById('results');
1651
+
1652
+ // Remove existing
1653
+ const existing = resultsDiv.querySelectorAll('.link-validation-section');
1654
+ existing.forEach(el => el.remove());
1655
 
1656
  // Create link results section
1657
  const linkSection = document.createElement('div');
1658
  linkSection.className = 'link-validation-section';
1659
+ // Inline styles to match refined look (or could add to CSS block)
1660
+ linkSection.style.background = 'white';
1661
+ linkSection.style.border = '1px solid var(--border)';
1662
+ linkSection.style.borderRadius = 'var(--radius-lg)';
1663
+ linkSection.style.padding = '24px';
1664
+ linkSection.style.marginTop = '32px';
1665
+ linkSection.style.boxShadow = 'var(--shadow-sm)';
1666
 
1667
  const header = document.createElement('div');
1668
  header.className = 'link-validation-header';
1669
+ header.innerHTML = `
1670
+ <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;">
1671
+ <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>
1672
+ Link Validation
1673
+ <span style="font-size: 14px; color: var(--text-secondary); font-weight: normal; margin-left: auto;">${linkReport.length} link(s) checked</span>
1674
+ </div>
1675
+ `;
1676
  linkSection.appendChild(header);
1677
 
1678
  if (linkReport.length === 0) {
1679
  const noLinks = document.createElement('div');
1680
  noLinks.style.padding = '10px';
1681
+ noLinks.style.color = 'var(--text-secondary)';
1682
  noLinks.style.fontStyle = 'italic';
1683
  noLinks.textContent = 'No links found in document.';
1684
  linkSection.appendChild(noLinks);
1685
  } else {
1686
  const list = document.createElement('ul');
1687
  list.className = 'link-list';
1688
+ list.style.listStyle = 'none';
1689
 
1690
  linkReport.forEach(link => {
1691
  const item = document.createElement('li');
1692
+ let statusColor = 'var(--success)';
1693
+ let borderColor = 'var(--success-bg)';
1694
+ let bgColor = '#F0FDF4';
1695
+
1696
+ if (link.status === 'broken') {
1697
+ statusColor = 'var(--error)';
1698
+ borderColor = 'var(--error-bg)';
1699
+ bgColor = '#FEF2F2';
1700
+ }
1701
+ if (link.status === 'warning') {
1702
+ statusColor = 'var(--warning)';
1703
+ borderColor = 'var(--warning-bg)';
1704
+ bgColor = '#FFFBEB';
1705
+ }
1706
 
1707
+ item.style.display = 'flex';
1708
+ item.style.marginBottom = '10px';
1709
+ item.style.padding = '12px';
1710
+ item.style.background = bgColor;
1711
+ item.style.border = `1px solid ${borderColor}`;
1712
+ item.style.borderRadius = 'var(--radius-md)';
1713
+ item.style.alignItems = 'center';
1714
 
1715
  const leftDiv = document.createElement('div');
1716
  leftDiv.style.flex = '1';
1717
  leftDiv.style.marginRight = '10px';
1718
  leftDiv.style.overflow = 'hidden';
1719
+ leftDiv.style.textOverflow = 'ellipsis';
1720
+
1721
+ const urlLink = document.createElement('a');
1722
+ urlLink.href = link.url;
1723
+ urlLink.target = '_blank';
1724
+ urlLink.textContent = link.url;
1725
+ urlLink.style.color = 'var(--primary)';
1726
+ urlLink.style.fontWeight = '500';
1727
+ urlLink.style.textDecoration = 'none';
1728
+ leftDiv.appendChild(urlLink);
1729
+
1730
+ const statusSpan = document.createElement('span');
1731
+ statusSpan.style.fontWeight = '700';
1732
+ statusSpan.style.color = statusColor;
1733
+ statusSpan.style.textTransform = 'uppercase';
1734
+ statusSpan.style.fontSize = '12px';
1735
+ statusSpan.textContent = link.status;
1736
 
1737
  item.appendChild(leftDiv);
1738
+ item.appendChild(statusSpan);
1739
  list.appendChild(item);
1740
  });
1741
  linkSection.appendChild(list);
1742
  }
1743
 
1744
+ resultsDiv.appendChild(linkSection);
1745
  }
1746
 
1747
  function displaySpellingOnlyResults(data) {
1748
  const resultsDiv = document.getElementById('results');
1749
  const statusDiv = document.getElementById('status');
1750
  const summaryDiv = document.getElementById('summary');
1751
+ const elementsList = document.getElementById('elementsList'); // unused but cleared
1752
 
1753
+ // 1. Status Section
1754
  const hasErrors = data.spell_check && data.spell_check.total_errors > 0;
 
1755
  statusDiv.className = `status ${hasErrors ? 'fail' : 'pass'}`;
1756
 
1757
+ const iconSvg = hasErrors
1758
+ ? '<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>'
1759
+ : '<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>';
1760
+
1761
+ statusDiv.innerHTML = `
1762
+ <div class="status-icon">
1763
+ ${iconSvg}
1764
+ </div>
1765
+ <div class="status-content">
1766
+ <h3>${hasErrors ? 'Quality Issues Found' : 'Text Quality Passed'}</h3>
1767
+ <p>${data.summary || (hasErrors ? 'Issues detected in document text.' : 'No spelling or grammar issues found.')}</p>
1768
+ </div>
1769
+ `;
1770
 
1771
+ // Hide summary, clear elements
1772
+ summaryDiv.style.display = 'none';
1773
  elementsList.innerHTML = '';
1774
+ elementsList.className = 'elements-grid'; // Ensure grid class just in case
1775
 
1776
+ // Display spell check results if available
1777
  if (data.spell_check) {
1778
  displaySpellCheck(data.spell_check);
1779
  }
 
1783
  }
1784
 
1785
  // Debug: Extract images
1786
+ const debugBtn = document.getElementById('debugBtn');
1787
+ if (debugBtn) {
1788
+ debugBtn.addEventListener('click', async function () {
1789
+ const templateKey = document.getElementById('templateSelect').value;
1790
+ const fileInput = document.getElementById('fileInput');
1791
+ const file = fileInput.files[0];
1792
+ const debugInfo = document.getElementById('debugInfo');
1793
+
1794
+ if (!templateKey) {
1795
+ alert('Please select a template first');
1796
+ return;
1797
+ }
1798
 
1799
+ if (!file) {
1800
+ alert('Please select a file first');
1801
+ return;
1802
+ }
1803
 
1804
+ debugInfo.style.display = 'block';
1805
+ debugInfo.innerHTML = '<p>Extracting images...</p>';
 
 
1806
 
1807
+ try {
1808
+ const formData = new FormData();
1809
+ formData.append('file', file);
1810
 
1811
+ const response = await fetch(`/debug/extract-images?template_key=${encodeURIComponent(templateKey)}`, {
1812
+ method: 'POST',
1813
+ body: formData
1814
+ });
1815
 
1816
+ const data = await response.json();
 
 
 
1817
 
1818
+ if (!response.ok) {
1819
+ throw new Error(data.detail || 'Extraction failed');
1820
+ }
1821
 
1822
+ // Format debug output
1823
+ let output = '=== IMAGE EXTRACTION DEBUG ===\n\n';
1824
+ output += `File: ${data.file_name}\n`;
1825
+ output += `Size: ${(data.file_size_bytes / 1024).toFixed(2)} KB\n`;
1826
+ output += `Text extracted: ${data.text_extracted ? 'Yes' : 'No'} (${data.text_length} chars)\n\n`;
1827
+ output += `Images Found: ${data.images_found}\n`;
1828
+ output += `Template Requires Visual Elements: ${data.template_requires_visual_elements ? 'Yes' : 'No'}\n\n`;
1829
+
1830
+ if (data.template_visual_elements.length > 0) {
1831
+ output += 'Template Visual Elements:\n';
1832
+ data.template_visual_elements.forEach(elem => {
1833
+ output += ` - ${elem.label} (${elem.type}) - Required: ${elem.required}\n`;
1834
+ });
1835
+ output += '\n';
1836
+ }
1837
 
1838
+ if (data.images.length > 0) {
1839
+ output += 'Extracted Images:\n';
1840
+ data.images.forEach((img, idx) => {
1841
+ output += `\n${idx + 1}. ${img.id}\n`;
1842
+ output += ` Path: ${img.file_path}\n`;
1843
+ output += ` Exists: ${img.file_exists ? 'Yes' : 'No'}\n`;
1844
+ output += ` Size: ${(img.file_size_bytes / 1024).toFixed(2)} KB\n`;
1845
+ output += ` Dimensions: ${img.dimensions}\n`;
1846
+ output += ` Mode: ${img.image_mode}\n`;
1847
+ output += ` Role: ${img.role_hint}\n`;
1848
+ output += ` Type: ${img.element_type}\n`;
1849
+ });
1850
+ } else {
1851
+ output += '\n⚠️ No images were extracted from the document.\n';
1852
+ output += 'This could mean:\n';
1853
+ output += ' - The document has no embedded images\n';
1854
+ output += ' - Images are in a format not supported\n';
1855
+ output += ' - Images are embedded as external links\n';
1856
+ }
1857
 
1858
+ debugInfo.innerHTML = '<pre>' + output + '</pre>';
1859
+ } catch (error) {
1860
+ debugInfo.innerHTML = '<pre style="color: red;">Error: ' + error.message + '</pre>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1861
  }
1862
+ });
1863
+ }
1864
 
 
 
 
 
 
1865
 
1866
  // Function to display comparison results
1867
  function displayComparisonResults(data) {
 
2163
  }
2164
 
2165
  // Create project modal handlers
2166
+ // Create project modal handlers
2167
+ const createProjectBtn = document.getElementById('createProjectBtn');
2168
+ if (createProjectBtn) {
2169
+ createProjectBtn.addEventListener('click', function () {
2170
+ document.getElementById('createProjectModal').style.display = 'flex';
2171
+ document.getElementById('projectName').value = '';
2172
+ document.getElementById('projectDescription').value = '';
2173
+ });
2174
+ }
2175
 
2176
+ const cancelProjectBtn = document.getElementById('cancelProjectBtn');
2177
+ if (cancelProjectBtn) {
2178
+ cancelProjectBtn.addEventListener('click', function () {
2179
+ document.getElementById('createProjectModal').style.display = 'none';
2180
+ });
2181
+ }
2182
 
2183
+ const saveProjectBtn = document.getElementById('saveProjectBtn');
2184
+ if (saveProjectBtn) {
2185
+ saveProjectBtn.addEventListener('click', async function () {
2186
+ const name = document.getElementById('projectName').value.trim();
2187
+ const description = document.getElementById('projectDescription').value.trim();
2188
 
2189
+ if (!name) {
2190
+ showError('Project name is required');
2191
+ return;
2192
+ }
2193
 
2194
+ try {
2195
+ const response = await fetch('/projects', {
2196
+ method: 'POST',
2197
+ headers: { 'Content-Type': 'application/json' },
2198
+ body: JSON.stringify({ name, description })
2199
+ });
2200
 
2201
+ if (!response.ok) {
2202
+ const error = await response.json();
2203
+ throw new Error(error.detail || 'Failed to create project');
2204
+ }
2205
 
2206
+ const project = await response.json();
2207
+ document.getElementById('createProjectModal').style.display = 'none';
2208
+ await loadProjects();
2209
+ document.getElementById('currentProject').value = project.id;
2210
+ showError(''); // clear error
2211
+ } catch (error) {
2212
+ showError(error.message);
2213
+ }
2214
+ });
2215
+ }
2216
 
2217
  // View all projects
2218
+ const viewProjectsBtn = document.getElementById('viewProjectsBtn');
2219
+ if (viewProjectsBtn) {
2220
+ viewProjectsBtn.addEventListener('click', function () {
2221
+ // For now, just alert - can be enhanced later
2222
+ alert('Projects view coming soon! For now, use the dropdown to select projects.');
2223
+ });
2224
+ }
2225
 
2226
 
2227
  // ==================== SHAREPOINT INTEGRATION ====================
 
2233
  breadcrumbs: [],
2234
  selectedFiles: new Set()
2235
  };
 
2236
  // Initialize UI based on auth state
2237
  function updateSharePointUI() {
2238
+ const connected = localStorage.getItem('sp_connected') === 'true';
2239
+ // FIX: Use correct ID from HTML (sharepointAuthSection, not sharepointConnectSection)
2240
+ const connectDiv = document.getElementById('sharepointAuthSection');
2241
+ const actionsDiv = document.getElementById('sharepointActionsSection');
2242
+
2243
+ if (connectDiv) connectDiv.style.display = connected ? 'none' : 'flex';
2244
+ if (actionsDiv) actionsDiv.style.display = connected ? 'block' : 'none';
2245
  }
2246
  updateSharePointUI();
2247
 
2248
  // Connect Button Handler
2249
+ const connectSharePointBtn = document.getElementById('connectSharePointBtn');
2250
+ console.log('SharePoint Button found:', !!connectSharePointBtn); // Debug
2251
+ if (connectSharePointBtn) {
2252
+ connectSharePointBtn.addEventListener('click', async () => {
2253
+ console.log('Connect Account button clicked!'); // Debug
2254
+ // Open popup immediately to avoid blocker
2255
  const width = 600;
2256
  const height = 700;
2257
  const left = (window.screen.width - width) / 2;
2258
  const top = (window.screen.height - height) / 2;
2259
 
2260
+ // Use unique name to ensure new window every time
2261
+ const popup = window.open(
2262
+ 'about:blank',
2263
+ `SharePointLogin_${Date.now()}`,
2264
  `width=${width},height=${height},top=${top},left=${left}`
2265
  );
 
 
 
 
2266
 
2267
+ console.log('Popup result:', popup); // Debug
2268
+
2269
+ if (!popup) {
2270
+ showError('Popup blocked! Please allow popups for this site.');
2271
+ return;
2272
+ }
2273
+
2274
+ // Safer way to set content
2275
+ try {
2276
+ popup.document.body.innerHTML = '<h3>Connecting to Microsoft...</h3><p>Please wait while we redirect you.</p>';
2277
+ } catch (e) {
2278
+ // Ignore modification errors if cross-origin or closed
2279
+ console.warn('Could not set popup content', e);
2280
+ }
2281
+
2282
+ try {
2283
+ const response = await fetch('/auth/sharepoint/login');
2284
+ const data = await response.json();
2285
+ console.log('Auth response:', data); // Debug
2286
+
2287
+ if (response.ok && data.auth_url) {
2288
+ if (!popup.closed) {
2289
+ popup.location.href = data.auth_url;
2290
+ }
2291
+ } else {
2292
+ if (!popup.closed) popup.close();
2293
+ showError('Failed to get login URL');
2294
+ }
2295
+ } catch (error) {
2296
+ console.error('Auth error:', error); // Debug
2297
+ if (!popup.closed) popup.close();
2298
+ showError('Failed to start login: ' + error.message);
2299
+ }
2300
+ });
2301
+ } else {
2302
+ console.error('SharePoint button NOT FOUND in DOM!');
2303
+ }
2304
 
2305
  // Logout
2306
+ const logoutSharePointBtn = document.getElementById('logoutSharePointBtn');
2307
+ if (logoutSharePointBtn) {
2308
+ logoutSharePointBtn.addEventListener('click', () => {
2309
+ localStorage.removeItem('sp_connected');
2310
+ updateSharePointUI();
2311
+ });
2312
+ }
2313
 
2314
+ // Browse
2315
+ const browseSharePointBtn = document.getElementById('browseSharePointBtn');
2316
+ if (browseSharePointBtn) {
2317
+ browseSharePointBtn.addEventListener('click', async () => {
2318
+ document.getElementById('sharepointModal').style.display = 'flex';
2319
+ await loadSharePointItems();
2320
+ });
2321
+ }
2322
 
2323
+ // Close Modal
2324
+ const closeSharePointModal = document.getElementById('closeSharePointModal');
2325
+ if (closeSharePointModal) {
2326
+ closeSharePointModal.addEventListener('click', () => {
2327
+ document.getElementById('sharepointModal').style.display = 'none';
2328
+ });
2329
+ }
2330
 
2331
+ // Back Button
2332
+ const spBackBtn = document.getElementById('spBackBtn');
2333
+ if (spBackBtn) {
2334
+ spBackBtn.addEventListener('click', async () => {
2335
+ if (currentPath.length > 0) {
2336
+ currentPath.pop(); // Remove current folder
2337
+ const parentFolder = currentPath.length > 0 ? currentPath[currentPath.length - 1] : null;
2338
+ await loadSharePointItems(parentFolder ? parentFolder.id : null);
2339
+ }
2340
+ });
2341
+ }
2342
+
2343
+ // Import Button
2344
+ const spImportBtn = document.getElementById('spImportBtn');
2345
+ if (spImportBtn) {
2346
+ spImportBtn.addEventListener('click', async () => {
2347
+ const checkboxes = document.querySelectorAll('.sp-item-checkbox:checked');
2348
+ if (checkboxes.length === 0) {
2349
+ alert('Please select at least one file to import.');
2350
+ return;
2351
+ }
2352
+
2353
+ const fileIds = Array.from(checkboxes).map(cb => cb.value);
2354
+ const btn = document.getElementById('spImportBtn');
2355
+ btn.disabled = true;
2356
+ btn.textContent = 'Importing...';
2357
+
2358
+ try {
2359
+ // This endpoint would handle downloading from Graph API and processing
2360
+ // For now, we simulate success or need to implement the backend logic
2361
+ const response = await fetch('/sharepoint/download-and-validate', {
2362
+ method: 'POST',
2363
+ headers: { 'Content-Type': 'application/json' },
2364
+ body: JSON.stringify({ file_ids: fileIds })
2365
+ });
2366
+
2367
+ if (response.ok) {
2368
+ const result = await response.json();
2369
+ document.getElementById('sharepointModal').style.display = 'none';
2370
+ // Refresh validation results or show success
2371
+ displayResults(result);
2372
+ } else {
2373
+ throw new Error('Import failed');
2374
+ }
2375
+ } catch (e) {
2376
+ alert('Error importing files: ' + e.message);
2377
+ } finally {
2378
+ btn.disabled = false;
2379
+ btn.textContent = 'Import Selected';
2380
+ }
2381
+ });
2382
  }
2383
 
2384
  // Load Items (Folder level)
 
2521
  return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
2522
  }
2523
 
2524
+ // App Logout
2525
+ const logoutBtn = document.getElementById('logoutBtn');
2526
+ if (logoutBtn) {
2527
+ logoutBtn.addEventListener('click', async () => {
2528
+ try {
2529
+ // Call backend logout if it exists, or just redirect
2530
+ await fetch('/logout', { method: 'POST' });
2531
+ window.location.href = '/login';
2532
+ } catch (e) {
2533
+ // Fallback
2534
+ window.location.href = '/login';
2535
+ }
2536
+ });
2537
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2538
 
 
 
 
 
 
 
 
2539
 
2540
  // Load templates when page loads
2541
  loadProjects();
app/validator.py CHANGED
@@ -681,9 +681,38 @@ class Validator:
681
  """
682
  logger.info(f"Starting comparison: {file1_name} vs {file2_name}")
683
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
684
  # Extract text from both documents
685
- text1 = extract_document_text(file1_content, file1_extension)
686
- text2 = extract_document_text(file2_content, file2_extension)
687
 
688
  logger.info(f"Extracted text - File 1: {len(text1)} chars, File 2: {len(text2)} chars")
689
 
@@ -691,18 +720,25 @@ class Validator:
691
  raise ValueError("Both documents appear to be empty or contain no extractable text")
692
 
693
  # Build LLM prompt for comparison
694
- comparison_prompt = f"""You are comparing two versions of a document to identify what changed.
695
 
696
- DOCUMENT 1 ({file1_name}):
697
- {text1[:10000]} # Limit to avoid token limits
698
 
699
- DOCUMENT 2 ({file2_name}):
700
  {text2[:10000]}
701
 
702
  Please analyze the differences between these two documents and provide:
703
 
704
  1. A natural language summary of the main changes (2-3 sentences)
705
- 2. A detailed list of specific changes
 
 
 
 
 
 
 
706
 
707
  Format your response as a JSON object with this structure:
708
  {{
@@ -710,19 +746,17 @@ Format your response as a JSON object with this structure:
710
  "changes": [
711
  {{
712
  "type": "addition|deletion|modification",
713
- "section": "Optional section name where change occurred",
714
  "description": "Description of the change"
715
  }}
716
  ]
717
  }}
718
 
719
- Focus on:
720
- - Content additions or deletions
721
- - Text modifications
722
- - Structural changes (headings, lists, tables)
723
- - Significant formatting changes
724
-
725
- If the documents are identical, return an empty changes array.
726
  """
727
 
728
  try:
@@ -744,9 +778,42 @@ If the documents are identical, return an empty changes array.
744
  response_text = message.content[0].text if message.content else ""
745
  logger.info(f"Received comparison response ({len(response_text)} chars)")
746
 
747
- # Parse JSON response
748
  import json
749
- comparison_data = json.loads(response_text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
750
 
751
  logger.info(f"Comparison complete: {len(comparison_data.get('changes', []))} changes detected")
752
 
 
681
  """
682
  logger.info(f"Starting comparison: {file1_name} vs {file2_name}")
683
 
684
+ # Normalize text to handle PDF ligatures and encoding differences
685
+ def normalize_text(text: str) -> str:
686
+ """Normalize Unicode ligatures and common PDF text artifacts."""
687
+ import unicodedata
688
+ # Common ligature replacements
689
+ ligatures = {
690
+ 'fi': 'fi',
691
+ 'fl': 'fl',
692
+ 'ff': 'ff',
693
+ 'ffi': 'ffi',
694
+ 'ffl': 'ffl',
695
+ 'Ꜳ': 'AA',
696
+ 'ꜳ': 'aa',
697
+ 'Æ': 'AE',
698
+ 'æ': 'ae',
699
+ 'Œ': 'OE',
700
+ 'œ': 'oe',
701
+ '\u00AD': '', # Soft hyphen
702
+ '\u200B': '', # Zero-width space
703
+ '\u200C': '', # Zero-width non-joiner
704
+ '\u200D': '', # Zero-width joiner
705
+ '\uFEFF': '', # BOM
706
+ }
707
+ for lig, replacement in ligatures.items():
708
+ text = text.replace(lig, replacement)
709
+ # Normalize Unicode to NFC form
710
+ text = unicodedata.normalize('NFC', text)
711
+ return text
712
+
713
  # Extract text from both documents
714
+ text1 = normalize_text(extract_document_text(file1_content, file1_extension))
715
+ text2 = normalize_text(extract_document_text(file2_content, file2_extension))
716
 
717
  logger.info(f"Extracted text - File 1: {len(text1)} chars, File 2: {len(text2)} chars")
718
 
 
720
  raise ValueError("Both documents appear to be empty or contain no extractable text")
721
 
722
  # Build LLM prompt for comparison
723
+ comparison_prompt = f"""You are comparing two versions of a document to identify MEANINGFUL content changes.
724
 
725
+ DOCUMENT 1 (Original - {file1_name}):
726
+ {text1[:10000]}
727
 
728
+ DOCUMENT 2 (Modified - {file2_name}):
729
  {text2[:10000]}
730
 
731
  Please analyze the differences between these two documents and provide:
732
 
733
  1. A natural language summary of the main changes (2-3 sentences)
734
+ 2. A detailed list of UNIQUE, MEANINGFUL changes only
735
+
736
+ IMPORTANT RULES:
737
+ - Do NOT report duplicate changes - each change should appear only once
738
+ - IGNORE font/encoding differences (like ligatures, special characters that look the same)
739
+ - IGNORE minor whitespace or formatting differences
740
+ - Focus on actual CONTENT changes (text additions, deletions, modifications)
741
+ - Group similar changes together instead of listing each instance separately
742
 
743
  Format your response as a JSON object with this structure:
744
  {{
 
746
  "changes": [
747
  {{
748
  "type": "addition|deletion|modification",
749
+ "section": "Section name where change occurred",
750
  "description": "Description of the change"
751
  }}
752
  ]
753
  }}
754
 
755
+ If the documents are essentially identical (only minor formatting/encoding differences), return:
756
+ {{
757
+ "summary": "Documents are essentially identical with no meaningful content changes.",
758
+ "changes": []
759
+ }}
 
 
760
  """
761
 
762
  try:
 
778
  response_text = message.content[0].text if message.content else ""
779
  logger.info(f"Received comparison response ({len(response_text)} chars)")
780
 
781
+ # Parse JSON response - extract from markdown code blocks if present
782
  import json
783
+ import re
784
+
785
+ # Try to extract JSON from markdown code blocks
786
+ json_text = response_text
787
+
788
+ # Check for ```json ... ``` or ``` ... ``` patterns
789
+ code_block_match = re.search(r'```(?:json)?\s*([\s\S]*?)```', response_text)
790
+ if code_block_match:
791
+ json_text = code_block_match.group(1).strip()
792
+ logger.info("Extracted JSON from markdown code block")
793
+ else:
794
+ # Try to find raw JSON object
795
+ json_match = re.search(r'\{[\s\S]*\}', response_text)
796
+ if json_match:
797
+ json_text = json_match.group(0)
798
+ logger.info("Extracted raw JSON object from response")
799
+
800
+ try:
801
+ comparison_data = json.loads(json_text)
802
+ except json.JSONDecodeError as json_err:
803
+ logger.error(f"JSON parsing failed. Raw response: {response_text[:500]}...")
804
+ raise ValueError(f"Failed to parse comparison response as JSON: {str(json_err)}")
805
+
806
+ # Deduplicate changes based on description
807
+ if 'changes' in comparison_data:
808
+ seen_descriptions = set()
809
+ unique_changes = []
810
+ for change in comparison_data['changes']:
811
+ desc = change.get('description', '').lower().strip()
812
+ if desc and desc not in seen_descriptions:
813
+ seen_descriptions.add(desc)
814
+ unique_changes.append(change)
815
+ comparison_data['changes'] = unique_changes
816
+ logger.info(f"After deduplication: {len(unique_changes)} unique changes")
817
 
818
  logger.info(f"Comparison complete: {len(comparison_data.get('changes', []))} changes detected")
819