| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>InklyAI - Signature Verification</title> |
| <style> |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| min-height: 100vh; |
| padding: 20px; |
| } |
| |
| .container { |
| max-width: 1200px; |
| margin: 0 auto; |
| background: white; |
| border-radius: 20px; |
| box-shadow: 0 20px 40px rgba(0,0,0,0.1); |
| overflow: hidden; |
| } |
| |
| .header { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| color: white; |
| padding: 30px; |
| text-align: center; |
| } |
| |
| .header h1 { |
| font-size: 2.5em; |
| margin-bottom: 10px; |
| font-weight: 300; |
| } |
| |
| .header p { |
| font-size: 1.2em; |
| opacity: 0.9; |
| } |
| |
| .main-content { |
| padding: 40px; |
| } |
| |
| .upload-section { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 40px; |
| margin-bottom: 40px; |
| } |
| |
| .upload-box { |
| border: 3px dashed #ddd; |
| border-radius: 15px; |
| padding: 40px; |
| text-align: center; |
| transition: all 0.3s ease; |
| cursor: pointer; |
| background: #fafafa; |
| } |
| |
| .upload-box:hover { |
| border-color: #667eea; |
| background: #f0f4ff; |
| } |
| |
| .upload-box.dragover { |
| border-color: #667eea; |
| background: #e8f2ff; |
| transform: scale(1.02); |
| } |
| |
| .upload-icon { |
| font-size: 3em; |
| color: #667eea; |
| margin-bottom: 20px; |
| } |
| |
| .upload-text { |
| font-size: 1.1em; |
| color: #666; |
| margin-bottom: 20px; |
| } |
| |
| .file-input { |
| display: none; |
| } |
| |
| .upload-btn { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| color: white; |
| border: none; |
| padding: 12px 30px; |
| border-radius: 25px; |
| cursor: pointer; |
| font-size: 1em; |
| transition: transform 0.2s ease; |
| } |
| |
| .upload-btn:hover { |
| transform: translateY(-2px); |
| } |
| |
| .preview-container { |
| margin-top: 20px; |
| text-align: center; |
| } |
| |
| .preview-image { |
| max-width: 200px; |
| max-height: 200px; |
| border-radius: 10px; |
| box-shadow: 0 5px 15px rgba(0,0,0,0.1); |
| margin-bottom: 10px; |
| } |
| |
| .file-info { |
| font-size: 0.9em; |
| color: #666; |
| } |
| |
| .verification-section { |
| background: #f8f9fa; |
| border-radius: 15px; |
| padding: 30px; |
| margin-bottom: 30px; |
| } |
| |
| .verification-title { |
| font-size: 1.5em; |
| margin-bottom: 20px; |
| color: #333; |
| text-align: center; |
| } |
| |
| .verify-btn { |
| background: linear-gradient(135deg, #28a745 0%, #20c997 100%); |
| color: white; |
| border: none; |
| padding: 15px 40px; |
| border-radius: 25px; |
| cursor: pointer; |
| font-size: 1.2em; |
| width: 100%; |
| margin-bottom: 20px; |
| transition: transform 0.2s ease; |
| } |
| |
| .verify-btn:hover { |
| transform: translateY(-2px); |
| } |
| |
| .verify-btn:disabled { |
| background: #6c757d; |
| cursor: not-allowed; |
| transform: none; |
| } |
| |
| .result-section { |
| background: white; |
| border-radius: 15px; |
| padding: 30px; |
| box-shadow: 0 5px 15px rgba(0,0,0,0.1); |
| display: none; |
| } |
| |
| .result-title { |
| font-size: 1.3em; |
| margin-bottom: 20px; |
| text-align: center; |
| } |
| |
| .result-content { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 30px; |
| align-items: center; |
| } |
| |
| .result-image { |
| max-width: 100%; |
| border-radius: 10px; |
| box-shadow: 0 5px 15px rgba(0,0,0,0.1); |
| } |
| |
| .result-details { |
| padding: 20px; |
| } |
| |
| .result-item { |
| display: flex; |
| justify-content: space-between; |
| margin-bottom: 15px; |
| padding: 10px; |
| background: #f8f9fa; |
| border-radius: 8px; |
| } |
| |
| .result-label { |
| font-weight: 600; |
| color: #333; |
| } |
| |
| .result-value { |
| color: #666; |
| } |
| |
| .similarity-score { |
| font-size: 1.5em; |
| font-weight: bold; |
| color: #28a745; |
| } |
| |
| .verification-status { |
| font-size: 1.2em; |
| font-weight: bold; |
| padding: 10px 20px; |
| border-radius: 20px; |
| text-align: center; |
| } |
| |
| .verified { |
| background: #d4edda; |
| color: #155724; |
| } |
| |
| .not-verified { |
| background: #f8d7da; |
| color: #721c24; |
| } |
| |
| .loading { |
| display: none; |
| text-align: center; |
| padding: 20px; |
| } |
| |
| .spinner { |
| border: 4px solid #f3f3f3; |
| border-top: 4px solid #667eea; |
| border-radius: 50%; |
| width: 40px; |
| height: 40px; |
| animation: spin 1s linear infinite; |
| margin: 0 auto 20px; |
| } |
| |
| @keyframes spin { |
| 0% { transform: rotate(0deg); } |
| 100% { transform: rotate(360deg); } |
| } |
| |
| .agent-section { |
| background: #e8f2ff; |
| border-radius: 15px; |
| padding: 30px; |
| margin-bottom: 30px; |
| } |
| |
| .agent-title { |
| font-size: 1.3em; |
| margin-bottom: 20px; |
| color: #333; |
| text-align: center; |
| } |
| |
| .agent-select { |
| width: 100%; |
| padding: 12px; |
| border: 2px solid #ddd; |
| border-radius: 8px; |
| font-size: 1em; |
| margin-bottom: 20px; |
| } |
| |
| .stats-section { |
| background: #f8f9fa; |
| border-radius: 15px; |
| padding: 30px; |
| margin-top: 30px; |
| } |
| |
| .stats-title { |
| font-size: 1.3em; |
| margin-bottom: 20px; |
| color: #333; |
| text-align: center; |
| } |
| |
| .stats-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
| gap: 20px; |
| } |
| |
| .stat-card { |
| background: white; |
| padding: 20px; |
| border-radius: 10px; |
| text-align: center; |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); |
| } |
| |
| .stat-value { |
| font-size: 2em; |
| font-weight: bold; |
| color: #667eea; |
| margin-bottom: 5px; |
| } |
| |
| .stat-label { |
| color: #666; |
| font-size: 0.9em; |
| } |
| |
| .error-message { |
| background: #f8d7da; |
| color: #721c24; |
| padding: 15px; |
| border-radius: 8px; |
| margin: 20px 0; |
| display: none; |
| } |
| |
| .success-message { |
| background: #d4edda; |
| color: #155724; |
| padding: 15px; |
| border-radius: 8px; |
| margin: 20px 0; |
| display: none; |
| } |
| |
| @media (max-width: 768px) { |
| .upload-section { |
| grid-template-columns: 1fr; |
| gap: 20px; |
| } |
| |
| .result-content { |
| grid-template-columns: 1fr; |
| } |
| |
| .main-content { |
| padding: 20px; |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <div class="header"> |
| <h1>InklyAI</h1> |
| <p>Advanced E-Signature Verification System</p> |
| </div> |
|
|
| <div class="main-content"> |
| |
| <div id="errorMessage" class="error-message"></div> |
| <div id="successMessage" class="success-message"></div> |
|
|
| |
| <div class="agent-section"> |
| <h2 class="agent-title">Select Agent for Verification</h2> |
| <select id="agentSelect" class="agent-select"> |
| <option value="">Select an agent...</option> |
| </select> |
| </div> |
|
|
| |
| <div class="upload-section"> |
| <div class="upload-box" id="uploadBox1"> |
| <div class="upload-icon">📝</div> |
| <div class="upload-text">Upload Reference Signature</div> |
| <button class="upload-btn" onclick="document.getElementById('file1').click()"> |
| Choose File |
| </button> |
| <input type="file" id="file1" class="file-input" accept="image/*" onchange="handleFileSelect(event, 1)"> |
| <div class="preview-container" id="preview1"></div> |
| </div> |
|
|
| <div class="upload-box" id="uploadBox2"> |
| <div class="upload-icon">✍️</div> |
| <div class="upload-text">Upload Signature to Verify</div> |
| <button class="upload-btn" onclick="document.getElementById('file2').click()"> |
| Choose File |
| </button> |
| <input type="file" id="file2" class="file-input" accept="image/*" onchange="handleFileSelect(event, 2)"> |
| <div class="preview-container" id="preview2"></div> |
| </div> |
| </div> |
|
|
| |
| <div class="verification-section"> |
| <h2 class="verification-title">Signature Verification</h2> |
| <button id="verifyBtn" class="verify-btn" onclick="verifySignatures()"> |
| Verify Signatures |
| </button> |
| <div class="loading" id="loading"> |
| <div class="spinner"></div> |
| <div>Verifying signatures...</div> |
| </div> |
| </div> |
|
|
| |
| <div class="result-section" id="resultSection"> |
| <h2 class="result-title">Verification Result</h2> |
| <div class="result-content"> |
| <div> |
| <img id="resultImage1" class="result-image" alt="Reference Signature"> |
| <div class="file-info" id="resultInfo1"></div> |
| </div> |
| <div class="result-details"> |
| <div class="result-item"> |
| <span class="result-label">Verification Status:</span> |
| <span id="verificationStatus" class="result-value verification-status"></span> |
| </div> |
| <div class="result-item"> |
| <span class="result-label">Similarity Score:</span> |
| <span id="similarityScore" class="result-value similarity-score"></span> |
| </div> |
| <div class="result-item"> |
| <span class="result-label">Confidence:</span> |
| <span id="confidence" class="result-value"></span> |
| </div> |
| <div class="result-item"> |
| <span class="result-label">Verification ID:</span> |
| <span id="verificationId" class="result-value"></span> |
| </div> |
| <div class="result-item"> |
| <span class="result-label">Timestamp:</span> |
| <span id="timestamp" class="result-value"></span> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="stats-section"> |
| <h2 class="stats-title">Agent Statistics</h2> |
| <div class="stats-grid" id="statsGrid"> |
| |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| let selectedFiles = { file1: null, file2: null }; |
| let selectedAgent = null; |
| |
| |
| document.addEventListener('DOMContentLoaded', function() { |
| loadAgents(); |
| loadStats(); |
| }); |
| |
| |
| ['uploadBox1', 'uploadBox2'].forEach((boxId, index) => { |
| const box = document.getElementById(boxId); |
| const fileInput = document.getElementById(`file${index + 1}`); |
| |
| box.addEventListener('dragover', (e) => { |
| e.preventDefault(); |
| box.classList.add('dragover'); |
| }); |
| |
| box.addEventListener('dragleave', () => { |
| box.classList.remove('dragover'); |
| }); |
| |
| box.addEventListener('drop', (e) => { |
| e.preventDefault(); |
| box.classList.remove('dragover'); |
| const files = e.dataTransfer.files; |
| if (files.length > 0) { |
| fileInput.files = files; |
| handleFileSelect({ target: fileInput }, index + 1); |
| } |
| }); |
| }); |
| |
| function handleFileSelect(event, fileNumber) { |
| const file = event.target.files[0]; |
| if (file) { |
| selectedFiles[`file${fileNumber}`] = file; |
| displayPreview(file, fileNumber); |
| updateVerifyButton(); |
| } |
| } |
| |
| function displayPreview(file, fileNumber) { |
| const preview = document.getElementById(`preview${fileNumber}`); |
| const reader = new FileReader(); |
| |
| reader.onload = function(e) { |
| preview.innerHTML = ` |
| <img src="${e.target.result}" class="preview-image" alt="Preview"> |
| <div class="file-info"> |
| <div>${file.name}</div> |
| <div>${(file.size / 1024).toFixed(1)} KB</div> |
| </div> |
| `; |
| }; |
| |
| reader.readAsDataURL(file); |
| } |
| |
| function updateVerifyButton() { |
| const verifyBtn = document.getElementById('verifyBtn'); |
| const hasFiles = selectedFiles.file1 && selectedFiles.file2; |
| const hasAgent = selectedAgent && selectedAgent !== ''; |
| |
| verifyBtn.disabled = !hasFiles || !hasAgent; |
| verifyBtn.textContent = hasFiles && hasAgent ? 'Verify Signatures' : 'Select Agent and Upload Both Signatures'; |
| } |
| |
| async function loadAgents() { |
| try { |
| const response = await fetch('/api/agents'); |
| const data = await response.json(); |
| |
| const select = document.getElementById('agentSelect'); |
| select.innerHTML = '<option value="">Select an agent...</option>'; |
| |
| data.agents.forEach(agent => { |
| const option = document.createElement('option'); |
| option.value = agent.agent_id; |
| option.textContent = `${agent.agent_id} (${agent.is_active ? 'Active' : 'Inactive'})`; |
| select.appendChild(option); |
| }); |
| |
| select.addEventListener('change', function() { |
| selectedAgent = this.value; |
| updateVerifyButton(); |
| }); |
| } catch (error) { |
| showError('Failed to load agents: ' + error.message); |
| } |
| } |
| |
| async function loadStats() { |
| try { |
| const response = await fetch('/api/stats'); |
| const data = await response.json(); |
| |
| const statsGrid = document.getElementById('statsGrid'); |
| statsGrid.innerHTML = ''; |
| |
| Object.entries(data.stats).forEach(([agentId, stats]) => { |
| const statCard = document.createElement('div'); |
| statCard.className = 'stat-card'; |
| statCard.innerHTML = ` |
| <div class="stat-value">${stats.total_verifications}</div> |
| <div class="stat-label">${agentId} Verifications</div> |
| <div style="margin-top: 10px; font-size: 0.8em; color: #666;"> |
| Success Rate: ${(stats.success_rate * 100).toFixed(1)}% |
| </div> |
| `; |
| statsGrid.appendChild(statCard); |
| }); |
| } catch (error) { |
| console.error('Failed to load stats:', error); |
| } |
| } |
| |
| async function verifySignatures() { |
| if (!selectedFiles.file1 || !selectedFiles.file2 || !selectedAgent) { |
| showError('Please select an agent and upload both signatures'); |
| return; |
| } |
| |
| const loading = document.getElementById('loading'); |
| const verifyBtn = document.getElementById('verifyBtn'); |
| const resultSection = document.getElementById('resultSection'); |
| |
| |
| loading.style.display = 'block'; |
| verifyBtn.disabled = true; |
| resultSection.style.display = 'none'; |
| |
| try { |
| const formData = new FormData(); |
| formData.append('agent_id', selectedAgent); |
| formData.append('signature1', selectedFiles.file1); |
| formData.append('signature2', selectedFiles.file2); |
| |
| const response = await fetch('/api/verify', { |
| method: 'POST', |
| body: formData |
| }); |
| |
| const result = await response.json(); |
| |
| if (result.success) { |
| displayResult(result); |
| showSuccess('Signatures verified successfully!'); |
| loadStats(); |
| } else { |
| showError('Verification failed: ' + result.error); |
| } |
| } catch (error) { |
| showError('Verification error: ' + error.message); |
| } finally { |
| loading.style.display = 'none'; |
| verifyBtn.disabled = false; |
| } |
| } |
| |
| function displayResult(result) { |
| const resultSection = document.getElementById('resultSection'); |
| const verificationStatus = document.getElementById('verificationStatus'); |
| const similarityScore = document.getElementById('similarityScore'); |
| const confidence = document.getElementById('confidence'); |
| const verificationId = document.getElementById('verificationId'); |
| const timestamp = document.getElementById('timestamp'); |
| |
| |
| verificationStatus.textContent = result.is_verified ? 'VERIFIED' : 'NOT VERIFIED'; |
| verificationStatus.className = `result-value verification-status ${result.is_verified ? 'verified' : 'not-verified'}`; |
| |
| similarityScore.textContent = (result.similarity_score * 100).toFixed(1) + '%'; |
| confidence.textContent = (result.confidence * 100).toFixed(1) + '%'; |
| verificationId.textContent = result.verification_id; |
| timestamp.textContent = new Date(result.timestamp).toLocaleString(); |
| |
| |
| const resultImage1 = document.getElementById('resultImage1'); |
| const resultInfo1 = document.getElementById('resultInfo1'); |
| |
| if (selectedFiles.file1) { |
| const reader = new FileReader(); |
| reader.onload = function(e) { |
| resultImage1.src = e.target.result; |
| }; |
| reader.readAsDataURL(selectedFiles.file1); |
| resultInfo1.textContent = `Reference: ${selectedFiles.file1.name}`; |
| } |
| |
| resultSection.style.display = 'block'; |
| } |
| |
| function showError(message) { |
| const errorDiv = document.getElementById('errorMessage'); |
| errorDiv.textContent = message; |
| errorDiv.style.display = 'block'; |
| setTimeout(() => { |
| errorDiv.style.display = 'none'; |
| }, 5000); |
| } |
| |
| function showSuccess(message) { |
| const successDiv = document.getElementById('successMessage'); |
| successDiv.textContent = message; |
| successDiv.style.display = 'block'; |
| setTimeout(() => { |
| successDiv.style.display = 'none'; |
| }, 3000); |
| } |
| </script> |
| </body> |
| </html> |
|
|