pos-image-api / receipt_processing_ui.html
rairo's picture
Update receipt_processing_ui.html
9ec7be1 verified
<!DOCTYPE html>
<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>