← 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';
}