Spaces:
Sleeping
Sleeping
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>AI Sneaker Category Predictor</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script> | |
tailwind.config = { | |
theme: { | |
extend: { | |
colors: { | |
'neo-black': '#0F1116', | |
'neo-blue': '#2E3BFF' | |
} | |
} | |
} | |
} | |
</script> | |
<style> | |
.glass-effect { | |
background: rgba(255, 255, 255, 0.05); | |
backdrop-filter: blur(10px); | |
border: 1px solid rgba(255, 255, 255, 0.1); | |
transition: all 0.3s ease; | |
} | |
.glass-effect:hover { | |
background: rgba(255, 255, 255, 0.08); | |
} | |
.gradient-border { | |
position: relative; | |
border: double 1px transparent; | |
border-radius: 0.5rem; | |
background-image: linear-gradient(#0F1116, #0F1116), | |
linear-gradient(to right, #2E3BFF, #7C3AED); | |
background-origin: border-box; | |
background-clip: padding-box, border-box; | |
transition: all 0.3s ease; | |
} | |
.gradient-border:hover { | |
background-image: linear-gradient(#0F1116, #0F1116), | |
linear-gradient(to right, #3E4BFF, #8C4AFD); | |
} | |
.custom-select { | |
position: relative; | |
display: inline-block; | |
width: 100%; | |
} | |
.custom-select select { | |
display: none; | |
} | |
.select-selected { | |
background-color: rgba(255, 255, 255, 0.05); | |
padding: 0.5rem 1rem; | |
border-radius: 0.5rem; | |
cursor: pointer; | |
} | |
.select-items { | |
position: absolute; | |
padding: 3px; | |
top: 100%; | |
left: 0; | |
right: 0; | |
z-index: 99; | |
background: rgb(0, 0, 0); | |
backdrop-filter: blur(10px); | |
border-radius: 0.5rem; | |
margin-top: 0.5rem; | |
max-height: 200px; | |
overflow-y: auto; | |
display: none; | |
} | |
.select-items div { | |
padding: 0.5rem 1rem; | |
cursor: pointer; | |
transition: all 0.2s; | |
} | |
.select-items div:hover { | |
background: #534dad96; | |
border-radius: 0.5rem; | |
} | |
.drop-zone { | |
border: 2px dashed rgba(46, 59, 255, 0.3); | |
border-radius: 1rem; | |
padding: 1rem; | |
text-align: center; | |
transition: all 0.3s ease; | |
} | |
.drop-zone.drag-over { | |
border-color: #2E3BFF; | |
background: rgba(46, 59, 255, 0.1); | |
} | |
.pulse { | |
animation: pulse 2s infinite; | |
} | |
@keyframes pulse { | |
0% { | |
transform: scale(1); | |
} | |
50% { | |
transform: scale(1.05); | |
} | |
100% { | |
transform: scale(1); | |
} | |
} | |
/* Custom scrollbar */ | |
.select-items::-webkit-scrollbar { | |
width: 6px; | |
} | |
.select-items::-webkit-scrollbar-track { | |
background: rgba(255, 255, 255, 0.1); | |
border-radius: 3px; | |
} | |
.select-items::-webkit-scrollbar-thumb { | |
background: rgba(46, 59, 255, 0.5); | |
border-radius: 3px; | |
} | |
.select-search { | |
padding: 0.5rem; | |
width: 100%; | |
background: rgba(255, 255, 255, 0.05); | |
border: 1px solid rgba(255, 255, 255, 0.1); | |
border-radius: 0.25rem; | |
color: white; | |
margin-bottom: 0.5rem; | |
} | |
.select-search:focus { | |
outline: none; | |
border-color: rgba(46, 59, 255, 0.5); | |
} | |
.select-option-hidden { | |
display: none; | |
} | |
</style> | |
</head> | |
<body class="bg-neo-black text-gray-100 min-h-screen"> | |
<div class="fixed w-full h-full"> | |
<div class="absolute top-0 left-0 w-96 h-96 bg-blue-500 rounded-full filter blur-[128px] opacity-20"></div> | |
<div class="absolute bottom-0 right-0 w-96 h-96 bg-purple-500 rounded-full filter blur-[128px] opacity-20"></div> | |
</div> | |
<div class="container mx-auto px-4 py-8 w-full relative"> | |
<h1 | |
class="text-5xl font-bold text-center mb-12 bg-clip-text text-transparent bg-gradient-to-r from-blue-500 to-purple-500"> | |
AI Sneaker Predictor | |
</h1> | |
<div class="grid grid-cols-1 md:grid-cols-3 gap-8"> | |
<div class="flex flex-col items-center justify-center w-full col-span-1 md:col-span-2"> | |
<div class="glass-effect p-8 rounded-xl space-y-6 w-full h-fit"> | |
<div class="flex items-start justify-center gap-5 w-full"> | |
<div id="dropZone" class="drop-zone h-full aspect-square w-full flex items-center justify-center"> | |
<div id="imagePreview" class="hidden w-full h-full"> | |
<img id="preview" class="w-full h-full rounded-lg shadow-lg border border-blue-500/20 bg-gradient-to-tr from-blue-500/15 to-purple-500/15" alt="Preview"> | |
</div> | |
<div id="dropText" class="text-blue-300"> | |
<svg class="w-12 h-12 mx-auto mb-4 text-blue-500" fill="none" stroke="currentColor" | |
viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /> | |
</svg> | |
<p class="text-lg">Drag and drop your sneaker image here</p> | |
<p class="text-sm text-blue-400 mt-2">or click to browse</p> | |
<input type="file" id="imageUpload" class="hidden" accept="image/*"> | |
</div> | |
</div> | |
<div class="space-y-6 w-full"> | |
<div class="custom-select"> | |
<label class="block text-sm font-medium text-blue-300 mb-2">Brand</label> | |
<select id="brand" required> | |
{% for brand in metadata.brand %} | |
<option value="{{brand}}">{{brand}}</option> | |
{% endfor %} | |
</select> | |
</div> | |
<div class="custom-select"> | |
<label class="block text-sm font-medium text-blue-300 mb-2">Color</label> | |
<select id="color" required> | |
{% for color in metadata.color %} | |
<option value="{{color}}">{{color}}</option> | |
{% endfor %} | |
</select> | |
</div> | |
<div class="custom-select"> | |
<label class="block text-sm font-medium text-blue-300 mb-2">Gender</label> | |
<select id="gender" required> | |
{% for gender in metadata.gender %} | |
<option value="{{gender}}">{{gender}}</option> | |
{% endfor %} | |
</select> | |
</div> | |
<div class="custom-select"> | |
<label class="block text-sm font-medium text-blue-300 mb-2">Midsole</label> | |
<select id="midsole" required> | |
{% for midsole in metadata.midsole %} | |
<option value="{{midsole}}">{% if midsole == "" %}Null{%else%}{{midsole}}{%endif%}</option> | |
{% endfor %} | |
</select> | |
</div> | |
<div class="custom-select"> | |
<label class="block text-sm font-medium text-blue-300 mb-2">Upper Material</label> | |
<select id="upperMaterial" required> | |
{% for upperMaterial in metadata.upperMaterial %} | |
<option value="{{upperMaterial}}">{% if upperMaterial == "" %}Null{%else%}{{upperMaterial}}{%endif%}</option> | |
{% endfor %} | |
</select> | |
</div> | |
</div> | |
</div> | |
<button | |
class="w-full py-4 px-6 rounded-lg font-medium transition-all duration-300 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 focus:ring-offset-neo-black transform hover:scale-105" | |
onclick="predict()"> | |
<div class="flex items-center justify-center space-x-3"> | |
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | |
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /> | |
</svg> | |
<span class="text-lg">Predict Category</span> | |
</div> | |
</button> | |
</div> | |
<div class="mt-8"> | |
<h3 class="text-xl font-semibold mb-4 text-blue-300">Example Sneakers</h3> | |
<div class="grid grid-cols-3 gap-4"> | |
{% for product in products %} | |
<div class="glass-effect p-4 rounded-xl cursor-pointer hover:scale-105 transition-transform" onclick="loadExample('{{loop.index0}}')"> | |
<img src="{{product.img}}" alt="{{product.name}}" class="w-full rounded-lg mb-2"> | |
<p class="text-sm text-blue-300">{{product.name}} ({{product.category}})</p> | |
</div> | |
{% endfor %} | |
</div> | |
</div> | |
</div> | |
<div> | |
<div id="result" class="hidden glass-effect p-6 rounded-xl"> | |
<h3 class="text-xl font-semibold mb-4 text-blue-300">AI Prediction</h3> | |
<div id="predictions" class="space-y-4"> | |
<!-- Predictions will be inserted here --> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
function initCustomSelects() { | |
document.querySelectorAll('.custom-select select').forEach(select => { | |
const div = document.createElement('div'); | |
div.classList.add('select-selected', 'gradient-border'); | |
div.textContent = select.options[select.selectedIndex].text; | |
select.parentElement.appendChild(div); | |
const itemsDiv = document.createElement('div'); | |
itemsDiv.classList.add('select-items'); | |
// Add search input | |
const searchInput = document.createElement('input'); | |
searchInput.type = 'text'; | |
searchInput.placeholder = 'Search...'; | |
searchInput.classList.add('select-search'); | |
itemsDiv.appendChild(searchInput); | |
const optionsContainer = document.createElement('div'); | |
Array.from(select.options).forEach(option => { | |
const optionDiv = document.createElement('div'); | |
optionDiv.textContent = option.text; | |
optionDiv.addEventListener('click', () => { | |
select.value = option.value; | |
div.textContent = option.text; | |
itemsDiv.style.display = 'none'; | |
}); | |
optionsContainer.appendChild(optionDiv); | |
}); | |
itemsDiv.appendChild(optionsContainer); | |
// Add search functionality | |
searchInput.addEventListener('input', (e) => { | |
const searchText = e.target.value.toLowerCase(); | |
Array.from(optionsContainer.children).forEach(optionDiv => { | |
const text = optionDiv.textContent.toLowerCase(); | |
optionDiv.classList.toggle('select-option-hidden', !text.includes(searchText)); | |
}); | |
}); | |
// Prevent dropdown from closing when clicking search | |
searchInput.addEventListener('click', (e) => { | |
e.stopPropagation(); | |
}); | |
select.parentElement.appendChild(itemsDiv); | |
div.addEventListener('click', (e) => { | |
e.stopPropagation(); | |
closeAllSelect(itemsDiv); | |
itemsDiv.style.display = itemsDiv.style.display === 'block' ? 'none' : 'block'; | |
if (itemsDiv.style.display === 'block') { | |
searchInput.focus(); | |
searchInput.value = ''; | |
// Show all options when opening dropdown | |
Array.from(optionsContainer.children).forEach(optionDiv => { | |
optionDiv.classList.remove('select-option-hidden'); | |
}); | |
} | |
}); | |
}); | |
document.addEventListener('click', () => closeAllSelect(null)); | |
} | |
function closeAllSelect(elmnt) { | |
document.querySelectorAll('.select-items').forEach(item => { | |
if (item !== elmnt) item.style.display = 'none'; | |
}); | |
} | |
const dropZone = document.getElementById('dropZone'); | |
const imageUpload = document.getElementById('imageUpload'); | |
['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, () => dropZone.classList.add('drag-over')); | |
}); | |
['dragleave', 'drop'].forEach(eventName => { | |
dropZone.addEventListener(eventName, () => dropZone.classList.remove('drag-over')); | |
}); | |
dropZone.addEventListener('drop', handleDrop); | |
dropZone.addEventListener('click', () => imageUpload.click()); | |
function handleDrop(e) { | |
const dt = e.dataTransfer; | |
const file = dt.files[0]; | |
handleFile(file); | |
} | |
document.getElementById('imageUpload').addEventListener('change', function (e) { | |
const file = e.target.files[0]; | |
if (file) handleFile(file); | |
}); | |
function handleFile(file) { | |
if (file) { | |
const reader = new FileReader(); | |
reader.onload = function (e) { | |
document.getElementById('preview').src = e.target.result; | |
document.getElementById('imagePreview').classList.remove('hidden'); | |
document.getElementById('dropText').classList.add('hidden'); | |
} | |
reader.readAsDataURL(file); | |
} | |
} | |
document.addEventListener('DOMContentLoaded', () => { | |
initCustomSelects(); | |
// Add examples data | |
window.examples = `{{ productsString | safe }}`; | |
window.examples = JSON.parse(window.examples); | |
}); | |
async function loadExample(index) { | |
index = parseInt(index); | |
const example = window.examples[index]; | |
// Set form values | |
document.getElementById('brand').value = example.brand; | |
document.getElementById('color').value = example.color; | |
document.getElementById('gender').value = example.gender; | |
document.getElementById('midsole').value = example.midsole; | |
document.getElementById('upperMaterial').value = example.upperMaterial; | |
// Update custom select displays | |
document.querySelectorAll('.select-selected').forEach(div => { | |
const select = div.previousElementSibling; | |
div.textContent = select.options[select.selectedIndex].text; | |
}); | |
// Load and display image | |
const response = await fetch(example.img); | |
const blob = await response.blob(); | |
const file = new File([blob], 'example.png', { type: 'image/png' }); | |
// Trigger file handler | |
handleFile(file); | |
// Update the hidden file input | |
const dataTransfer = new DataTransfer(); | |
dataTransfer.items.add(file); | |
document.getElementById('imageUpload').files = dataTransfer.files; | |
} | |
async function predict() { | |
const imageFile = document.getElementById('imageUpload').files[0]; | |
if (!imageFile) { | |
alert('Please select an image'); | |
return; | |
} | |
const reader = new FileReader(); | |
reader.onload = async function (e) { | |
const base64Image = e.target.result.split(',')[1]; | |
const data = { | |
image: base64Image, | |
brand: document.getElementById('brand').value, | |
color: document.getElementById('color').value, | |
gender: document.getElementById('gender').value, | |
midsole: document.getElementById('midsole').value, | |
upperMaterial: document.getElementById('upperMaterial').value | |
}; | |
try { | |
const response = await fetch('/predict', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify(data) | |
}); | |
const result = await response.json(); | |
if (result.error) { | |
alert('Error: ' + result.error); | |
} else { | |
document.getElementById('result').classList.remove('hidden'); | |
const predictionsContainer = document.getElementById('predictions'); | |
predictionsContainer.innerHTML = ''; | |
// Sort categories by confidence | |
const predictions = result.categories.map((category, index) => ({ | |
category, | |
confidence: result.confidence[index] | |
})).sort((a, b) => b.confidence - a.confidence); | |
predictions.forEach(({ category, confidence }) => { | |
const confidencePercent = (confidence * 100).toFixed(2); | |
const predictionHtml = ` | |
<div class="gradient-border p-4"> | |
<div class="flex items-center justify-between"> | |
<p>Category: <span class="font-semibold text-blue-400">${category}</span></p> | |
<p>Confidence: <span class="font-semibold text-blue-400">${confidencePercent}</span>%</p> | |
</div> | |
<div class="w-full bg-gray-700/30 rounded-full h-4 mt-2"> | |
<div class="bg-gradient-to-r from-blue-500 to-purple-500 h-4 rounded-full transition-all duration-500" | |
style="width: ${confidencePercent}%"></div> | |
</div> | |
</div>`; | |
predictionsContainer.innerHTML += predictionHtml; | |
}); | |
} | |
} catch (error) { | |
alert('Error: ' + error.message); | |
} | |
}; | |
reader.readAsDataURL(imageFile); | |
} | |
</script> | |
</body> | |
</html> |