← back to Wine Finder
public/js/verify.js
467 lines
/**
* Wine Label Verification - Mobile-Optimized
*/
// State
let selectedImage = null;
let selectedFile = null;
// DOM Elements
const uploadZone = document.getElementById('uploadZone');
const fileInput = document.getElementById('fileInput');
const cameraInput = document.getElementById('cameraInput');
const cameraBtn = document.getElementById('cameraBtn');
const galleryBtn = document.getElementById('galleryBtn');
const verifyBtn = document.getElementById('verifyBtn');
const previewContainer = document.getElementById('previewContainer');
const previewImage = document.getElementById('previewImage');
const removeImageBtn = document.getElementById('removeImageBtn');
const resultsContainer = document.getElementById('resultsContainer');
const loadingOverlay = document.getElementById('loadingOverlay');
const logoutBtn = document.getElementById('logoutBtn');
// Initialize
document.addEventListener('DOMContentLoaded', () => {
setupEventListeners();
checkMobileCapabilities();
});
function setupEventListeners() {
// Upload zone click
uploadZone.addEventListener('click', () => {
fileInput.click();
});
// File input change
fileInput.addEventListener('change', handleFileSelect);
cameraInput.addEventListener('change', handleFileSelect);
// Camera button (mobile)
cameraBtn.addEventListener('click', (e) => {
e.stopPropagation();
cameraInput.click();
});
// Gallery button
galleryBtn.addEventListener('click', (e) => {
e.stopPropagation();
fileInput.click();
});
// Verify button
verifyBtn.addEventListener('click', verifyWineLabel);
// Remove image button
removeImageBtn.addEventListener('click', clearSelectedImage);
// Drag and drop (desktop)
uploadZone.addEventListener('dragover', handleDragOver);
uploadZone.addEventListener('dragleave', handleDragLeave);
uploadZone.addEventListener('drop', handleDrop);
// Logout
logoutBtn.addEventListener('click', () => {
window.location.href = '/logout';
});
// Prevent default drag behavior on whole page
document.addEventListener('dragover', (e) => e.preventDefault());
document.addEventListener('drop', (e) => e.preventDefault());
}
function checkMobileCapabilities() {
// Check if device has camera
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
if (!isMobile) {
cameraBtn.style.display = 'none';
}
}
async function handleFileSelect(e) {
const file = e.target.files[0];
if (!file) return;
// Validate file type
if (!file.type.match('image.*')) {
alert('Please select an image file');
return;
}
// Validate file size (10MB max)
if (file.size > 10 * 1024 * 1024) {
alert('Image file is too large. Please select an image under 10MB.');
return;
}
selectedFile = file;
displayImagePreview(file);
// If camera captured (from cameraInput), automatically search all databases
if (e.target.id === 'cameraInput') {
console.log('Camera capture detected - searching all databases...');
await searchAllDatabasesForWine(file);
}
}
function displayImagePreview(file) {
const reader = new FileReader();
reader.onload = (e) => {
selectedImage = e.target.result;
previewImage.src = selectedImage;
previewContainer.classList.add('show');
uploadZone.style.display = 'none';
verifyBtn.disabled = false;
resultsContainer.classList.remove('show');
};
reader.readAsDataURL(file);
}
function clearSelectedImage() {
selectedImage = null;
selectedFile = null;
previewImage.src = '';
previewContainer.classList.remove('show');
uploadZone.style.display = 'block';
verifyBtn.disabled = true;
resultsContainer.classList.remove('show');
fileInput.value = '';
cameraInput.value = '';
}
function handleDragOver(e) {
e.preventDefault();
e.stopPropagation();
uploadZone.classList.add('dragover');
}
function handleDragLeave(e) {
e.preventDefault();
e.stopPropagation();
uploadZone.classList.remove('dragover');
}
function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
uploadZone.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
if (file.type.match('image.*')) {
selectedFile = file;
displayImagePreview(file);
} else {
alert('Please drop an image file');
}
}
}
async function verifyWineLabel() {
if (!selectedFile) {
alert('Please select an image first');
return;
}
// Show loading
loadingOverlay.classList.add('show');
verifyBtn.disabled = true;
try {
// Create form data
const formData = new FormData();
formData.append('image', selectedFile);
formData.append('confidenceThreshold', '0.40');
formData.append('crossCheckDatabase', 'true');
// Call verification API
const response = await fetch('/api/verification/verify-label', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Verification failed');
}
const result = await response.json();
// Display results
displayResults(result);
} catch (error) {
console.error('Verification error:', error);
alert('Error verifying wine label. Please try again.');
} finally {
loadingOverlay.classList.remove('show');
verifyBtn.disabled = false;
}
}
function displayResults(result) {
if (!result.success) {
alert('Verification failed: ' + (result.error || 'Unknown error'));
return;
}
const verification = result.verification;
// Update status
const statusEl = document.getElementById('resultStatus');
const statusMessages = {
'authentic': { icon: '✅', text: 'Authentic Wine', class: 'authentic' },
'likely_authentic': { icon: '✓', text: 'Likely Authentic', class: 'likely_authentic' },
'unknown': { icon: '❓', text: 'Unknown Wine', class: 'unknown' },
'suspicious': { icon: '⚠️', text: 'Suspicious - Verify Manually', class: 'suspicious' },
'error': { icon: '❌', text: 'Verification Error', class: 'suspicious' }
};
const statusInfo = statusMessages[verification.status] || statusMessages['unknown'];
statusEl.className = `result-status ${statusInfo.class}`;
statusEl.innerHTML = `
<span class="status-icon">${statusInfo.icon}</span>
<span>${statusInfo.text}</span>
`;
// Update confidence
const confidence = Math.round(verification.confidence * 100);
document.getElementById('confidenceValue').textContent = `${confidence}%`;
const confidenceFill = document.getElementById('confidenceFill');
setTimeout(() => {
confidenceFill.style.width = `${confidence}%`;
}, 100);
// Update wine info
const wineInfoEl = document.getElementById('wineInfo');
wineInfoEl.innerHTML = '';
const wine = verification.wine;
const infoItems = [
{ label: 'Wine Name', value: wine.name || 'Not detected' },
{ label: 'Vintage', value: wine.vintage || 'Not detected' },
{ label: 'Winery', value: wine.winery || 'Not detected' },
{ label: 'Region', value: wine.region || 'Not detected' },
{ label: 'Type', value: wine.type || 'Not detected' },
{ label: 'Appellation', value: wine.appellation || 'Not detected' },
{ label: 'Alcohol', value: wine.alcohol || 'Not detected' }
];
if (wine.organic) {
infoItems.push({ label: 'Organic', value: '✓ Certified' });
}
if (wine.sustainable) {
infoItems.push({ label: 'Sustainable', value: '✓ Certified' });
}
infoItems.forEach(item => {
const div = document.createElement('div');
div.className = 'info-item';
div.innerHTML = `
<span class="info-label">${item.label}</span>
<span class="info-value">${item.value}</span>
`;
wineInfoEl.appendChild(div);
});
// Update details
const details = verification.details;
const detailSection = document.getElementById('detailSection');
const detailList = document.getElementById('detailList');
if (details) {
detailList.innerHTML = '';
const detailItems = [];
if (details.labelDetected) {
detailItems.push('Wine label detected in image');
}
if (details.apiUsed) {
detailItems.push(`Verification method: ${details.apiUsed}`);
}
if (details.textExtracted && details.textExtracted.length > 0) {
detailItems.push(`Text extracted: ${details.textExtracted.length} item(s)`);
}
if (details.databaseMatch) {
detailItems.push('Matched against wine database');
}
if (details.predictions && details.predictions.length > 0) {
detailItems.push(`${details.predictions.length} prediction(s) made`);
}
detailItems.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
detailList.appendChild(li);
});
detailSection.style.display = detailItems.length > 0 ? 'block' : 'none';
}
// Show results
resultsContainer.classList.add('show');
// Scroll to results
setTimeout(() => {
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 300);
}
/**
* Search all wine databases after camera capture
* Extracts wine info from label and searches across all sources
*/
async function searchAllDatabasesForWine(file) {
try {
// Show loading state
const searchStatus = document.getElementById('searchStatus');
if (searchStatus) {
searchStatus.textContent = '🔍 Searching all databases...';
searchStatus.style.display = 'block';
}
// First, verify the wine label to extract information
console.log('Step 1: Verifying wine label with Roboflow...');
const formData = new FormData();
formData.append('image', file);
formData.append('confidenceThreshold', '0.40');
formData.append('crossCheckDatabase', 'true');
const verifyResponse = await fetch('/api/verification/verify-label', {
method: 'POST',
body: formData
});
if (!verifyResponse.ok) {
throw new Error('Verification failed');
}
const verificationResult = await verifyResponse.json();
console.log('Verification result:', verificationResult);
// Extract wine name for search
let searchQuery = '';
if (verificationResult.success && verificationResult.verification.wine) {
const wine = verificationResult.verification.wine;
searchQuery = wine.name || wine.winery || '';
// Add vintage if available
if (wine.vintage) {
searchQuery += ' ' + wine.vintage;
}
}
// If we got a wine name, search all databases
if (searchQuery) {
console.log('Step 2: Searching databases for:', searchQuery);
if (searchStatus) {
searchStatus.textContent = `🔍 Searching for "${searchQuery}"...`;
}
// Search all sources
const sources = ['kl', 'vivino', 'totalwine', 'winesensed', 'xwines', 'ucdavis-ava'];
const searchResponse = await fetch(`/api/scraper/search?query=${encodeURIComponent(searchQuery)}&sources=${sources.join(',')}`);
if (searchResponse.ok) {
const searchResults = await searchResponse.json();
console.log('Database search results:', searchResults);
// Display search results
displayDatabaseSearchResults(searchResults, searchQuery);
}
}
// Always show verification results
displayResults(verificationResult);
if (searchStatus) {
searchStatus.style.display = 'none';
}
} catch (error) {
console.error('Database search error:', error);
const searchStatus = document.getElementById('searchStatus');
if (searchStatus) {
searchStatus.textContent = '⚠️ Search error - showing verification only';
setTimeout(() => {
searchStatus.style.display = 'none';
}, 3000);
}
}
}
/**
* Display database search results
*/
function displayDatabaseSearchResults(results, query) {
const searchResultsContainer = document.getElementById('databaseSearchResults');
if (!searchResultsContainer) return;
searchResultsContainer.innerHTML = '';
if (!results.wines || results.wines.length === 0) {
searchResultsContainer.innerHTML = `
<div class="search-empty">
<p>No matches found in databases for "${query}"</p>
</div>
`;
searchResultsContainer.style.display = 'block';
return;
}
const header = document.createElement('div');
header.className = 'search-results-header';
header.innerHTML = `
<h3>Database Search Results</h3>
<p>Found ${results.wines.length} wine(s) matching "${query}"</p>
`;
searchResultsContainer.appendChild(header);
const resultsList = document.createElement('div');
resultsList.className = 'search-results-list';
results.wines.slice(0, 10).forEach(wine => {
const wineCard = document.createElement('div');
wineCard.className = 'wine-search-result';
wineCard.innerHTML = `
<div class="wine-result-info">
<h4>${wine.name || 'Unknown Wine'}</h4>
<p class="wine-meta">
${wine.winery ? wine.winery + ' • ' : ''}
${wine.vintage || ''}
${wine.region ? ' • ' + wine.region : ''}
</p>
<p class="wine-source">Source: ${wine.source || 'Unknown'}</p>
</div>
<div class="wine-result-price">
${wine.price ? '<span class="price">$' + wine.price + '</span>' : '<span class="price-na">N/A</span>'}
${wine.rating ? '<span class="rating">⭐ ' + wine.rating + '</span>' : ''}
</div>
`;
resultsList.appendChild(wineCard);
});
searchResultsContainer.appendChild(resultsList);
searchResultsContainer.style.display = 'block';
// Scroll to search results
setTimeout(() => {
searchResultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 500);
}
// Handle authentication
if (!document.cookie.includes('connect.sid')) {
window.location.href = '/login';
}