Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Qx Intelligent Receipt Processing</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap'); | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', 'Segoe UI', Roboto, sans-serif; | |
| background: linear-gradient(135deg, #eef2f9 0%, #dbe6f7 100%); | |
| min-height: 100vh; | |
| color: #2e3a59; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 40px; | |
| color: #2e3a59; | |
| } | |
| .header h1 { | |
| font-size: 2.5rem; | |
| margin-bottom: 10px; | |
| text-shadow: 1px 1px 3px rgba(0,0,0,0.1); | |
| } | |
| .header p { | |
| font-size: 1.1rem; | |
| opacity: 0.85; | |
| } | |
| .main-content { | |
| background: white; | |
| border-radius: 20px; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.06); | |
| overflow: hidden; | |
| } | |
| .tabs { | |
| display: flex; | |
| background: #f1f5f9; | |
| border-bottom: 1px solid #dce3eb; | |
| } | |
| .tab { | |
| flex: 1; | |
| padding: 18px; | |
| text-align: center; | |
| background: none; | |
| border: none; | |
| cursor: pointer; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| color: #64748b; | |
| transition: background 0.3s, color 0.3s; | |
| } | |
| .tab.active { | |
| background: #4a90e2; | |
| color: white; | |
| } | |
| .tab:hover:not(.active) { | |
| background: #e3ecf8; | |
| color: #356ac3; | |
| } | |
| .tab-content { | |
| padding: 40px; | |
| display: none; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| .upload-area { | |
| border: 3px dashed #4a90e2; | |
| border-radius: 15px; | |
| padding: 40px; | |
| text-align: center; | |
| margin-bottom: 30px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| background: #f6f9fe; | |
| } | |
| .upload-area:hover, | |
| .upload-area.dragover { | |
| border-color: #356ac3; | |
| background: #e3ecf8; | |
| transform: scale(1.01); | |
| } | |
| .upload-icon { | |
| font-size: 3rem; | |
| color: #4a90e2; | |
| margin-bottom: 20px; | |
| } | |
| .btn { | |
| background: #4a90e2; | |
| color: white; | |
| border: none; | |
| padding: 12px 28px; | |
| border-radius: 25px; | |
| cursor: pointer; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| transition: all 0.3s ease; | |
| margin: 10px; | |
| } | |
| .btn:hover { | |
| background: #356ac3; | |
| transform: translateY(-1px); | |
| box-shadow: 0 4px 12px rgba(74, 144, 226, 0.3); | |
| } | |
| .btn:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| .btn-secondary { | |
| background: #64748b; | |
| } | |
| .btn-secondary:hover { | |
| background: #4b5563; | |
| } | |
| .btn-success { | |
| background: #22c55e; | |
| } | |
| .btn-success:hover { | |
| background: #16a34a; | |
| } | |
| .loading { | |
| display: none; | |
| text-align: center; | |
| padding: 20px; | |
| } | |
| .spinner { | |
| width: 40px; | |
| height: 40px; | |
| border: 4px solid #f3f3f3; | |
| border-top: 4px solid #4a90e2; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 15px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .receipt-card { | |
| background: #f1f5f9; | |
| border-radius: 10px; | |
| padding: 20px; | |
| margin-bottom: 20px; | |
| border-left: 5px solid #4a90e2; | |
| } | |
| .receipt-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 15px; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| } | |
| .store-name { | |
| font-size: 1.2rem; | |
| font-weight: bold; | |
| color: #356ac3; | |
| } | |
| .receipt-date { | |
| color: #64748b; | |
| font-size: 0.9rem; | |
| } | |
| .total-amount { | |
| font-size: 1.1rem; | |
| font-weight: bold; | |
| color: #22c55e; | |
| } | |
| .items-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin-top: 15px; | |
| } | |
| .items-table th, | |
| .items-table td { | |
| padding: 10px; | |
| text-align: left; | |
| border-bottom: 1px solid #dce3eb; | |
| } | |
| .items-table th { | |
| background: #e9eff6; | |
| font-weight: 600; | |
| color: #3b3f5c; | |
| } | |
| .category-stock { | |
| background: #d1fae5; | |
| color: #065f46; | |
| padding: 3px 8px; | |
| border-radius: 12px; | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| } | |
| .category-expense { | |
| background: #fee2e2; | |
| color: #991b1b; | |
| padding: 3px 8px; | |
| border-radius: 12px; | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| } | |
| .error { | |
| background: #fee2e2; | |
| color: #991b1b; | |
| padding: 15px; | |
| border-radius: 5px; | |
| margin-top: 15px; | |
| } | |
| .success { | |
| background: #d1fae5; | |
| color: #065f46; | |
| padding: 15px; | |
| border-radius: 5px; | |
| margin-top: 15px; | |
| } | |
| .file-list { | |
| margin-top: 20px; | |
| } | |
| .file-item { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 10px; | |
| background: #f8fafc; | |
| border-radius: 5px; | |
| margin-bottom: 10px; | |
| } | |
| .session-info { | |
| background: #e3ecf8; | |
| padding: 15px; | |
| border-radius: 10px; | |
| margin-bottom: 20px; | |
| } | |
| .hidden { | |
| display: none; | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 6px; | |
| background: #e9eff6; | |
| border-radius: 3px; | |
| overflow: hidden; | |
| margin-top: 15px; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: #4a90e2; | |
| width: 0%; | |
| transition: width 0.3s ease; | |
| } | |
| @media (max-width: 768px) { | |
| .container { | |
| padding: 10px; | |
| } | |
| .tabs { | |
| flex-direction: column; | |
| } | |
| .tab-content { | |
| padding: 20px; | |
| } | |
| .header h1 { | |
| font-size: 2rem; | |
| } | |
| .receipt-header { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>Qx Intelligent Receipt Processing</h1> | |
| <p>AI-Powered Receipt Analysis & Categorization</p> | |
| </div> | |
| <div class="main-content"> | |
| <div class="tabs"> | |
| <button class="tab active" onclick="switchTab('single')">Single Receipt</button> | |
| <button class="tab" onclick="switchTab('multipart')">Multi-Part Receipt</button> | |
| <button class="tab" onclick="switchTab('bulk')">Bulk Processing</button> | |
| </div> | |
| <!-- Single Receipt Tab --> | |
| <div id="single-tab" class="tab-content active"> | |
| <div class="upload-area" onclick="document.getElementById('single-file').click()" ondrop="handleDrop(event, 'single')" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)"> | |
| <div class="upload-icon">📄</div> | |
| <h3>Upload Receipt Image</h3> | |
| <p>Click here or drag and drop your receipt image</p> | |
| <input type="file" id="single-file" accept="image/*" style="display: none;" onchange="handleSingleFile(this)"> | |
| </div> | |
| <div class="text-center"> | |
| <button class="btn" onclick="processSingleReceipt()" id="single-process-btn" disabled>Process Receipt</button> | |
| </div> | |
| <div id="single-loading" class="loading"> | |
| <div class="spinner"></div> | |
| <p>Processing receipt...</p> | |
| </div> | |
| <div id="single-results" class="results"></div> | |
| </div> | |
| <!-- Multi-Part Receipt Tab --> | |
| <div id="multipart-tab" class="tab-content"> | |
| <div class="session-info hidden" id="session-info"> | |
| <h4>Session Active</h4> | |
| <p>Session ID: <span id="session-id"></span></p> | |
| <p>Parts uploaded: <span id="parts-count">0</span></p> | |
| </div> | |
| <div class="upload-area" onclick="document.getElementById('multipart-file').click()" ondrop="handleDrop(event, 'multipart')" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)"> | |
| <div class="upload-icon">📄</div> | |
| <h3>Upload Receipt Parts</h3> | |
| <p>Upload multiple images of the same long receipt</p> | |
| <input type="file" id="multipart-file" accept="image/*" style="display: none;" onchange="handleMultipartFile(this)"> | |
| </div> | |
| <div class="text-center"> | |
| <button class="btn btn-secondary" onclick="startSession()" id="start-session-btn">Start New Session</button> | |
| <button class="btn" onclick="addReceiptPart()" id="add-part-btn" disabled>Add Part</button> | |
| <button class="btn btn-success" onclick="processMultipartReceipt()" id="multipart-process-btn" disabled>Process Complete Receipt</button> | |
| </div> | |
| <div id="multipart-loading" class="loading"> | |
| <div class="spinner"></div> | |
| <p>Processing multi-part receipt...</p> | |
| </div> | |
| <div id="multipart-results" class="results"></div> | |
| </div> | |
| <!-- Bulk Processing Tab --> | |
| <div id="bulk-tab" class="tab-content"> | |
| <div class="upload-area" onclick="document.getElementById('bulk-files').click()" ondrop="handleDrop(event, 'bulk')" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)"> | |
| <div class="upload-icon">📁</div> | |
| <h3>Upload Multiple Receipts</h3> | |
| <p>Select multiple receipt images for bulk processing</p> | |
| <input type="file" id="bulk-files" accept="image/*" multiple style="display: none;" onchange="handleBulkFiles(this)"> | |
| </div> | |
| <div id="bulk-file-list" class="file-list hidden"></div> | |
| <div class="text-center"> | |
| <button class="btn" onclick="processBulkReceipts()" id="bulk-process-btn" disabled>Process All Receipts</button> | |
| </div> | |
| <div id="bulk-loading" class="loading"> | |
| <div class="spinner"></div> | |
| <p>Processing receipts...</p> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" id="bulk-progress"></div> | |
| </div> | |
| </div> | |
| <div id="bulk-results" class="results"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const API_BASE_URL = 'https://rairo-pos-image-api.hf.space'; | |
| let currentSessionId = null; | |
| let singleFile = null; | |
| let multipartFile = null; | |
| let bulkFiles = []; | |
| function switchTab(tabName) { | |
| // Hide all tabs | |
| document.querySelectorAll('.tab-content').forEach(tab => { | |
| tab.classList.remove('active'); | |
| }); | |
| document.querySelectorAll('.tab').forEach(tab => { | |
| tab.classList.remove('active'); | |
| }); | |
| // Show selected tab | |
| document.getElementById(tabName + '-tab').classList.add('active'); | |
| event.target.classList.add('active'); | |
| // Clear results | |
| clearResults(); | |
| } | |
| function clearResults() { | |
| document.querySelectorAll('.results').forEach(result => { | |
| result.innerHTML = ''; | |
| }); | |
| } | |
| function handleDragOver(event) { | |
| event.preventDefault(); | |
| event.currentTarget.classList.add('dragover'); | |
| } | |
| function handleDragLeave(event) { | |
| event.currentTarget.classList.remove('dragover'); | |
| } | |
| function handleDrop(event, type) { | |
| event.preventDefault(); | |
| event.currentTarget.classList.remove('dragover'); | |
| const files = event.dataTransfer.files; | |
| if (files.length > 0) { | |
| if (type === 'bulk') { | |
| handleBulkFiles({files: files}); | |
| } else { | |
| const input = document.getElementById(type + '-file'); | |
| input.files = files; | |
| if (type === 'single') { | |
| handleSingleFile(input); | |
| } else { | |
| handleMultipartFile(input); | |
| } | |
| } | |
| } | |
| } | |
| function handleSingleFile(input) { | |
| if (input.files && input.files[0]) { | |
| singleFile = input.files[0]; | |
| document.getElementById('single-process-btn').disabled = false; | |
| showMessage('single-results', `Selected: ${singleFile.name}`, 'success'); | |
| } | |
| } | |
| function handleMultipartFile(input) { | |
| if (input.files && input.files[0]) { | |
| multipartFile = input.files[0]; | |
| document.getElementById('add-part-btn').disabled = currentSessionId === null; | |
| } | |
| } | |
| function handleBulkFiles(input) { | |
| if (input.files && input.files.length > 0) { | |
| bulkFiles = Array.from(input.files); | |
| document.getElementById('bulk-process-btn').disabled = false; | |
| displayBulkFileList(); | |
| document.getElementById('bulk-file-list').classList.remove('hidden'); | |
| } | |
| } | |
| function displayBulkFileList() { | |
| const listContainer = document.getElementById('bulk-file-list'); | |
| listContainer.innerHTML = '<h4>Selected Files:</h4>'; | |
| bulkFiles.forEach((file, index) => { | |
| const fileItem = document.createElement('div'); | |
| fileItem.className = 'file-item'; | |
| fileItem.innerHTML = ` | |
| <span>${file.name}</span> | |
| <button class="btn btn-secondary" onclick="removeBulkFile(${index})" style="padding: 5px 10px; font-size: 0.8rem;">Remove</button> | |
| `; | |
| listContainer.appendChild(fileItem); | |
| }); | |
| } | |
| function removeBulkFile(index) { | |
| bulkFiles.splice(index, 1); | |
| if (bulkFiles.length === 0) { | |
| document.getElementById('bulk-file-list').classList.add('hidden'); | |
| document.getElementById('bulk-process-btn').disabled = true; | |
| } else { | |
| displayBulkFileList(); | |
| } | |
| } | |
| async function processSingleReceipt() { | |
| if (!singleFile) return; | |
| showLoading('single-loading'); | |
| clearResults(); | |
| const formData = new FormData(); | |
| formData.append('image', singleFile); | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/process-receipt`, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| hideLoading('single-loading'); | |
| if (result.success) { | |
| displayReceiptResult('single-results', result.data); | |
| } else { | |
| showMessage('single-results', `Error: ${result.error}`, 'error'); | |
| } | |
| } catch (error) { | |
| hideLoading('single-loading'); | |
| showMessage('single-results', `Network error: ${error.message}`, 'error'); | |
| } | |
| } | |
| async function startSession() { | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/start-receipt-session`, { | |
| method: 'POST' | |
| }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| currentSessionId = result.session_id; | |
| document.getElementById('session-id').textContent = currentSessionId; | |
| document.getElementById('session-info').classList.remove('hidden'); | |
| document.getElementById('start-session-btn').disabled = true; | |
| document.getElementById('add-part-btn').disabled = multipartFile === null; | |
| document.getElementById('multipart-process-btn').disabled = false; | |
| updatePartsCount(0); | |
| } else { | |
| showMessage('multipart-results', `Error: ${result.error}`, 'error'); | |
| } | |
| } catch (error) { | |
| showMessage('multipart-results', `Network error: ${error.message}`, 'error'); | |
| } | |
| } | |
| async function addReceiptPart() { | |
| if (!currentSessionId || !multipartFile) return; | |
| const formData = new FormData(); | |
| formData.append('image', multipartFile); | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/add-receipt-part/${currentSessionId}`, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| updatePartsCount(result.parts_count); | |
| showMessage('multipart-results', `Part added successfully. Total parts: ${result.parts_count}`, 'success'); | |
| multipartFile = null; | |
| document.getElementById('multipart-file').value = ''; | |
| document.getElementById('add-part-btn').disabled = true; | |
| } else { | |
| showMessage('multipart-results', `Error: ${result.error}`, 'error'); | |
| } | |
| } catch (error) { | |
| showMessage('multipart-results', `Network error: ${error.message}`, 'error'); | |
| } | |
| } | |
| async function processMultipartReceipt() { | |
| if (!currentSessionId) return; | |
| showLoading('multipart-loading'); | |
| clearResults(); | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/process-receipt-session/${currentSessionId}`, { | |
| method: 'POST' | |
| }); | |
| const result = await response.json(); | |
| hideLoading('multipart-loading'); | |
| if (result.success) { | |
| displayReceiptResult('multipart-results', result.data); | |
| resetMultipartSession(); | |
| } else { | |
| showMessage('multipart-results', `Error: ${result.error}`, 'error'); | |
| } | |
| } catch (error) { | |
| hideLoading('multipart-loading'); | |
| showMessage('multipart-results', `Network error: ${error.message}`, 'error'); | |
| } | |
| } | |
| async function processBulkReceipts() { | |
| if (bulkFiles.length === 0) return; | |
| showLoading('bulk-loading'); | |
| clearResults(); | |
| const formData = new FormData(); | |
| bulkFiles.forEach(file => { | |
| formData.append('images', file); | |
| }); | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/bulk-process-receipts`, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| hideLoading('bulk-loading'); | |
| if (result.success) { | |
| displayBulkResults('bulk-results', result); | |
| } else { | |
| showMessage('bulk-results', `Error: ${result.error}`, 'error'); | |
| } | |
| } catch (error) { | |
| hideLoading('bulk-loading'); | |
| showMessage('bulk-results', `Network error: ${error.message}`, 'error'); | |
| } | |
| } | |
| function displayReceiptResult(containerId, receiptData) { | |
| const container = document.getElementById(containerId); | |
| const receiptCard = document.createElement('div'); | |
| receiptCard.className = 'receipt-card'; | |
| receiptCard.innerHTML = ` | |
| <div class="receipt-header"> | |
| <div> | |
| <div class="store-name">${receiptData.store_name || 'Unknown Store'}</div> | |
| <div class="receipt-date">${receiptData.receipt_date || 'No date'}</div> | |
| </div> | |
| <div class="total-amount">Total: $${receiptData.total_amount || '0.00'}</div> | |
| </div> | |
| <table class="items-table"> | |
| <thead> | |
| <tr> | |
| <th>Item</th> | |
| <th>Qty</th> | |
| <th>Unit Price</th> | |
| <th>Total</th> | |
| <th>Category</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| ${receiptData.items ? receiptData.items.map(item => ` | |
| <tr> | |
| <td>${item.name}</td> | |
| <td>${item.quantity}</td> | |
| <td>$${item.unit_price}</td> | |
| <td>$${item.total_price}</td> | |
| <td><span class="category-${item.category}">${item.category.toUpperCase()}</span></td> | |
| </tr> | |
| `).join('') : '<tr><td colspan="5">No items found</td></tr>'} | |
| </tbody> | |
| </table> | |
| `; | |
| container.appendChild(receiptCard); | |
| } | |
| function displayBulkResults(containerId, bulkResult) { | |
| const container = document.getElementById(containerId); | |
| const summaryDiv = document.createElement('div'); | |
| summaryDiv.className = 'success'; | |
| summaryDiv.innerHTML = ` | |
| <h4>Bulk Processing Complete</h4> | |
| <p>Successfully processed: ${bulkResult.processed_count} receipts</p> | |
| <p>Errors: ${bulkResult.error_count}</p> | |
| `; | |
| container.appendChild(summaryDiv); | |
| if (bulkResult.errors && bulkResult.errors.length > 0) { | |
| const errorsDiv = document.createElement('div'); | |
| errorsDiv.className = 'error'; | |
| errorsDiv.innerHTML = ` | |
| <h5>Errors:</h5> | |
| <ul> | |
| ${bulkResult.errors.map(error => `<li>${error}</li>`).join('')} | |
| </ul> | |
| `; | |
| container.appendChild(errorsDiv); | |
| } | |
| if (bulkResult.results && bulkResult.results.length > 0) { | |
| bulkResult.results.forEach((result, index) => { | |
| const resultDiv = document.createElement('div'); | |
| resultDiv.innerHTML = `<h4>Receipt ${result.file_index}: ${result.filename}</h4>`; | |
| container.appendChild(resultDiv); | |
| displayReceiptResult(containerId, result.data); | |
| }); | |
| } | |
| } | |
| function updatePartsCount(count) { | |
| document.getElementById('parts-count').textContent = count; | |
| } | |
| function resetMultipartSession() { | |
| currentSessionId = null; | |
| document.getElementById('session-info').classList.add('hidden'); | |
| document.getElementById('start-session-btn').disabled = false; | |
| document.getElementById('add-part-btn').disabled = true; | |
| document.getElementById('multipart-process-btn').disabled = true; | |
| document.getElementById('multipart-file').value = ''; | |
| multipartFile = null; | |
| } | |
| function showLoading(loadingId) { | |
| document.getElementById(loadingId).style.display = 'block'; | |
| } | |
| function hideLoading(loadingId) { | |
| document.getElementById(loadingId).style.display = 'none'; | |
| } | |
| function showMessage(containerId, message, type) { | |
| const container = document.getElementById(containerId); | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = type; | |
| messageDiv.textContent = message; | |
| container.appendChild(messageDiv); | |
| } | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Prevent default drag behaviors | |
| document.addEventListener('dragenter', e => e.preventDefault()); | |
| document.addEventListener('dragover', e => e.preventDefault()); | |
| document.addEventListener('dragleave', e => e.preventDefault()); | |
| document.addEventListener('drop', e => e.preventDefault()); | |
| }); | |
| </script> | |
| </body> | |
| </html> |