|
|
<!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> |
|
|
|