Spaces:
Runtime error
Runtime error
| <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> | |