|
<!DOCTYPE html> |
|
<html lang="fr"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>UMR - Contrôle Qualité PDF</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.12.313/pdf.min.js"></script> |
|
<style> |
|
.pdf-container { |
|
height: 500px; |
|
overflow-y: auto; |
|
border: 1px solid #e5e7eb; |
|
position: relative; |
|
} |
|
.chat-container { |
|
height: 400px; |
|
overflow-y: auto; |
|
} |
|
.highlight { |
|
background-color: rgba(255, 255, 0, 0.3); |
|
} |
|
.page-selector { |
|
position: absolute; |
|
bottom: 10px; |
|
left: 10px; |
|
z-index: 100; |
|
background: white; |
|
padding: 5px; |
|
border-radius: 5px; |
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2); |
|
} |
|
.control-zone { |
|
position: absolute; |
|
border: 2px dashed rgba(59, 130, 246, 0.5); |
|
background-color: rgba(59, 130, 246, 0.2); |
|
z-index: 10; |
|
cursor: move; |
|
} |
|
#zoneCreator { |
|
position: relative; |
|
} |
|
.zone-tooltip { |
|
position: absolute; |
|
background: white; |
|
padding: 5px; |
|
border-radius: 4px; |
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2); |
|
z-index: 20; |
|
font-size: 12px; |
|
pointer-events: none; |
|
} |
|
.zone-actions { |
|
position: absolute; |
|
top: -25px; |
|
right: 0; |
|
display: flex; |
|
gap: 5px; |
|
} |
|
.zone-action-btn { |
|
background: white; |
|
border: 1px solid #e5e7eb; |
|
border-radius: 4px; |
|
width: 20px; |
|
height: 20px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
cursor: pointer; |
|
font-size: 12px; |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-50"> |
|
<div class="container mx-auto px-4 py-8"> |
|
<header class="mb-8"> |
|
<h1 class="text-3xl font-bold text-blue-800">UMR - Contrôle Qualité des Courriers PDF</h1> |
|
<p class="text-gray-600 mt-2">Comparez vos documents PDF avec les modèles de référence</p> |
|
</header> |
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> |
|
|
|
<div class="bg-white rounded-lg shadow p-4"> |
|
<h2 class="text-xl font-semibold mb-4 text-blue-700">Document de Référence</h2> |
|
<div class="mb-4"> |
|
<input type="file" id="referenceFile" accept=".pdf" class="block w-full text-sm text-gray-500 |
|
file:mr-4 file:py-2 file:px-4 |
|
file:rounded-md file:border-0 |
|
file:text-sm file:font-semibold |
|
file:bg-blue-50 file:text-blue-700 |
|
hover:file:bg-blue-100"> |
|
</div> |
|
<div class="relative pdf-container" id="referenceViewer"> |
|
<canvas id="referenceCanvas"></canvas> |
|
<div class="page-selector hidden" id="refPageSelector"> |
|
Page <span id="refCurrentPage">1</span> sur <span id="refTotalPages">0</span> |
|
<button class="ml-2 px-2 py-1 bg-blue-100 rounded" id="refPrevPage">←</button> |
|
<button class="ml-1 px-2 py-1 bg-blue-100 rounded" id="refNextPage">→</button> |
|
</div> |
|
</div> |
|
<button id="defineZonesBtn" class="mt-4 hidden bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded"> |
|
Définir les zones de contrôle |
|
</button> |
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow p-4"> |
|
<h2 class="text-xl font-semibold mb-4 text-blue-700">Document à Contrôler</h2> |
|
<div class="mb-4"> |
|
<input type="file" id="controlFile" accept=".pdf" class="block w-full text-sm text-gray-500 |
|
file:mr-4 file:py-2 file:px-4 |
|
file:rounded-md file:border-0 |
|
file:text-sm file:font-semibold |
|
file:bg-blue-50 file:text-blue-700 |
|
hover:file:bg-blue-100"> |
|
</div> |
|
<div class="relative pdf-container" id="controlViewer"> |
|
<canvas id="controlCanvas"></canvas> |
|
<div class="page-selector hidden" id="ctrlPageSelector"> |
|
Page <span id="ctrlCurrentPage">1</span> sur <span id="ctrlTotalPages">0</span> |
|
<button class="ml-2 px-2 py-1 bg-blue-100 rounded" id="ctrlPrevPage">←</button> |
|
<button class="ml-1 px-2 py-1 bg-blue-100 rounded" id="ctrlNextPage">→</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="zoneCreator" class="hidden bg-white rounded-lg shadow p-4 mb-6"> |
|
<h2 class="text-xl font-semibold mb-4 text-blue-700">Définition des Zones de Contrôle</h2> |
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> |
|
<div> |
|
<div class="mb-4"> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Type de document</label> |
|
<select id="docType" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"> |
|
<option value="lettre">Lettre du Président</option> |
|
<option value="lexique">Lexique</option> |
|
<option value="attestation_alloc">Attestation Fiscale Allocataire</option> |
|
<option value="attestation_cotis">Attestation Fiscale Cotisant</option> |
|
<option value="releve">Relevé de Situation</option> |
|
<option value="contact">Page de Contact</option> |
|
</select> |
|
</div> |
|
<div class="mb-4"> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Nom de la zone</label> |
|
<input type="text" id="zoneName" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" placeholder="Ex: En-tête, Logo, Adresse..."> |
|
</div> |
|
<div class="mb-4"> |
|
<label class="block text-sm font-medium text-gray-700 mb-1">Page</label> |
|
<input type="number" id="zonePage" min="1" value="1" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"> |
|
</div> |
|
<div class="flex space-x-4"> |
|
<button id="saveZoneBtn" class="bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded"> |
|
Enregistrer la zone |
|
</button> |
|
<button id="cancelZoneBtn" class="bg-gray-600 hover:bg-gray-700 text-white font-medium py-2 px-4 rounded"> |
|
Annuler |
|
</button> |
|
</div> |
|
</div> |
|
<div class="bg-gray-50 p-4 rounded-lg"> |
|
<h3 class="font-medium text-gray-800 mb-3">Instructions</h3> |
|
<ol class="list-decimal list-inside text-sm text-gray-700 space-y-2"> |
|
<li>Cliquez et glissez pour dessiner une zone sur le document de référence</li> |
|
<li>Ajustez la taille et la position si nécessaire</li> |
|
<li>Remplissez les informations sur la zone (type, nom, page)</li> |
|
<li>Cliquez sur "Enregistrer la zone" pour la sauvegarder</li> |
|
<li>Répétez pour toutes les zones à contrôler</li> |
|
</ol> |
|
<div class="mt-4 p-3 bg-blue-50 rounded-lg"> |
|
<h4 class="font-medium text-blue-800 mb-2">Conseils :</h4> |
|
<ul class="list-disc list-inside text-sm text-blue-700 space-y-1"> |
|
<li>Nommez clairement vos zones pour les retrouver facilement</li> |
|
<li>Créez des zones suffisamment larges pour couvrir le texte</li> |
|
<li>Vérifiez que la page indiquée correspond bien à la zone</li> |
|
</ul> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow p-4 mb-6"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h2 class="text-xl font-semibold text-blue-700">Interface de Contrôle</h2> |
|
<button id="startControlBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded"> |
|
Lancer le contrôle |
|
</button> |
|
</div> |
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> |
|
|
|
<div class="bg-gray-50 p-4 rounded-lg"> |
|
<h3 class="font-medium text-gray-800 mb-3">Types de documents détectés</h3> |
|
<ul id="docTypesList" class="space-y-2"> |
|
<li class="text-sm text-gray-500 italic">Aucun document chargé</li> |
|
</ul> |
|
</div> |
|
|
|
|
|
<div class="bg-gray-50 p-4 rounded-lg"> |
|
<h3 class="font-medium text-gray-800 mb-3">Anomalies détectées</h3> |
|
<ul id="anomaliesList" class="space-y-2"> |
|
<li class="text-sm text-gray-500 italic">Aucune anomalie détectée</li> |
|
</ul> |
|
</div> |
|
|
|
|
|
<div class="bg-gray-50 p-4 rounded-lg"> |
|
<h3 class="font-medium text-gray-800 mb-3">Actions</h3> |
|
<div class="space-y-3"> |
|
<button id="exportCSVBtn" class="w-full bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded"> |
|
Exporter en CSV |
|
</button> |
|
<button id="generatePDFBtn" class="w-full bg-purple-600 hover:bg-purple-700 text-white font-medium py-2 px-4 rounded"> |
|
Générer PDF corrigé |
|
</button> |
|
<button id="resetBtn" class="w-full bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded"> |
|
Réinitialiser |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow p-4"> |
|
<h2 class="text-xl font-semibold mb-4 text-blue-700">Interface Conversationnelle</h2> |
|
<div class="chat-container mb-4 p-4 bg-gray-50 rounded-lg" id="chatMessages"> |
|
<div class="text-center text-gray-500 italic">Bienvenue dans l'interface de contrôle qualité. Chargez vos documents pour commencer.</div> |
|
</div> |
|
<div class="flex"> |
|
<input type="text" id="chatInput" placeholder="Posez votre question..." class="flex-1 px-4 py-2 border border-gray-300 rounded-l-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"> |
|
<button id="sendMessageBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-r-md"> |
|
Envoyer |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.12.313/pdf.worker.min.js'; |
|
|
|
|
|
let referencePdf = null; |
|
let controlPdf = null; |
|
let referenceZones = []; |
|
let currentZone = null; |
|
let isCreatingZone = false; |
|
let startX, startY; |
|
let chatHistory = []; |
|
let detectedAnomalies = []; |
|
let detectedDocTypes = []; |
|
let activeZoneIndex = -1; |
|
let isDraggingZone = false; |
|
let dragOffsetX = 0; |
|
let dragOffsetY = 0; |
|
|
|
|
|
const referenceFileInput = document.getElementById('referenceFile'); |
|
const controlFileInput = document.getElementById('controlFile'); |
|
const referenceCanvas = document.getElementById('referenceCanvas'); |
|
const controlCanvas = document.getElementById('controlCanvas'); |
|
const refPageSelector = document.getElementById('refPageSelector'); |
|
const ctrlPageSelector = document.getElementById('ctrlPageSelector'); |
|
const refCurrentPage = document.getElementById('refCurrentPage'); |
|
const refTotalPages = document.getElementById('refTotalPages'); |
|
const ctrlCurrentPage = document.getElementById('ctrlCurrentPage'); |
|
const ctrlTotalPages = document.getElementById('ctrlTotalPages'); |
|
const refPrevPage = document.getElementById('refPrevPage'); |
|
const refNextPage = document.getElementById('refNextPage'); |
|
const ctrlPrevPage = document.getElementById('ctrlPrevPage'); |
|
const ctrlNextPage = document.getElementById('ctrlNextPage'); |
|
const defineZonesBtn = document.getElementById('defineZonesBtn'); |
|
const zoneCreator = document.getElementById('zoneCreator'); |
|
const docTypeSelect = document.getElementById('docType'); |
|
const zoneNameInput = document.getElementById('zoneName'); |
|
const zonePageInput = document.getElementById('zonePage'); |
|
const saveZoneBtn = document.getElementById('saveZoneBtn'); |
|
const cancelZoneBtn = document.getElementById('cancelZoneBtn'); |
|
const startControlBtn = document.getElementById('startControlBtn'); |
|
const docTypesList = document.getElementById('docTypesList'); |
|
const anomaliesList = document.getElementById('anomaliesList'); |
|
const exportCSVBtn = document.getElementById('exportCSVBtn'); |
|
const generatePDFBtn = document.getElementById('generatePDFBtn'); |
|
const resetBtn = document.getElementById('resetBtn'); |
|
const chatMessages = document.getElementById('chatMessages'); |
|
const chatInput = document.getElementById('chatInput'); |
|
const sendMessageBtn = document.getElementById('sendMessageBtn'); |
|
|
|
|
|
referenceFileInput.addEventListener('change', handleReferenceFile); |
|
controlFileInput.addEventListener('change', handleControlFile); |
|
defineZonesBtn.addEventListener('click', startZoneCreation); |
|
saveZoneBtn.addEventListener('click', saveZone); |
|
cancelZoneBtn.addEventListener('click', cancelZoneCreation); |
|
startControlBtn.addEventListener('click', startControlProcess); |
|
exportCSVBtn.addEventListener('click', exportToCSV); |
|
generatePDFBtn.addEventListener('click', generateCorrectedPDF); |
|
resetBtn.addEventListener('click', resetApplication); |
|
sendMessageBtn.addEventListener('click', sendChatMessage); |
|
chatInput.addEventListener('keypress', (e) => { |
|
if (e.key === 'Enter') sendChatMessage(); |
|
}); |
|
|
|
|
|
refPrevPage.addEventListener('click', () => navigatePage('ref', -1)); |
|
refNextPage.addEventListener('click', () => navigatePage('ref', 1)); |
|
ctrlPrevPage.addEventListener('click', () => navigatePage('ctrl', -1)); |
|
ctrlNextPage.addEventListener('click', () => navigatePage('ctrl', 1)); |
|
|
|
|
|
function handleReferenceFile(e) { |
|
const file = e.target.files[0]; |
|
if (!file) return; |
|
|
|
const reader = new FileReader(); |
|
reader.onload = async function() { |
|
try { |
|
const typedArray = new Uint8Array(reader.result); |
|
referencePdf = await pdfjsLib.getDocument(typedArray).promise; |
|
|
|
refTotalPages.textContent = referencePdf.numPages; |
|
refPageSelector.classList.remove('hidden'); |
|
|
|
await renderPage('ref', 1); |
|
defineZonesBtn.classList.remove('hidden'); |
|
|
|
addChatMessage("Système", "Document de référence chargé avec succès. Vous pouvez maintenant définir les zones de contrôle."); |
|
} catch (error) { |
|
console.error('Error loading PDF:', error); |
|
addChatMessage("Erreur", "Erreur lors du chargement du PDF de référence."); |
|
} |
|
}; |
|
reader.readAsArrayBuffer(file); |
|
} |
|
|
|
function handleControlFile(e) { |
|
const file = e.target.files[0]; |
|
if (!file) return; |
|
|
|
const reader = new FileReader(); |
|
reader.onload = async function() { |
|
try { |
|
const typedArray = new Uint8Array(reader.result); |
|
controlPdf = await pdfjsLib.getDocument(typedArray).promise; |
|
|
|
ctrlTotalPages.textContent = controlPdf.numPages; |
|
ctrlPageSelector.classList.remove('hidden'); |
|
|
|
await renderPage('ctrl', 1); |
|
|
|
addChatMessage("Système", "Document à contrôler chargé avec succès. Vous pouvez maintenant lancer le contrôle."); |
|
} catch (error) { |
|
console.error('Error loading PDF:', error); |
|
addChatMessage("Erreur", "Erreur lors du chargement du PDF à contrôler."); |
|
} |
|
}; |
|
reader.readAsArrayBuffer(file); |
|
} |
|
|
|
async function renderPage(type, pageNum) { |
|
try { |
|
const pdf = type === 'ref' ? referencePdf : controlPdf; |
|
const canvas = type === 'ref' ? referenceCanvas : controlCanvas; |
|
const pageInfo = type === 'ref' ? refCurrentPage : ctrlCurrentPage; |
|
|
|
if (!pdf) return; |
|
|
|
const page = await pdf.getPage(pageNum); |
|
const viewport = page.getViewport({ scale: 1.0 }); |
|
|
|
|
|
canvas.height = viewport.height; |
|
canvas.width = viewport.width; |
|
|
|
|
|
pageInfo.textContent = pageNum; |
|
|
|
|
|
await page.render({ |
|
canvasContext: canvas.getContext('2d'), |
|
viewport: viewport |
|
}).promise; |
|
|
|
|
|
if (type === 'ref') { |
|
displayZonesForPage(pageNum); |
|
} |
|
|
|
return viewport; |
|
} catch (error) { |
|
console.error('Error rendering page:', error); |
|
} |
|
} |
|
|
|
function navigatePage(type, direction) { |
|
const currentPage = type === 'ref' ? parseInt(refCurrentPage.textContent) : parseInt(ctrlCurrentPage.textContent); |
|
const totalPages = type === 'ref' ? referencePdf.numPages : controlPdf.numPages; |
|
|
|
let newPage = currentPage + direction; |
|
if (newPage < 1) newPage = 1; |
|
if (newPage > totalPages) newPage = totalPages; |
|
|
|
renderPage(type, newPage); |
|
} |
|
|
|
function startZoneCreation() { |
|
isCreatingZone = true; |
|
zoneCreator.classList.remove('hidden'); |
|
defineZonesBtn.classList.add('hidden'); |
|
|
|
|
|
referenceCanvas.style.cursor = 'crosshair'; |
|
referenceCanvas.addEventListener('mousedown', startZoneDrawing); |
|
referenceCanvas.addEventListener('mousemove', drawZone); |
|
referenceCanvas.addEventListener('mouseup', endZoneDrawing); |
|
|
|
addChatMessage("Système", "Mode création de zones activé. Cliquez et glissez pour définir une zone de contrôle."); |
|
} |
|
|
|
function startZoneDrawing(e) { |
|
if (!isCreatingZone) return; |
|
|
|
const rect = referenceCanvas.getBoundingClientRect(); |
|
startX = e.clientX - rect.left; |
|
startY = e.clientY - rect.top; |
|
|
|
|
|
const existingZone = findZoneAtPosition(startX, startY); |
|
if (existingZone) { |
|
activeZoneIndex = existingZone.index; |
|
isDraggingZone = true; |
|
dragOffsetX = startX - existingZone.zone.x; |
|
dragOffsetY = startY - existingZone.zone.y; |
|
return; |
|
} |
|
|
|
|
|
if (currentZone) { |
|
currentZone.remove(); |
|
currentZone = null; |
|
} |
|
|
|
|
|
currentZone = document.createElement('div'); |
|
currentZone.className = 'control-zone'; |
|
currentZone.style.left = startX + 'px'; |
|
currentZone.style.top = startY + 'px'; |
|
currentZone.style.width = '0'; |
|
currentZone.style.height = '0'; |
|
|
|
document.getElementById('referenceViewer').appendChild(currentZone); |
|
} |
|
|
|
function findZoneAtPosition(x, y) { |
|
const zones = document.querySelectorAll('.control-zone'); |
|
for (let i = 0; i < zones.length; i++) { |
|
const zone = zones[i]; |
|
const zoneRect = zone.getBoundingClientRect(); |
|
const canvasRect = referenceCanvas.getBoundingClientRect(); |
|
|
|
const zoneX = zoneRect.left - canvasRect.left; |
|
const zoneY = zoneRect.top - canvasRect.top; |
|
const zoneWidth = zoneRect.width; |
|
const zoneHeight = zoneRect.height; |
|
|
|
if (x >= zoneX && x <= zoneX + zoneWidth && y >= zoneY && y <= zoneY + zoneHeight) { |
|
|
|
for (let j = 0; j < referenceZones.length; j++) { |
|
if (Math.abs(referenceZones[j].x - zoneX) < 5 && |
|
Math.abs(referenceZones[j].y - zoneY) < 5 && |
|
Math.abs(referenceZones[j].width - zoneWidth) < 5 && |
|
Math.abs(referenceZones[j].height - zoneHeight) < 5) { |
|
return { index: j, zone: referenceZones[j] }; |
|
} |
|
} |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
function drawZone(e) { |
|
if (!isCreatingZone) return; |
|
|
|
const rect = referenceCanvas.getBoundingClientRect(); |
|
const currentX = e.clientX - rect.left; |
|
const currentY = e.clientY - rect.top; |
|
|
|
if (isDraggingZone && activeZoneIndex >= 0) { |
|
|
|
const zone = referenceZones[activeZoneIndex]; |
|
zone.x = currentX - dragOffsetX; |
|
zone.y = currentY - dragOffsetY; |
|
|
|
|
|
displayZonesForPage(parseInt(refCurrentPage.textContent)); |
|
return; |
|
} |
|
|
|
if (!currentZone) return; |
|
|
|
const width = currentX - startX; |
|
const height = currentY - startY; |
|
|
|
currentZone.style.width = Math.abs(width) + 'px'; |
|
currentZone.style.height = Math.abs(height) + 'px'; |
|
|
|
if (width < 0) { |
|
currentZone.style.left = currentX + 'px'; |
|
} |
|
if (height < 0) { |
|
currentZone.style.top = currentY + 'px'; |
|
} |
|
} |
|
|
|
function endZoneDrawing() { |
|
if (!isCreatingZone) return; |
|
|
|
if (isDraggingZone) { |
|
isDraggingZone = false; |
|
activeZoneIndex = -1; |
|
return; |
|
} |
|
|
|
if (currentZone) { |
|
|
|
showZoneInfoTooltip(); |
|
} |
|
|
|
|
|
referenceCanvas.removeEventListener('mousemove', drawZone); |
|
referenceCanvas.removeEventListener('mouseup', endZoneDrawing); |
|
} |
|
|
|
function showZoneInfoTooltip() { |
|
if (!currentZone) return; |
|
|
|
const tooltip = document.createElement('div'); |
|
tooltip.className = 'zone-tooltip'; |
|
tooltip.innerHTML = ` |
|
<div class="font-medium mb-1">Nouvelle zone</div> |
|
<div class="text-xs">Remplissez les détails à droite</div> |
|
`; |
|
|
|
const rect = currentZone.getBoundingClientRect(); |
|
tooltip.style.left = (rect.left + rect.width/2 - 75) + 'px'; |
|
tooltip.style.top = (rect.top - 60) + 'px'; |
|
|
|
document.body.appendChild(tooltip); |
|
|
|
|
|
setTimeout(() => { |
|
if (tooltip.parentNode) { |
|
tooltip.parentNode.removeChild(tooltip); |
|
} |
|
}, 3000); |
|
|
|
|
|
document.addEventListener('click', function removeTooltip() { |
|
if (tooltip.parentNode) { |
|
tooltip.parentNode.removeChild(tooltip); |
|
} |
|
document.removeEventListener('click', removeTooltip); |
|
}, { once: true }); |
|
} |
|
|
|
function saveZone() { |
|
if (!currentZone) { |
|
addChatMessage("Erreur", "Veuillez d'abord créer une zone en cliquant et glissant sur le document."); |
|
return; |
|
} |
|
|
|
const rect = currentZone.getBoundingClientRect(); |
|
const canvasRect = referenceCanvas.getBoundingClientRect(); |
|
|
|
|
|
const x = rect.left - canvasRect.left; |
|
const y = rect.top - canvasRect.top; |
|
const width = rect.width; |
|
const height = rect.height; |
|
|
|
if (width < 10 || height < 10) { |
|
addChatMessage("Erreur", "La zone est trop petite. Veuillez créer une zone plus grande."); |
|
return; |
|
} |
|
|
|
if (!zoneNameInput.value.trim()) { |
|
addChatMessage("Erreur", "Veuillez donner un nom à la zone."); |
|
zoneNameInput.focus(); |
|
return; |
|
} |
|
|
|
const zone = { |
|
type: docTypeSelect.value, |
|
name: zoneNameInput.value.trim(), |
|
page: parseInt(zonePageInput.value), |
|
x: x, |
|
y: y, |
|
width: width, |
|
height: height |
|
}; |
|
|
|
referenceZones.push(zone); |
|
|
|
|
|
if (currentZone) { |
|
currentZone.remove(); |
|
currentZone = null; |
|
} |
|
isCreatingZone = false; |
|
|
|
|
|
referenceCanvas.style.cursor = 'default'; |
|
|
|
|
|
displayZonesForPage(parseInt(zonePageInput.value)); |
|
|
|
|
|
zoneNameInput.value = ''; |
|
|
|
addChatMessage("Système", `Zone "${zone.name}" enregistrée pour le type "${docTypeSelect.options[docTypeSelect.selectedIndex].text}".`); |
|
} |
|
|
|
function cancelZoneCreation() { |
|
if (currentZone) { |
|
currentZone.remove(); |
|
currentZone = null; |
|
} |
|
|
|
isCreatingZone = false; |
|
isDraggingZone = false; |
|
activeZoneIndex = -1; |
|
referenceCanvas.style.cursor = 'default'; |
|
zoneCreator.classList.add('hidden'); |
|
defineZonesBtn.classList.remove('hidden'); |
|
|
|
|
|
if (referenceZones.length > 0) { |
|
defineZonesBtn.click(); |
|
} |
|
|
|
addChatMessage("Système", "Création de zone annulée."); |
|
} |
|
|
|
function displayZonesForPage(pageNum) { |
|
|
|
document.querySelectorAll('.control-zone').forEach(zone => { |
|
if (zone !== currentZone) zone.remove(); |
|
}); |
|
|
|
|
|
document.querySelectorAll('.zone-actions').forEach(actions => { |
|
actions.remove(); |
|
}); |
|
|
|
|
|
referenceZones.filter(zone => zone.page === pageNum).forEach((zone, index) => { |
|
const zoneElement = document.createElement('div'); |
|
zoneElement.className = 'control-zone'; |
|
zoneElement.style.left = zone.x + 'px'; |
|
zoneElement.style.top = zone.y + 'px'; |
|
zoneElement.style.width = zone.width + 'px'; |
|
zoneElement.style.height = zone.height + 'px'; |
|
zoneElement.dataset.index = index; |
|
|
|
|
|
const actions = document.createElement('div'); |
|
actions.className = 'zone-actions'; |
|
actions.innerHTML = ` |
|
<button class="zone-action-btn edit-zone" title="Modifier">✏️</button> |
|
<button class="zone-action-btn delete-zone" title="Supprimer">🗑️</button> |
|
`; |
|
zoneElement.appendChild(actions); |
|
|
|
|
|
actions.style.left = (zone.width - actions.offsetWidth) + 'px'; |
|
|
|
document.getElementById('referenceViewer').appendChild(zoneElement); |
|
|
|
|
|
zoneElement.querySelector('.edit-zone').addEventListener('click', (e) => { |
|
e.stopPropagation(); |
|
editZone(index); |
|
}); |
|
|
|
zoneElement.querySelector('.delete-zone').addEventListener('click', (e) => { |
|
e.stopPropagation(); |
|
deleteZone(index); |
|
}); |
|
}); |
|
} |
|
|
|
function editZone(index) { |
|
const zone = referenceZones[index]; |
|
|
|
|
|
docTypeSelect.value = zone.type; |
|
zoneNameInput.value = zone.name; |
|
zonePageInput.value = zone.page; |
|
|
|
|
|
document.querySelectorAll('.control-zone').forEach(z => { |
|
z.style.borderColor = 'rgba(59, 130, 246, 0.5)'; |
|
}); |
|
|
|
const zoneElement = document.querySelector(`.control-zone[data-index="${index}"]`); |
|
if (zoneElement) { |
|
zoneElement.style.borderColor = 'rgba(255, 165, 0, 0.8)'; |
|
} |
|
|
|
addChatMessage("Système", `Modification de la zone "${zone.name}". Vous pouvez ajuster ses propriétés ou la déplacer directement sur le document.`); |
|
} |
|
|
|
function deleteZone(index) { |
|
const zoneName = referenceZones[index].name; |
|
if (confirm(`Voulez-vous vraiment supprimer la zone "${zoneName}" ?`)) { |
|
referenceZones.splice(index, 1); |
|
displayZonesForPage(parseInt(refCurrentPage.textContent)); |
|
addChatMessage("Système", `Zone "${zoneName}" supprimée.`); |
|
} |
|
} |
|
|
|
async function startControlProcess() { |
|
if (!referencePdf || !controlPdf) { |
|
addChatMessage("Erreur", "Veuillez charger les deux documents avant de lancer le processus."); |
|
return; |
|
} |
|
|
|
if (referenceZones.length === 0) { |
|
addChatMessage("Erreur", "Veuillez définir des zones de contrôle avant de lancer le processus."); |
|
return; |
|
} |
|
|
|
addChatMessage("Système", "Début du processus de contrôle..."); |
|
|
|
|
|
detectedAnomalies = []; |
|
detectedDocTypes = []; |
|
|
|
|
|
detectDocumentTypes(); |
|
|
|
|
|
simulateComparison(); |
|
|
|
|
|
updateResultsUI(); |
|
|
|
addChatMessage("Système", "Contrôle terminé. " + detectedAnomalies.length + " anomalies détectées."); |
|
} |
|
|
|
function detectDocumentTypes() { |
|
|
|
|
|
detectedDocTypes = [ |
|
{ type: 'lettre', page: 1, status: 'détecté' }, |
|
{ type: 'lexique', page: 2, status: 'détecté' }, |
|
{ type: 'attestation_alloc', page: 3, status: 'détecté' }, |
|
{ type: 'releve', page: 4, status: 'détecté' }, |
|
{ type: 'contact', page: 5, status: 'détecté' } |
|
]; |
|
} |
|
|
|
function simulateComparison() { |
|
|
|
|
|
|
|
|
|
detectedAnomalies = [ |
|
{ |
|
docType: 'lettre', |
|
page: 1, |
|
location: 'Haut de la page', |
|
description: 'Le logo en en-tête est décalé de 5px vers la droite', |
|
severity: 'Majeure', |
|
refText: 'Logo UMR positionné à x:50, y:50', |
|
controlText: 'Logo UMR positionné à x:55, y:50' |
|
}, |
|
{ |
|
docType: 'lettre', |
|
page: 1, |
|
location: 'Bas de la page', |
|
description: 'La mention légale est incomplète', |
|
severity: 'Mineure', |
|
refText: 'UMR est une SA au capital de 247 668 709 €. Siège social : 12 Rue de Cornulier - 75 000 PARIS.', |
|
controlText: 'UMR est une SA au capital de 247 668 709 €.' |
|
}, |
|
{ |
|
docType: 'attestation_alloc', |
|
page: 3, |
|
location: 'Milieu de la page', |
|
description: 'La phrase "Au titre des pensions : 1 632 €" est absente', |
|
severity: 'Majeure', |
|
refText: 'Au titre des pensions : 1 632 €', |
|
controlText: '' |
|
}, |
|
{ |
|
docType: 'releve', |
|
page: 4, |
|
location: 'Bas de la page', |
|
description: 'La valeur du point affichée diffère', |
|
severity: 'Variable', |
|
refText: 'valeur de service du point* de votre contrat PER Corem a été revalorisée à hauteur de 3,5% au 01/01/2025', |
|
controlText: 'valeur de service du point* de votre contrat PER Corem a été revalorisée à hauteur de 2,5% au 01/01/2025', |
|
needsConfirmation: true |
|
} |
|
]; |
|
} |
|
|
|
function updateResultsUI() { |
|
|
|
docTypesList.innerHTML = ''; |
|
if (detectedDocTypes.length === 0) { |
|
docTypesList.innerHTML = '<li class="text-sm text-gray-500 italic">Aucun type de document détecté</li>'; |
|
} else { |
|
detectedDocTypes.forEach(doc => { |
|
const li = document.createElement('li'); |
|
li.className = 'text-sm'; |
|
li.innerHTML = `<span class="font-medium">${getDocTypeName(doc.type)}</span> (Page ${doc.page}) - <span class="text-green-600">${doc.status}</span>`; |
|
docTypesList.appendChild(li); |
|
}); |
|
} |
|
|
|
|
|
anomaliesList.innerHTML = ''; |
|
if (detectedAnomalies.length === 0) { |
|
anomaliesList.innerHTML = '<li class="text-sm text-gray-500 italic">Aucune anomalie détectée</li>'; |
|
} else { |
|
detectedAnomalies.forEach((anomaly, index) => { |
|
const li = document.createElement('li'); |
|
li.className = 'text-sm'; |
|
|
|
let severityClass = 'text-yellow-600'; |
|
if (anomaly.severity === 'Majeure') severityClass = 'text-red-600'; |
|
if (anomaly.severity === 'Variable') severityClass = 'text-blue-600'; |
|
|
|
li.innerHTML = ` |
|
<div class="font-medium">${getDocTypeName(anomaly.docType)} (${anomaly.location}, Page ${anomaly.page})</div> |
|
<div>${anomaly.description} <span class="${severityClass}">(${anomaly.severity})</span></div> |
|
${anomaly.needsConfirmation ? '<div class="text-blue-600 mt-1">[Nécessite confirmation]</div>' : ''} |
|
`; |
|
|
|
li.addEventListener('click', () => showAnomalyDetails(index)); |
|
anomaliesList.appendChild(li); |
|
}); |
|
} |
|
} |
|
|
|
function getDocTypeName(typeCode) { |
|
const types = { |
|
'lettre': 'Lettre du Président', |
|
'lexique': 'Lexique', |
|
'attestation_alloc': 'Attestation Fiscale Allocataire', |
|
'attestation_cotis': 'Attestation Fiscale Cotisant', |
|
'releve': 'Relevé de Situation', |
|
'contact': 'Page de Contact' |
|
}; |
|
return types[typeCode] || typeCode; |
|
} |
|
|
|
function showAnomalyDetails(index) { |
|
const anomaly = detectedAnomalies[index]; |
|
|
|
let message = ` |
|
<strong>${getDocTypeName(anomaly.docType)} (Page ${anomaly.page})</strong><br> |
|
<span class="${anomaly.severity === 'Majeure' ? 'text-red-600' : anomaly.severity === 'Variable' ? 'text-blue-600' : 'text-yellow-600'}"> |
|
${anomaly.severity}: ${anomaly.description} |
|
</span><br><br> |
|
<strong>Document de référence:</strong><br> |
|
${anomaly.refText || 'Aucun texte correspondant'}<br><br> |
|
<strong>Document à contrôler:</strong><br> |
|
${anomaly.controlText || 'Aucun texte correspondant'} |
|
`; |
|
|
|
if (anomaly.needsConfirmation) { |
|
message += `<br><br><div class="mt-4"> |
|
<button onclick="confirmAnomaly(${index}, true)" class="mr-2 bg-green-600 hover:bg-green-700 text-white font-medium py-1 px-3 rounded text-sm"> |
|
Confirmer la différence |
|
</button> |
|
<button onclick="confirmAnomaly(${index}, false)" class="bg-red-600 hover:bg-red-700 text-white font-medium py-1 px-3 rounded text-sm"> |
|
Rejeter la différence |
|
</button> |
|
</div>`; |
|
} |
|
|
|
addChatMessage("Détail de l'anomalie", message); |
|
} |
|
|
|
function confirmAnomaly(index, isConfirmed) { |
|
const anomaly = detectedAnomalies[index]; |
|
|
|
if (isConfirmed) { |
|
addChatMessage("Système", `Vous avez confirmé que la différence dans "${anomaly.description}" est normale.`); |
|
|
|
} else { |
|
addChatMessage("Système", `Vous avez indiqué que la différence dans "${anomaly.description}" est anormale.`); |
|
|
|
} |
|
|
|
|
|
detectedAnomalies[index].needsConfirmation = false; |
|
|
|
|
|
updateResultsUI(); |
|
} |
|
|
|
function exportToCSV() { |
|
if (detectedAnomalies.length === 0) { |
|
addChatMessage("Erreur", "Aucune anomalie à exporter."); |
|
return; |
|
} |
|
|
|
|
|
let csvContent = "Type de document,Page,Localisation,Description,Sévérité,Texte de référence,Texte du document\n"; |
|
|
|
detectedAnomalies.forEach(anomaly => { |
|
csvContent += `"${getDocTypeName(anomaly.docType)}",${anomaly.page},"${anomaly.location}","${anomaly.description}","${anomaly.severity}","${anomaly.refText.replace(/"/g, '""')}","${anomaly.controlText.replace(/"/g, '""')}"\n`; |
|
}); |
|
|
|
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); |
|
const url = URL.createObjectURL(blob); |
|
const link = document.createElement('a'); |
|
link.setAttribute('href', url); |
|
link.setAttribute('download', 'anomalies_detectees.csv'); |
|
link.style.visibility = 'hidden'; |
|
document.body.appendChild(link); |
|
link.click(); |
|
document.body.removeChild(link); |
|
|
|
addChatMessage("Système", "Export CSV des anomalies terminé."); |
|
} |
|
|
|
function generateCorrectedPDF() { |
|
|
|
|
|
addChatMessage("Système", "Génération du PDF corrigé en cours..."); |
|
|
|
setTimeout(() => { |
|
addChatMessage("Système", "PDF corrigé généré avec succès. Les anomalies ont été surlignées."); |
|
|
|
|
|
const link = document.createElement('a'); |
|
link.setAttribute('href', '#'); |
|
link.setAttribute('download', 'document_corrige.pdf'); |
|
link.style.visibility = 'hidden'; |
|
document.body.appendChild(link); |
|
link.click(); |
|
document.body.removeChild(link); |
|
}, 2000); |
|
} |
|
|
|
function resetApplication() { |
|
|
|
referenceFileInput.value = ''; |
|
controlFileInput.value = ''; |
|
|
|
|
|
referenceCanvas.getContext('2d').clearRect(0, 0, referenceCanvas.width, referenceCanvas.height); |
|
controlCanvas.getContext('2d').clearRect(0, 0, controlCanvas.width, controlCanvas.height); |
|
|
|
|
|
refPageSelector.classList.add('hidden'); |
|
ctrlPageSelector.classList.add('hidden'); |
|
|
|
|
|
if (currentZone) { |
|
currentZone.remove(); |
|
currentZone = null; |
|
} |
|
isCreatingZone = false; |
|
isDraggingZone = false; |
|
activeZoneIndex = -1; |
|
referenceCanvas.style.cursor = 'default'; |
|
zoneCreator.classList.add('hidden'); |
|
defineZonesBtn.classList.add('hidden'); |
|
|
|
|
|
referenceZones = []; |
|
document.querySelectorAll('.control-zone').forEach(zone => zone.remove()); |
|
document.querySelectorAll('.zone-actions').forEach(actions => actions.remove()); |
|
|
|
|
|
detectedAnomalies = []; |
|
detectedDocTypes = []; |
|
docTypesList.innerHTML = '<li class="text-sm text-gray-500 italic">Aucun type de document détecté</li>'; |
|
anomaliesList.innerHTML = '<li class="text-sm text-gray-500 italic">Aucune anomalie détectée</li>'; |
|
|
|
|
|
chatHistory = []; |
|
chatMessages.innerHTML = '<div class="text-center text-gray-500 italic">Bienvenue dans l\'interface de contrôle qualité. Chargez vos documents pour commencer.</div>'; |
|
|
|
|
|
referencePdf = null; |
|
controlPdf = null; |
|
|
|
addChatMessage("Système", "Application réinitialisée. Vous pouvez charger de nouveaux documents."); |
|
} |
|
|
|
function addChatMessage(sender, message) { |
|
const messageDiv = document.createElement('div'); |
|
messageDiv.className = 'mb-3'; |
|
|
|
const senderSpan = document.createElement('span'); |
|
senderSpan.className = 'font-medium text-blue-700'; |
|
senderSpan.textContent = sender + ': '; |
|
|
|
const contentDiv = document.createElement('div'); |
|
contentDiv.className = 'mt-1 ml-4 p-3 bg-white rounded-lg shadow-sm'; |
|
contentDiv.innerHTML = message; |
|
|
|
messageDiv.appendChild(senderSpan); |
|
messageDiv.appendChild(contentDiv); |
|
|
|
chatMessages.appendChild(messageDiv); |
|
chatMessages.scrollTop = chatMessages.scrollHeight; |
|
|
|
|
|
chatHistory.push({ sender, message }); |
|
} |
|
|
|
function sendChatMessage() { |
|
const message = chatInput.value.trim(); |
|
if (!message) return; |
|
|
|
addChatMessage("Vous", message); |
|
chatInput.value = ''; |
|
|
|
|
|
setTimeout(() => { |
|
let response = "Je suis désolé, je ne peux pas répondre à cette question dans cette version de démonstration. Dans une version complète, je pourrais analyser les documents et répondre à vos questions sur les anomalies détectées."; |
|
|
|
|
|
if (message.toLowerCase().includes('bonjour') || message.toLowerCase().includes('salut')) { |
|
response = "Bonjour ! Comment puis-je vous aider avec le contrôle de vos documents aujourd'hui ?"; |
|
} else if (message.toLowerCase().includes('aide') || message.toLowerCase().includes('comment')) { |
|
response = "Pour utiliser cette application :<br>1. Chargez un document de référence<br>2. Définissez les zones de contrôle<br>3. Chargez un document à vérifier<br>4. Lancez le contrôle<br>Je vous signalerai alors les anomalies détectées."; |
|
} else if (message.toLowerCase().includes('anomalie') && detectedAnomalies.length > 0) { |
|
response = `Il y a actuellement ${detectedAnomalies.length} anomalies détectées. Cliquez sur une anomalie dans la liste pour voir les détails.`; |
|
} else if (message.toLowerCase().includes('type') && detectedDocTypes.length > 0) { |
|
const typesList = detectedDocTypes.map(doc => getDocTypeName(doc.type)).join(', '); |
|
response = `Les types de documents détectés sont : ${typesList}.`; |
|
} |
|
|
|
addChatMessage("Assistant", response); |
|
}, 500); |
|
} |
|
|
|
|
|
window.confirmAnomaly = confirmAnomaly; |
|
</script> |
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=DaniGoua/pdf6" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |