| | <!DOCTYPE html> |
| | <html lang="en" class="dark"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>ImageMatch | Visual Search</title> |
| | <link rel="icon" type="image/x-icon" href="/static/favicon.ico"> |
| | <script src="https://cdn.tailwindcss.com"></script> |
| | <script> |
| | tailwind.config = { |
| | darkMode: 'class', |
| | theme: { |
| | extend: { |
| | colors: { |
| | primary: { |
| | 500: '#3b82f6', |
| | }, |
| | secondary: { |
| | 500: '#6366f1', |
| | } |
| | } |
| | } |
| | } |
| | } |
| | </script> |
| | <script src="https://unpkg.com/feather-icons"></script> |
| | <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> |
| | <style> |
| | .dropzone { |
| | transition: all 0.3s ease; |
| | } |
| | .dropzone-active { |
| | border-color: #3b82f6; |
| | background-color: rgba(59, 130, 246, 0.05); |
| | } |
| | .result-grid { |
| | display: grid; |
| | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); |
| | gap: 1.5rem; |
| | } |
| | @media (max-width: 640px) { |
| | .result-grid { |
| | grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); |
| | } |
| | } |
| | .image-card { |
| | transition: transform 0.2s ease, box-shadow 0.2s ease; |
| | } |
| | .image-card:hover { |
| | transform: translateY(-4px); |
| | box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2); |
| | } |
| | </style> |
| | </head> |
| | <body class="bg-gray-900 text-gray-100 min-h-screen"> |
| | <div class="container mx-auto px-4 py-8"> |
| | <header class="mb-12 text-center"> |
| | <h1 class="text-4xl font-bold mb-2 bg-gradient-to-r from-primary-500 to-secondary-500 bg-clip-text text-transparent">ImageMatch</h1> |
| | <p class="text-gray-400 max-w-2xl mx-auto">Upload an image to find visually similar matches from our collection</p> |
| | </header> |
| |
|
| | <main class="max-w-4xl mx-auto"> |
| | <div class="mb-10"> |
| | <div |
| | id="dropzone" |
| | class="dropzone border-2 border-dashed border-gray-700 rounded-xl p-8 text-center cursor-pointer hover:border-gray-600 transition-colors" |
| | > |
| | <div class="flex flex-col items-center justify-center"> |
| | <i data-feather="upload" class="w-12 h-12 text-primary-500 mb-4"></i> |
| | <h2 class="text-xl font-semibold mb-2">Drag & drop your image here</h2> |
| | <p class="text-gray-400 mb-4">or click to browse files</p> |
| | <input type="file" id="fileInput" class="hidden" accept="image/*"> |
| | <button id="browseBtn" class="px-4 py-2 bg-primary-500 hover:bg-primary-600 rounded-md transition-colors"> |
| | Select Image |
| | </button> |
| | </div> |
| | </div> |
| | <div id="previewContainer" class="mt-4 flex justify-center hidden"> |
| | <div class="relative inline-block"> |
| | <img id="previewImage" src="" alt="Preview" class="max-h-48 rounded-md shadow-lg"> |
| | <button id="removeImageBtn" class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600 transition-colors"> |
| | <i data-feather="x" class="w-4 h-4"></i> |
| | </button> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | <div class="bg-gray-800 rounded-xl p-6 mb-10"> |
| | <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6"> |
| | <h3 class="text-xl font-semibold">Search Settings</h3> |
| | <div class="flex items-center space-x-4"> |
| | <div> |
| | <label for="resultCount" class="block text-sm font-medium text-gray-300 mb-1">Number of Results</label> |
| | <div class="flex items-center space-x-2"> |
| | <input |
| | type="range" |
| | id="resultCount" |
| | min="3" |
| | max="10" |
| | value="5" |
| | class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" |
| | > |
| | <span id="countValue" class="w-8 text-center">5</span> |
| | </div> |
| | </div> |
| | <button |
| | id="searchBtn" |
| | class="px-4 py-2 bg-secondary-500 hover:bg-secondary-600 rounded-md transition-colors flex items-center space-x-2" |
| | disabled |
| | > |
| | <i data-feather="search" class="w-4 h-4"></i> |
| | <span>Find Matches</span> |
| | </button> |
| | </div> |
| | </div> |
| |
|
| | <div class="flex items-center space-x-2 mb-4"> |
| | <i data-feather="info" class="w-4 h-4 text-blue-400"></i> |
| | <p class="text-sm text-gray-400">For best results, use clear, high-quality images without excessive text.</p> |
| | </div> |
| | </div> |
| |
|
| | <div id="resultsSection" class="hidden"> |
| | <div class="flex justify-between items-center mb-6"> |
| | <h3 class="text-2xl font-semibold">Similar Images</h3> |
| | <div class="flex items-center space-x-2"> |
| | <span class="text-sm text-gray-400">Sort by:</span> |
| | <select class="bg-gray-800 border border-gray-700 text-gray-300 rounded-md px-3 py-1 text-sm"> |
| | <option>Most Similar</option> |
| | <option>Color</option> |
| | <option>Size</option> |
| | </select> |
| | </div> |
| | </div> |
| |
|
| | <div class="bg-gray-800 rounded-xl p-6"> |
| | <div id="loadingIndicator" class="hidden flex justify-center items-center py-12"> |
| | <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary-500"></div> |
| | </div> |
| |
|
| | <div id="resultsContainer" class="result-grid"> |
| | |
| | </div> |
| |
|
| | <div id="noResults" class="hidden text-center py-12"> |
| | <i data-feather="frown" class="w-12 h-12 mx-auto text-gray-500 mb-4"></i> |
| | <h4 class="text-xl font-medium text-gray-400 mb-2">No matches found</h4> |
| | <p class="text-gray-500">Try uploading a different image or adjusting your search parameters.</p> |
| | </div> |
| | </div> |
| | </div> |
| | </main> |
| |
|
| | <footer class="mt-20 text-center text-gray-500 text-sm"> |
| | <p>© 2023 ImageMatch. All rights reserved.</p> |
| | </footer> |
| | </div> |
| |
|
| | <script> |
| | feather.replace(); |
| | |
| | |
| | const dropzone = document.getElementById('dropzone'); |
| | const fileInput = document.getElementById('fileInput'); |
| | const browseBtn = document.getElementById('browseBtn'); |
| | const previewContainer = document.getElementById('previewContainer'); |
| | const previewImage = document.getElementById('previewImage'); |
| | const removeImageBtn = document.getElementById('removeImageBtn'); |
| | const searchBtn = document.getElementById('searchBtn'); |
| | const resultCount = document.getElementById('resultCount'); |
| | const countValue = document.getElementById('countValue'); |
| | const resultsSection = document.getElementById('resultsSection'); |
| | const loadingIndicator = document.getElementById('loadingIndicator'); |
| | const resultsContainer = document.getElementById('resultsContainer'); |
| | const noResults = document.getElementById('noResults'); |
| | |
| | |
| | resultCount.addEventListener('input', () => { |
| | countValue.textContent = resultCount.value; |
| | }); |
| | |
| | |
| | browseBtn.addEventListener('click', () => { |
| | fileInput.click(); |
| | }); |
| | |
| | |
| | fileInput.addEventListener('change', (e) => { |
| | handleFileSelection(e.target.files[0]); |
| | }); |
| | |
| | |
| | ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
| | dropzone.addEventListener(eventName, preventDefaults, false); |
| | }); |
| | |
| | function preventDefaults(e) { |
| | e.preventDefault(); |
| | e.stopPropagation(); |
| | } |
| | |
| | ['dragenter', 'dragover'].forEach(eventName => { |
| | dropzone.addEventListener(eventName, highlight, false); |
| | }); |
| | |
| | ['dragleave', 'drop'].forEach(eventName => { |
| | dropzone.addEventListener(eventName, unhighlight, false); |
| | }); |
| | |
| | function highlight() { |
| | dropzone.classList.add('dropzone-active'); |
| | } |
| | |
| | function unhighlight() { |
| | dropzone.classList.remove('dropzone-active'); |
| | } |
| | |
| | dropzone.addEventListener('drop', (e) => { |
| | const dt = e.dataTransfer; |
| | const file = dt.files[0]; |
| | handleFileSelection(file); |
| | }); |
| | |
| | |
| | function handleFileSelection(file) { |
| | if (!file.type.match('image.*')) { |
| | alert('Please select an image file'); |
| | return; |
| | } |
| | |
| | const reader = new FileReader(); |
| | |
| | reader.onload = (e) => { |
| | previewImage.src = e.target.result; |
| | previewContainer.classList.remove('hidden'); |
| | searchBtn.disabled = false; |
| | }; |
| | |
| | reader.readAsDataURL(file); |
| | } |
| | |
| | |
| | removeImageBtn.addEventListener('click', (e) => { |
| | e.stopPropagation(); |
| | previewImage.src = ''; |
| | previewContainer.classList.add('hidden'); |
| | fileInput.value = ''; |
| | searchBtn.disabled = true; |
| | resultsSection.classList.add('hidden'); |
| | }); |
| | |
| | |
| | searchBtn.addEventListener('click', () => { |
| | |
| | resultsSection.classList.remove('hidden'); |
| | loadingIndicator.classList.remove('hidden'); |
| | resultsContainer.innerHTML = ''; |
| | noResults.classList.add('hidden'); |
| | |
| | |
| | setTimeout(() => { |
| | loadingIndicator.classList.add('hidden'); |
| | |
| | |
| | const mockResults = Array(parseInt(resultCount.value)).fill().map((_, i) => ({ |
| | id: i, |
| | similarity: (90 - (i * 5)) + '%', |
| | url: `http://static.photos/technology/320x240/${100 + i}` |
| | })); |
| | |
| | if (mockResults.length > 0) { |
| | resultsContainer.innerHTML = mockResults.map(result => ` |
| | <div class="image-card bg-gray-700 rounded-lg overflow-hidden group"> |
| | <div class="relative pt-[100%]"> |
| | <img src="${result.url}" alt="Result ${result.id}" class="absolute top-0 left-0 w-full h-full object-cover"> |
| | <div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3 opacity-0 group-hover:opacity-100 transition-opacity"> |
| | <div class="flex justify-between items-end"> |
| | <span class="text-sm font-medium">Similarity: ${result.similarity}</span> |
| | <button class="text-white hover:text-primary-500 transition-colors"> |
| | <i data-feather="download" class="w-4 h-4"></i> |
| | </button> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | `).join(''); |
| | |
| | feather.replace(); |
| | } else { |
| | noResults.classList.remove('hidden'); |
| | } |
| | }, 1500); |
| | }); |
| | </script> |
| | </body> |
| | </html> |
| |
|