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