OCR / static /index.html
Mariem-Daha's picture
Upload 6 files
fb7a6e9 verified
Raw
History Blame Contribute Delete
20.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Receipt OCR API - Test Interface</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;
}
header {
text-align: center;
color: white;
margin-bottom: 40px;
}
header h1 {
font-size: 3em;
margin-bottom: 10px;
}
.card {
background: white;
border-radius: 15px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.upload-area {
border: 3px dashed #667eea;
border-radius: 10px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover {
background: #f8f9ff;
border-color: #764ba2;
}
.upload-area.dragover {
background: #e8ecff;
border-color: #764ba2;
}
input[type="file"] {
display: none;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 15px 30px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: transform 0.2s;
}
.btn:hover {
transform: translateY(-2px);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading {
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;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.result {
margin-top: 20px;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.result-header h3 {
color: #333;
}
.status-badge {
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
}
.status-success {
background: #d4edda;
color: #155724;
}
.status-error {
background: #f8d7da;
color: #721c24;
}
.receipt-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.info-item {
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.info-label {
font-size: 12px;
color: #6c757d;
text-transform: uppercase;
margin-bottom: 5px;
}
.info-value {
font-size: 18px;
font-weight: bold;
color: #333;
}
.items-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.items-table th {
background: #667eea;
color: white;
padding: 12px;
text-align: left;
}
.items-table td {
padding: 12px;
border-bottom: 1px solid #dee2e6;
}
.items-table tr:hover {
background: #f8f9fa;
}
.json-view {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-top: 20px;
overflow-x: auto;
}
pre {
margin: 0;
font-size: 14px;
}
.receipts-list {
margin-top: 20px;
}
.receipt-card {
background: #f8f9fa;
border-radius: 8px;
padding: 15px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.3s;
}
.receipt-card:hover {
background: #e9ecef;
transform: translateX(5px);
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🧾 Receipt OCR API</h1>
<p>Upload receipts and get instant structured data extraction</p>
</header>
<div class="card">
<h2>Upload Receipt</h2>
<div class="upload-area" id="uploadArea">
<p style="font-size: 48px; margin-bottom: 10px;">📸</p>
<p style="font-size: 18px; margin-bottom: 10px;">Drag & drop your receipt here</p>
<p style="color: #6c757d;">or</p>
<button class="btn" onclick="document.getElementById('fileInput').click()">
Choose File
</button>
<input type="file" id="fileInput" accept="image/*,.pdf">
<p style="margin-top: 10px; color: #6c757d; font-size: 14px;">
Supported: JPG, PNG, PDF, TIFF, WEBP
</p>
</div>
<div id="loading" class="loading" style="display: none;">
<div class="spinner"></div>
<p style="margin-top: 15px; color: #667eea;">Processing receipt...</p>
</div>
<div id="result" class="result" style="display: none;"></div>
</div>
<div class="card">
<h2>Recent Receipts</h2>
<button class="btn" onclick="loadReceipts()">📋 Load Receipts</button>
<div id="receiptsList" class="receipts-list"></div>
</div>
<div class="card">
<h2>Statistics</h2>
<button class="btn" onclick="loadStats()">📊 Load Statistics</button>
<div id="statsView"></div>
</div>
</div>
<script>
const API_BASE = window.location.origin;
// File input change
document.getElementById('fileInput').addEventListener('change', (e) => {
if (e.target.files.length > 0) {
uploadFile(e.target.files[0]);
}
});
// Drag and drop
const uploadArea = document.getElementById('uploadArea');
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
uploadFile(e.dataTransfer.files[0]);
}
});
// Upload file
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
document.getElementById('loading').style.display = 'block';
document.getElementById('result').style.display = 'none';
try {
const response = await fetch(`${API_BASE}/upload`, {
method: 'POST',
body: formData
});
const data = await response.json();
document.getElementById('loading').style.display = 'none';
// Check if response is successful
if (data.success && data.data) {
displayResult(data);
} else if (data.detail) {
// FastAPI error response
displayError(data.detail);
} else if (data.error) {
// Custom error response
displayError(data.error);
} else {
displayError('Unknown error occurred during processing');
}
} catch (error) {
document.getElementById('loading').style.display = 'none';
displayError(error.message);
}
}
// Display result
function displayResult(data) {
const resultDiv = document.getElementById('result');
const receiptData = data.data;
// Validate receiptData exists
if (!receiptData) {
displayError('No receipt data received from server');
return;
}
let itemsHTML = '';
if (receiptData.items && receiptData.items.length > 0) {
itemsHTML = `
<h3 style="margin-top: 20px;">Items</h3>
<table class="items-table">
<thead>
<tr>
<th>Description</th>
<th>Qty</th>
<th>Unit Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
${receiptData.items.map(item => `
<tr>
<td>${item.description || 'N/A'}</td>
<td>${item.quantity || 'N/A'}</td>
<td>${item.unit_price ? item.unit_price.toFixed(2) : 'N/A'}</td>
<td><strong>${item.total_price ? item.total_price.toFixed(2) : 'N/A'}</strong></td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
resultDiv.innerHTML = `
<div class="result-header">
<h3>✅ Receipt Processed Successfully</h3>
<span class="status-badge status-success">ID: ${data.receipt_id}</span>
</div>
<div class="receipt-info">
<div class="info-item">
<div class="info-label">Merchant</div>
<div class="info-value">${receiptData.merchant_name || 'N/A'}</div>
</div>
<div class="info-item">
<div class="info-label">Date</div>
<div class="info-value">${receiptData.date || 'N/A'}</div>
</div>
<div class="info-item">
<div class="info-label">Time</div>
<div class="info-value">${receiptData.time || 'N/A'}</div>
</div>
<div class="info-item">
<div class="info-label">Total</div>
<div class="info-value">${receiptData.currency || ''} ${receiptData.total ? receiptData.total.toFixed(2) : 'N/A'}</div>
</div>
<div class="info-item">
<div class="info-label">Tax</div>
<div class="info-value">${receiptData.currency || ''} ${receiptData.tax ? receiptData.tax.toFixed(2) : 'N/A'}</div>
</div>
<div class="info-item">
<div class="info-label">Payment</div>
<div class="info-value">${receiptData.payment_method || 'N/A'}</div>
</div>
<div class="info-item" style="background: #e8f5e9;">
<div class="info-label">Processing Cost 💰</div>
<div class="info-value" style="color: #2e7d32;">
${data.processing_cost ? data.processing_cost.total : (receiptData.processing_cost ? `$${receiptData.processing_cost.toFixed(6)}` : '$0.00')}
</div>
</div>
</div>
${itemsHTML}
${data.processing_cost ? `
<div class="receipt-info" style="margin-top: 20px; border: 2px solid #4CAF50;">
<h4 style="margin: 0 0 15px 0; color: #2e7d32;">💰 Processing Cost Breakdown</h4>
<div class="info-item">
<div class="info-label">Total Cost</div>
<div class="info-value" style="color: #2e7d32; font-weight: bold;">${data.processing_cost.total}</div>
</div>
<div class="info-item">
<div class="info-label">Document AI</div>
<div class="info-value">${data.processing_cost.document_ai}</div>
</div>
<div class="info-item">
<div class="info-label">Gemini AI</div>
<div class="info-value">${data.processing_cost.gemini}</div>
</div>
<div class="info-item">
<div class="info-label">Tokens Used</div>
<div class="info-value">${data.processing_cost.tokens.total} tokens (${data.processing_cost.tokens.input} in + ${data.processing_cost.tokens.output} out)</div>
</div>
<div style="grid-column: 1 / -1; padding: 10px; background: #fff3cd; border-radius: 5px; margin-top: 10px;">
<small><strong>💡 Note:</strong> First 1,000 receipts/month are FREE due to Google Cloud free tiers. This cost only applies after exceeding free quota.</small>
</div>
</div>
` : ''}
<div class="json-view">
<h4>Complete JSON Data</h4>
<pre>${JSON.stringify(receiptData, null, 2)}</pre>
</div>
`;
resultDiv.style.display = 'block';
}
// Display error
function displayError(message) {
const resultDiv = document.getElementById('result');
resultDiv.innerHTML = `
<div class="result-header">
<h3>❌ Error</h3>
<span class="status-badge status-error">Failed</span>
</div>
<p style="color: #721c24; padding: 15px; background: #f8d7da; border-radius: 8px;">
${message}
</p>
`;
resultDiv.style.display = 'block';
}
// Load receipts
async function loadReceipts() {
try {
const response = await fetch(`${API_BASE}/receipts?limit=10`);
const data = await response.json();
const listDiv = document.getElementById('receiptsList');
if (data.receipts.length === 0) {
listDiv.innerHTML = '<p style="color: #6c757d; padding: 20px;">No receipts found. Upload one to get started!</p>';
return;
}
listDiv.innerHTML = data.receipts.map(receipt => `
<div class="receipt-card" onclick="viewReceipt(${receipt.id})">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<strong>${receipt.merchant_name || 'Unknown'}</strong>
<p style="color: #6c757d; font-size: 14px; margin-top: 5px;">
${receipt.date || 'N/A'} ${receipt.time || ''}
</p>
${receipt.processing_cost ? `<p style="color: #2e7d32; font-size: 12px; margin-top: 3px;">💰 $${receipt.processing_cost.toFixed(6)}</p>` : ''}
</div>
<div style="text-align: right;">
<div style="font-size: 20px; font-weight: bold; color: #667eea;">
${receipt.currency || ''} ${receipt.total ? receipt.total.toFixed(2) : 'N/A'}
</div>
<div style="color: #6c757d; font-size: 12px;">
${receipt.items_count} items
</div>
</div>
</div>
</div>
`).join('');
} catch (error) {
console.error('Error loading receipts:', error);
}
}
// View receipt
async function viewReceipt(id) {
try {
const response = await fetch(`${API_BASE}/receipts/${id}`);
const receipt = await response.json();
displayResult({ success: true, receipt_id: id, data: receipt });
window.scrollTo({ top: 0, behavior: 'smooth' });
} catch (error) {
console.error('Error loading receipt:', error);
}
}
// Load statistics
async function loadStats() {
try {
const response = await fetch(`${API_BASE}/stats`);
const stats = await response.json();
const statsDiv = document.getElementById('statsView');
statsDiv.innerHTML = `
<div class="receipt-info" style="margin-top: 20px;">
<div class="info-item">
<div class="info-label">Total Receipts</div>
<div class="info-value">${stats.total_receipts}</div>
</div>
<div class="info-item">
<div class="info-label">Total Amount</div>
<div class="info-value">$${stats.total_amount.toFixed(2)}</div>
</div>
</div>
<h3 style="margin-top: 20px;">Top Merchants</h3>
${stats.top_merchants.map(m => `
<div class="receipt-card">
<div style="display: flex; justify-content: space-between;">
<strong>${m.merchant}</strong>
<span>${m.receipt_count} receipts - $${m.total_spent.toFixed(2)}</span>
</div>
</div>
`).join('')}
`;
} catch (error) {
console.error('Error loading stats:', error);
}
}
</script>
</body>
</html>