orbittv2 / index.html
aniketkumar1106's picture
Update index.html
1efca41 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>ORBIT | Visual Commerce</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: { sans: ['Plus Jakarta Sans', 'sans-serif'] },
colors: {
orbit: { yellow: '#FFD600', dark: '#0F172A', green: '#10B981', red: '#EF4444' }
},
animation: {
'spin-slow': 'spin 3s linear infinite',
'float': 'float 6s ease-in-out infinite',
'bag-shake': 'shake 0.5s cubic-bezier(.36,.07,.19,.97) both infinite',
'slide-up': 'slideUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards',
'pulse-mic': 'pulseMic 1.5s infinite',
'pop-in': 'popIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards',
},
keyframes: {
float: { '0%, 100%': { transform: 'translateY(0)' }, '50%': { transform: 'translateY(-10px)' } },
shake: { '10%, 90%': { transform: 'translate3d(-1px, 0, 0)' }, '20%, 80%': { transform: 'translate3d(2px, 0, 0)' }, '30%, 50%, 70%': { transform: 'translate3d(-4px, 0, 0)' }, '40%, 60%': { transform: 'translate3d(4px, 0, 0)' } },
slideUp: { '0%': { opacity: 0, transform: 'translateY(20px)' }, '100%': { opacity: 1, transform: 'translateY(0)' } },
popIn: { '0%': { opacity: 0, transform: 'scale(0.95)' }, '100%': { opacity: 1, transform: 'scale(1)' } },
pulseMic: { '0%': { boxShadow: '0 0 0 0 rgba(239, 68, 68, 0.7)' }, '70%': { boxShadow: '0 0 0 10px rgba(239, 68, 68, 0)' } }
}
}
}
}
</script>
<style>
body { background-color: #F8FAFC; color: #0F172A; -webkit-tap-highlight-color: transparent; }
::-webkit-scrollbar { width: 0px; }
.glass { background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(12px); border-bottom: 1px solid rgba(0,0,0,0.05); }
.paper-bag {
width: 80px; height: 100px; background: #8D6E63; border-radius: 6px; position: relative; margin: 0 auto;
background: linear-gradient(135deg, #A1887F 0%, #6D4C41 100%);
box-shadow: 0 15px 35px -5px rgba(109, 76, 65, 0.3);
}
.paper-bag::before { content: ''; position: absolute; top: -15px; left: 20px; width: 40px; height: 30px; border: 4px solid #5D4037; border-bottom: none; border-radius: 20px 20px 0 0; z-index: -1; }
.result-card:hover .result-img { transform: scale(1.08); }
.result-card:active { transform: scale(0.98); }
.modal-backdrop { backdrop-filter: blur(8px); background-color: rgba(15, 23, 42, 0.4); }
.viewer-backdrop { backdrop-filter: blur(15px); background-color: rgba(0, 0, 0, 0.85); }
input[type=range] { -webkit-appearance: none; background: transparent; }
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none; height: 20px; width: 20px; border-radius: 50%;
background: #FFD600; border: 3px solid white; box-shadow: 0 2px 6px rgba(0,0,0,0.2);
margin-top: -8px; transition: transform 0.1s;
}
input[type=range]::-webkit-slider-runnable-track { width: 100%; height: 4px; background: #E2E8F0; border-radius: 2px; }
</style>
</head>
<body class="min-h-screen relative overflow-x-hidden text-slate-800">
<div id="loader" class="fixed inset-0 z-[120] bg-white flex flex-col items-center justify-center transition-opacity duration-500">
<div class="relative w-24 h-24 mb-8">
<div class="absolute inset-0 border-4 border-gray-100 rounded-full"></div>
<div class="absolute inset-0 border-[4px] border-orbit-yellow rounded-full border-t-transparent border-l-transparent animate-spin"></div>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-12 h-12 bg-orbit-dark rounded-full flex items-center justify-center animate-pulse shadow-xl">
<i class="fa-solid fa-bolt text-orbit-yellow text-lg"></i>
</div>
</div>
<h2 class="text-slate-900 font-extrabold tracking-[0.2em] text-3xl">ORBIT</h2>
</div>
<div id="tutorialOverlay" class="fixed inset-0 z-[110] bg-white flex flex-col justify-between p-8 transition-all duration-700 ease-out translate-y-full opacity-0">
<div class="flex-1 flex flex-col items-center justify-center text-center space-y-8">
<div class="relative w-40 h-40 bg-orbit-yellow/10 rounded-full flex items-center justify-center animate-float">
<div class="paper-bag scale-110"></div>
</div>
<div>
<h1 class="text-4xl font-black text-slate-900 mb-2">Visual Search</h1>
<p class="text-slate-500 text-lg leading-relaxed">Discovery made simple.<br>Scan, find, repeat.</p>
</div>
</div>
<button onclick="finishTutorial()" class="w-full bg-orbit-dark text-white font-bold h-16 rounded-2xl text-lg shadow-xl active:scale-95 transition-all">Start Exploring</button>
</div>
<div id="imageViewerModal" class="fixed inset-0 z-[150] hidden flex flex-col items-center justify-center p-4 viewer-backdrop transition-opacity duration-300 opacity-0">
<button onclick="closeImageViewer()" class="absolute top-6 right-6 text-white/50 hover:text-white transition-colors p-2 z-50"><i class="fa-solid fa-xmark text-3xl"></i></button>
<div class="max-w-2xl w-full flex flex-col gap-4 animate-pop-in relative">
<div class="relative rounded-2xl overflow-hidden bg-black aspect-square shadow-2xl border border-white/10">
<img id="viewerImage" src="" class="w-full h-full object-contain">
</div>
<div class="bg-zinc-900 rounded-xl p-4 border border-white/10 flex items-center gap-3">
<div class="flex-1 min-w-0">
<p class="text-[10px] text-zinc-500 font-bold uppercase tracking-widest mb-1">Product Identity</p>
<p id="viewerFilename" class="text-white font-mono text-sm truncate select-all">LOADING...</p>
</div>
<button onclick="copyViewerText()" class="px-5 py-3 rounded-lg bg-orbit-yellow text-slate-900 font-bold text-sm">COPY</button>
</div>
</div>
</div>
<div id="bestMatchModal" class="fixed inset-0 z-[90] hidden flex items-center justify-center p-6 modal-backdrop opacity-0 transition-opacity duration-300">
<div class="bg-white rounded-[2rem] p-6 w-full max-w-sm shadow-2xl scale-90 transition-transform duration-300" id="modalCard">
<div class="text-center mb-4"><div class="inline-block bg-orbit-yellow text-slate-900 px-4 py-1.5 rounded-full text-xs font-black tracking-widest uppercase shadow-md">🏆 Match Alert</div></div>
<div class="relative aspect-square rounded-2xl overflow-hidden mb-5 bg-gray-50">
<img id="bestMatchImg" src="" class="w-full h-full object-cover">
<div class="absolute bottom-3 left-3 bg-white/90 backdrop-blur px-3 py-1.5 rounded-xl">
<span id="bestMatchScore" class="text-orbit-green font-black text-lg">0%</span>
</div>
</div>
<h3 id="bestMatchName" class="text-xl font-bold text-slate-900 font-mono truncate mb-6 text-center">ID-00000</h3>
<button onclick="closeModal()" class="w-full py-4 rounded-xl font-bold text-slate-900 bg-gray-100">Dismiss</button>
</div>
</div>
<div id="app" class="relative max-w-md mx-auto min-h-screen pb-32 opacity-0 transition-opacity duration-1000">
<header class="sticky top-0 z-50 pt-6 pb-4 px-6 glass rounded-b-3xl">
<h1 class="text-4xl font-black text-slate-900 tracking-tighter italic mb-6">ORBIT<span class="text-orbit-yellow">.</span></h1>
<div class="relative bg-white rounded-2xl flex items-center p-1 shadow-sm border border-slate-100">
<div class="w-10 h-10 flex items-center justify-center text-gray-400"><i class="fa-solid fa-magnifying-glass text-sm"></i></div>
<input type="text" id="textInput" placeholder="Describe your search..." class="flex-1 bg-transparent border-none outline-none text-slate-900 font-bold h-12 text-sm">
<button onclick="fullSystemReset()" class="w-8 h-8 rounded-lg bg-gray-50 text-gray-300 hover:text-orbit-red transition-all mr-1"><i class="fa-solid fa-circle-xmark"></i></button>
<button id="micBtn" onclick="startVoice()" class="w-8 h-8 rounded-lg bg-gray-50 text-slate-400 flex items-center justify-center mr-1"><i class="fa-solid fa-microphone text-xs"></i></button>
<button onclick="openUpload()" class="w-8 h-8 rounded-lg bg-gray-50 text-gray-400 flex items-center justify-center mr-1"><i class="fa-regular fa-image text-xs"></i></button>
</div>
</header>
<main class="px-6 mt-6 space-y-8">
<div class="relative w-full aspect-[4/5] bg-white rounded-[2.5rem] shadow-sm overflow-hidden group border border-slate-50">
<video id="webcam" class="absolute inset-0 w-full h-full object-cover hidden" autoplay playsinline></video>
<img id="previewImg" class="absolute inset-0 w-full h-full object-contain bg-gray-50 hidden">
<button id="resetBtn" onclick="resetState()" class="absolute top-4 right-4 z-30 w-10 h-10 bg-white/90 backdrop-blur rounded-full text-orbit-red shadow-lg hidden items-center justify-center"><i class="fa-solid fa-xmark"></i></button>
<div id="camPlaceholder" class="absolute inset-0 flex flex-col items-center justify-center">
<div id="bagContainer" class="mb-8 animate-float"><div class="paper-bag"></div></div>
<button onclick="toggleCam()" class="bg-orbit-dark text-white px-8 py-4 rounded-full font-bold shadow-xl flex items-center gap-3"><i class="fa-solid fa-camera"></i><span>Scan Object</span></button>
<input type="file" id="fileInput" class="hidden" accept="image/*">
</div>
<button id="captureBtn" onclick="capture()" class="absolute bottom-8 left-1/2 -translate-x-1/2 w-20 h-20 bg-white rounded-full shadow-2xl hidden items-center justify-center"><div class="w-16 h-16 border-[5px] border-orbit-yellow rounded-full"></div></button>
</div>
<div id="hybridSection" class="bg-white rounded-2xl p-4 shadow-sm border border-slate-100 hidden animate-slide-up">
<div class="flex justify-between items-center mb-2">
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Text</span>
<span id="sliderLabel" class="text-[10px] font-black text-orbit-yellow uppercase bg-orbit-dark px-3 py-1 rounded">Balanced</span>
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Visual</span>
</div>
<input type="range" id="weightSlider" min="0" max="1" step="0.1" value="0.5" oninput="updateSliderLabel(this.value)" class="w-full">
</div>
<div id="resultsSection" class="pb-10">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-black text-slate-900">Results</h2>
<span id="resultCount" class="text-[10px] font-bold text-slate-400 bg-gray-100 px-3 py-1 rounded-full uppercase hidden">0 Found</span>
</div>
<div id="emptyState" class="py-12 flex flex-col items-center justify-center opacity-40">
<i class="fa-solid fa-layer-group text-4xl text-gray-300 mb-2"></i>
<p class="text-xs font-bold text-slate-300 uppercase">Standby</p>
</div>
<div id="resultsArea" class="grid grid-cols-2 gap-4"></div>
</div>
</main>
<div class="fixed bottom-8 left-6 right-6 z-40">
<button onclick="search()" class="w-full h-16 bg-orbit-yellow text-slate-900 rounded-2xl font-black text-xl shadow-xl flex items-center justify-center gap-3"><span>Find It</span><i class="fa-solid fa-bolt"></i></button>
</div>
</div>
<script>
let currentFile = null;
let videoStream = null;
function normalizeConfidence(raw) {
if (raw > 100) return Math.max(0, Math.min(100, Math.round(100 - (raw / 100))));
if (raw <= 1 && raw >= 0) return Math.round(raw * 100);
return Math.round(raw);
}
function fullSystemReset() {
document.getElementById('textInput').value = "";
resetState();
document.getElementById('resultsArea').innerHTML = '';
document.getElementById('emptyState').classList.remove('hidden');
document.getElementById('resultCount').classList.add('hidden');
vibrate([20, 10, 20]);
}
function resetState() {
currentFile = null;
document.getElementById('fileInput').value = "";
document.getElementById('previewImg').classList.add('hidden');
document.getElementById('previewImg').src = "";
document.getElementById('resetBtn').classList.add('hidden');
document.getElementById('hybridSection').classList.add('hidden');
document.getElementById('camPlaceholder').classList.remove('hidden');
stopCam();
}
// FEATURE: MORE LIKE THIS (Uses image URL to fetch blob and re-search)
async function moreLikeThis(imgUrl) {
vibrate(50);
const grid = document.getElementById('resultsArea');
grid.innerHTML = `<div class="col-span-2 py-12 text-center"><p class="animate-pulse font-bold text-orbit-yellow">DEEP SCANNING...</p></div>`;
window.scrollTo({ top: 0, behavior: 'smooth' });
try {
const response = await fetch(imgUrl);
const blob = await response.blob();
const file = new File([blob], "ref.jpg", { type: "image/jpeg" });
// Clear text, set new image, set weight to Pure Visual
document.getElementById('textInput').value = "";
loadFile(file);
document.getElementById('weightSlider').value = 0.9;
updateSliderLabel(0.9);
setTimeout(() => search(), 300);
} catch (e) { console.error("Discovery failed", e); }
}
document.getElementById('textInput').addEventListener('input', (e) => {
if (e.target.value.trim() !== "" && currentFile !== null) resetState();
});
window.onload = () => {
setTimeout(() => {
document.getElementById('loader').style.opacity = '0';
setTimeout(() => {
document.getElementById('loader').style.display = 'none';
document.getElementById('tutorialOverlay').classList.remove('translate-y-full', 'opacity-0');
}, 500);
}, 1000);
};
function finishTutorial() {
document.getElementById('tutorialOverlay').classList.add('opacity-0', 'translate-y-full');
setTimeout(() => { document.getElementById('tutorialOverlay').style.display = 'none'; }, 700);
document.getElementById('app').style.opacity = '1';
}
async function search() {
const textValue = document.getElementById('textInput').value.trim();
const grid = document.getElementById('resultsArea');
const empty = document.getElementById('emptyState');
const weight = document.getElementById('weightSlider').value;
if (!textValue && !currentFile) { vibrate([50, 50]); return; }
empty.classList.add('hidden');
grid.innerHTML = `<div class="col-span-2 py-12 text-center"><div class="paper-bag animate-bag-shake mb-4"></div><p class="font-bold text-slate-300 text-xs uppercase tracking-widest">SigLIP 2 Analysing...</p></div>`;
const formData = new FormData();
if (textValue) formData.append('text', textValue);
if (currentFile) formData.append('file', currentFile);
formData.append('weight', weight);
try {
const req = await fetch("https://aniketkumar1106-orbittv2.hf.space", { method: 'POST', body: formData });
const res = await req.json();
grid.innerHTML = '';
if (!res.results || res.results.length === 0) { empty.classList.remove('hidden'); return; }
document.getElementById('resultCount').innerText = `${res.results.length} FOUND`;
document.getElementById('resultCount').classList.remove('hidden');
res.results.forEach((item, index) => {
const confidence = normalizeConfidence(item.score);
const card = document.createElement('div');
card.className = "result-card bg-white rounded-2xl overflow-hidden shadow-sm animate-slide-up border border-slate-50 relative group";
let badge = confidence > 75 ? "bg-orbit-green text-white" : "bg-white text-slate-400 shadow-sm";
card.innerHTML = `
<div class="aspect-square relative bg-gray-50 overflow-hidden">
<img src="${item.url}" class="result-img w-full h-full object-cover mix-blend-multiply transition-all duration-500" onclick="openImageViewer('${item.url}', '${item.id}')">
<div class="absolute top-2 left-2 ${badge} px-2 py-1 rounded-lg text-[9px] font-black z-10">${confidence}%</div>
<button onclick="moreLikeThis('${item.url}')" class="absolute bottom-2 right-2 w-8 h-8 bg-white/90 backdrop-blur rounded-full text-orbit-dark shadow-lg opacity-0 group-hover:opacity-100 transition-all transform translate-y-2 group-hover:translate-y-0 flex items-center justify-center hover:bg-orbit-yellow">
<i class="fa-solid fa-wand-magic-sparkles text-[10px]"></i>
</button>
</div>
<div class="p-3">
<div class="flex justify-between items-center gap-1">
<p class="text-[9px] text-slate-400 font-mono truncate font-bold flex-1 uppercase tracking-tighter">${item.id}</p>
<button onclick="navigator.clipboard.writeText('${item.id}')" class="text-slate-300 hover:text-orbit-yellow transition-colors"><i class="fa-regular fa-copy text-[10px]"></i></button>
</div>
</div>`;
grid.appendChild(card);
});
const best = res.results[0];
const score = normalizeConfidence(best.score);
if (score >= 70) {
setTimeout(() => {
document.getElementById('bestMatchImg').src = best.url;
document.getElementById('bestMatchName').innerText = best.id;
document.getElementById('bestMatchScore').innerText = score + "%";
document.getElementById('bestMatchModal').classList.remove('hidden');
setTimeout(() => {
document.getElementById('bestMatchModal').classList.remove('opacity-0');
confetti({ particleCount: 150, spread: 70, origin: { y: 0.6 }, colors: ['#FFD600', '#0F172A', '#10B981'] });
}, 10);
}, 500);
}
} catch (e) { grid.innerHTML = `<p class="col-span-2 text-center text-red-500 font-bold">Offline</p>`; }
}
function startVoice() {
const Speech = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!Speech) return;
const mic = document.getElementById('micBtn'); mic.classList.add('animate-pulse-mic', 'text-orbit-red');
const rec = new Speech(); rec.start();
rec.onresult = (e) => {
document.getElementById('textInput').value = e.results[0][0].transcript;
mic.classList.remove('animate-pulse-mic', 'text-orbit-red');
search();
};
rec.onerror = () => mic.classList.remove('animate-pulse-mic', 'text-orbit-red');
}
function toggleCam() {
if (videoStream) { stopCam(); return; }
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }).then(s => {
videoStream = s; const v = document.getElementById('webcam'); v.srcObject = s; v.classList.remove('hidden');
document.getElementById('camPlaceholder').classList.add('hidden');
document.getElementById('captureBtn').classList.remove('hidden');
document.getElementById('resetBtn').classList.remove('hidden');
});
}
function stopCam() {
if (videoStream) videoStream.getTracks().forEach(t => t.stop()); videoStream = null;
document.getElementById('webcam').classList.add('hidden');
document.getElementById('captureBtn').classList.add('hidden');
}
function capture() {
const v = document.getElementById('webcam'); const c = document.createElement('canvas');
c.width = v.videoWidth; c.height = v.videoHeight;
c.getContext('2d').drawImage(v, 0, 0);
c.toBlob(b => { loadFile(new File([b], "scan.jpg", {type:"image/jpeg"})); stopCam(); });
}
function loadFile(file) {
currentFile = file; const r = new FileReader();
r.onload = (e) => {
document.getElementById('previewImg').src = e.target.result;
document.getElementById('previewImg').classList.remove('hidden');
document.getElementById('camPlaceholder').classList.add('hidden');
document.getElementById('resetBtn').classList.remove('hidden');
document.getElementById('hybridSection').classList.remove('hidden');
}; r.readAsDataURL(file);
}
function openUpload() { document.getElementById('fileInput').click(); }
document.getElementById('fileInput').addEventListener('change', (e) => { if(e.target.files[0]) loadFile(e.target.files[0]); });
function updateSliderLabel(v) { document.getElementById('sliderLabel').innerText = v < 0.4 ? "Text Heavy" : (v > 0.6 ? "Visual Heavy" : "Balanced"); }
function closeModal() { document.getElementById('bestMatchModal').classList.add('opacity-0'); setTimeout(() => document.getElementById('bestMatchModal').classList.add('hidden'), 300); }
function copyViewerText() { navigator.clipboard.writeText(document.getElementById('viewerFilename').innerText).then(() => { vibrate(50); }); }
function closeImageViewer() { document.getElementById('imageViewerModal').classList.add('opacity-0'); setTimeout(() => document.getElementById('imageViewerModal').classList.add('hidden'), 300); }
function openImageViewer(url, id) {
document.getElementById('viewerImage').src = url;
document.getElementById('viewerFilename').innerText = id;
document.getElementById('imageViewerModal').classList.remove('hidden');
setTimeout(() => document.getElementById('imageViewerModal').classList.remove('opacity-0'), 10);
}
function vibrate(p) { if(navigator.vibrate) navigator.vibrate(p); }
</script>
</body>
</html>