Antigravity AI
Fix hardcoded localhost URLs to use relative paths for Hugging Face deployment
3f5ff7f | <html lang="ar" dir="rtl"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Dark Real Estate - Studio</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700;800;900&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| fontFamily: { sans: ['Cairo', 'sans-serif'] }, | |
| colors: { | |
| dark: { bg: '#0B0F19', surface: '#111827', accent: '#F59E0B' } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| body { background-color: #0B0F19; color: #FFFFFF; } | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { width: 6px; } | |
| ::-webkit-scrollbar-track { background: #0B0F19; } | |
| ::-webkit-scrollbar-thumb { background: #374151; border-radius: 10px; } | |
| ::-webkit-scrollbar-thumb:hover { background: #F59E0B; } | |
| .upload-zone { | |
| border: 2px dashed rgba(245, 158, 11, 0.4); | |
| transition: all 0.3s; | |
| background: rgba(0, 0, 0, 0.2); | |
| } | |
| .upload-zone:hover { | |
| border-color: #F59E0B; | |
| background: rgba(245, 158, 11, 0.05); | |
| } | |
| /* Slider */ | |
| .slider-container { | |
| position: relative; | |
| width: 100%; | |
| height: 60vh; | |
| overflow: hidden; | |
| border-radius: 1rem; | |
| user-select: none; | |
| touch-action: none; | |
| box-shadow: 0 10px 40px rgba(0,0,0,0.8); | |
| } | |
| .slider-img { | |
| position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; | |
| } | |
| .slider-after-container { | |
| position: absolute; top: 0; left: 0; width: 50%; height: 100%; overflow: hidden; | |
| border-right: 3px solid #F59E0B; z-index: 10; | |
| } | |
| .slider-handle { | |
| position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); | |
| width: 40px; height: 40px; background: #F59E0B; border-radius: 50%; | |
| display: flex; align-items: center; justify-content: center; | |
| cursor: ew-resize; z-index: 20; color: #000; box-shadow: 0 0 15px rgba(0,0,0,0.5); | |
| } | |
| /* Scanning effect */ | |
| .scan-line { | |
| position: absolute; top: 0; left: 0; width: 100%; height: 5px; | |
| background: #F59E0B; box-shadow: 0 0 15px #F59E0B, 0 0 30px #FCD34D; | |
| z-index: 50; animation: scanAnim 3s ease-in-out infinite; opacity: 0.8; | |
| } | |
| @keyframes scanAnim { | |
| 0% { top: 0; } | |
| 50% { top: 100%; } | |
| 100% { top: 0; } | |
| } | |
| .scan-overlay { | |
| position: absolute; inset: 0; background: rgba(245, 158, 11, 0.1); z-index: 40; | |
| } | |
| .btn-glow:hover { box-shadow: 0 0 20px rgba(245, 158, 11, 0.4); } | |
| </style> | |
| </head> | |
| <body class="h-screen flex flex-col overflow-hidden"> | |
| <!-- Navbar --> | |
| <nav class="flex items-center px-6 py-4 border-b border-white/10 bg-dark-bg z-50"> | |
| <div class="flex items-center gap-3 cursor-pointer" onclick="window.location.href='index.html'"> | |
| <img src="logo.png" alt="Dark Logo" class="h-8 object-contain"> | |
| </div> | |
| <div class="mr-auto text-sm font-bold text-gray-400 bg-dark-surface px-4 py-2 rounded-full border border-white/5"> | |
| <i class="fa-solid fa-layer-group text-dark-accent mr-1"></i> استوديو التصميم | |
| </div> | |
| </nav> | |
| <!-- Split Screen Main --> | |
| <main class="flex-1 flex overflow-hidden"> | |
| <!-- RIGHT PANEL (Controls - Arabic RTL so it's on the right side visually) --> | |
| <aside class="w-full md:w-80 lg:w-96 bg-dark-surface border-l border-white/10 flex flex-col z-20 shadow-2xl"> | |
| <div class="flex-1 overflow-y-auto p-6 space-y-8"> | |
| <!-- Upload --> | |
| <div> | |
| <h3 class="text-lg font-bold mb-3"><i class="fa-solid fa-image text-dark-accent ml-2"></i>صورة الغرفة</h3> | |
| <div id="dropzone" class="upload-zone rounded-xl h-40 flex flex-col items-center justify-center cursor-pointer relative overflow-hidden group"> | |
| <input type="file" id="file-input" class="hidden" accept="image/png, image/jpeg, image/jpg"> | |
| <i class="fa-solid fa-cloud-arrow-up text-3xl text-gray-500 mb-2 group-hover:text-dark-accent transition"></i> | |
| <p class="font-bold text-sm">اسحب الصورة أو اضغط هنا</p> | |
| <!-- Mini Preview overlay --> | |
| <div id="image-preview" class="absolute inset-0 bg-black hidden flex-col items-center justify-center group"> | |
| <img id="preview-img" src="" class="w-full h-full object-cover opacity-50"> | |
| <button id="remove-img-btn" class="absolute bg-red-500 text-white w-8 h-8 rounded-full z-10 hover:scale-110 transition"><i class="fa-solid fa-xmark"></i></button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Style Selection --> | |
| <div> | |
| <h3 class="text-lg font-bold mb-3"><i class="fa-solid fa-palette text-dark-accent ml-2"></i>النمط المطلوب</h3> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <button class="style-btn relative overflow-hidden bg-black border-2 border-dark-accent h-24 rounded-lg font-bold transition flex flex-col items-center justify-center gap-1 group"> | |
| <div class="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1600210492486-724fe5c67fb0?w=200')] bg-cover opacity-30"></div> | |
| <span class="relative z-10 text-dark-accent text-sm shadow-black drop-shadow-md">مودرن</span> | |
| </button> | |
| <button class="style-btn relative overflow-hidden bg-black border-2 border-transparent hover:border-white/20 h-24 rounded-lg font-bold transition flex flex-col items-center justify-center gap-1 group text-gray-400"> | |
| <div class="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1540932239986-30128078f3c5?w=200')] bg-cover opacity-20"></div> | |
| <span class="relative z-10 text-sm shadow-black drop-shadow-md">كلاسيك</span> | |
| </button> | |
| <button class="style-btn relative overflow-hidden bg-black border-2 border-transparent hover:border-white/20 h-24 rounded-lg font-bold transition flex flex-col items-center justify-center gap-1 group text-gray-400"> | |
| <div class="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?w=200')] bg-cover opacity-20"></div> | |
| <span class="relative z-10 text-sm shadow-black drop-shadow-md">بوهيمي</span> | |
| </button> | |
| <button class="style-btn relative overflow-hidden bg-black border-2 border-transparent hover:border-white/20 h-24 rounded-lg font-bold transition flex flex-col items-center justify-center gap-1 group text-gray-400"> | |
| <div class="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=200')] bg-cover opacity-20"></div> | |
| <span class="relative z-10 text-sm shadow-black drop-shadow-md">مينيمال</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Instructions --> | |
| <div> | |
| <h3 class="text-lg font-bold mb-3"><i class="fa-solid fa-pen-nib text-dark-accent ml-2"></i>تعليمات المولد</h3> | |
| <textarea id="extra-instructions" class="w-full bg-black border border-white/10 rounded-lg p-3 text-sm text-white focus:border-dark-accent outline-none h-24 resize-none" placeholder="أريد إضافة مكتب زجاجي ونباتات داخلية..."></textarea> | |
| </div> | |
| </div> | |
| <!-- Generate Action --> | |
| <div class="p-6 bg-dark-surface border-t border-white/10"> | |
| <button id="generate-btn" class="w-full bg-dark-accent text-black font-black py-4 rounded-xl flex items-center justify-center gap-2 btn-glow transition"> | |
| <i class="fa-solid fa-bolt"></i> توليد التصميم | |
| </button> | |
| </div> | |
| </aside> | |
| <!-- LEFT PANEL (Canvas Viewer) --> | |
| <section class="flex-1 bg-[#05080f] relative flex flex-col overflow-y-auto"> | |
| <!-- Canvas Container --> | |
| <div class="p-8 w-full max-w-5xl mx-auto flex-1 flex flex-col"> | |
| <!-- Empty/Placeholder State --> | |
| <div id="canvas-empty" class="flex-1 flex flex-col items-center justify-center text-gray-600"> | |
| <i class="fa-solid fa-border-none text-6xl mb-4 opacity-50"></i> | |
| <p class="text-xl font-bold">مساحة العمل فارغة</p> | |
| <p class="text-sm">قم برفع صورة للبدء في المعاينة الحية</p> | |
| </div> | |
| <!-- Processing State (Scanning) --> | |
| <div id="canvas-processing" class="hidden flex-1 relative rounded-2xl overflow-hidden border border-white/10"> | |
| <img id="processing-img" src="" class="w-full h-full object-contain bg-black"> | |
| <div class="scan-overlay"></div> | |
| <div class="scan-line"></div> | |
| <div class="absolute bottom-10 left-1/2 -translate-x-1/2 bg-black/80 backdrop-blur-md px-6 py-3 rounded-full border border-dark-accent/50 text-dark-accent font-bold flex items-center gap-3"> | |
| <i class="fa-solid fa-circle-notch fa-spin"></i> <span id="processing-text">يتم تحليل الغرفة...</span> | |
| </div> | |
| </div> | |
| <!-- Results State --> | |
| <div id="canvas-results" class="hidden flex-col w-full"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h2 class="text-2xl font-black text-white"><i class="fa-solid fa-wand-magic-sparkles text-dark-accent ml-2"></i>النتيجة النهائية</h2> | |
| <span id="result-desc" class="text-gray-400 text-sm"></span> | |
| </div> | |
| <!-- Slider --> | |
| <div class="slider-container mb-8" id="comparison-slider"> | |
| <!-- Before --> | |
| <img id="result-before-img" src="" class="slider-img grayscale" alt="Before"> | |
| <!-- After --> | |
| <div class="slider-after-container" id="slider-overlay"> | |
| <img id="result-after-img" src="" class="slider-img" style="width: 200%; max-width: none;" alt="After"> | |
| </div> | |
| <!-- Handle --> | |
| <div class="slider-handle" id="slider-handle"><i class="fa-solid fa-arrows-left-right"></i></div> | |
| <div class="absolute top-4 right-4 bg-black/80 text-white px-2 py-1 rounded text-xs z-10 pointer-events-none">قبل</div> | |
| <div class="absolute top-4 left-4 bg-dark-accent text-black px-2 py-1 rounded text-xs font-bold z-20 pointer-events-none">بعد</div> | |
| </div> | |
| <!-- Furniture --> | |
| <h3 class="text-xl font-bold mb-4 border-b border-white/10 pb-2">عناصر التصميم (الأثاث المقترح)</h3> | |
| <div id="furniture-grid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 pb-12"> | |
| <!-- Populated by JS --> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const dropzone = document.getElementById('dropzone'); | |
| const fileInput = document.getElementById('file-input'); | |
| const preview = document.getElementById('image-preview'); | |
| const previewImg = document.getElementById('preview-img'); | |
| const removeImgBtn = document.getElementById('remove-img-btn'); | |
| const styleBtns = document.querySelectorAll('.style-btn'); | |
| const extraInstructions = document.getElementById('extra-instructions'); | |
| const generateBtn = document.getElementById('generate-btn'); | |
| const canvasEmpty = document.getElementById('canvas-empty'); | |
| const canvasProcessing = document.getElementById('canvas-processing'); | |
| const canvasResults = document.getElementById('canvas-results'); | |
| const processingImg = document.getElementById('processing-img'); | |
| const processingText = document.getElementById('processing-text'); | |
| let isImageUploaded = false; | |
| let selectedStyle = "مودرن"; | |
| let base64Image = null; | |
| // 1. Upload Logic | |
| dropzone.addEventListener('click', (e) => { | |
| if(!isImageUploaded && e.target !== fileInput) fileInput.click(); | |
| }); | |
| fileInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if(file) { | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| const img = new Image(); | |
| img.onload = () => { | |
| const canvas = document.createElement('canvas'); | |
| const MAX = 1000; | |
| let w = img.width, h = img.height; | |
| if (w > h && w > MAX) { h *= MAX / w; w = MAX; } | |
| else if (h > MAX) { w *= MAX / h; h = MAX; } | |
| canvas.width = w; canvas.height = h; | |
| canvas.getContext('2d').drawImage(img, 0, 0, w, h); | |
| base64Image = canvas.toDataURL('image/jpeg', 0.8); | |
| previewImg.src = base64Image; | |
| preview.classList.remove('hidden'); | |
| preview.classList.add('flex'); | |
| isImageUploaded = true; | |
| // Show in canvas right away | |
| canvasEmpty.classList.add('hidden'); | |
| canvasProcessing.classList.remove('hidden'); | |
| canvasProcessing.querySelector('.scan-line').style.display = 'none'; | |
| canvasProcessing.querySelector('.scan-overlay').style.display = 'none'; | |
| canvasProcessing.querySelector('.absolute.bottom-10').style.display = 'none'; | |
| processingImg.src = base64Image; | |
| canvasResults.classList.add('hidden'); | |
| }; | |
| img.src = event.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| removeImgBtn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| preview.classList.add('hidden'); | |
| preview.classList.remove('flex'); | |
| isImageUploaded = false; | |
| fileInput.value = ''; | |
| base64Image = null; | |
| canvasEmpty.classList.remove('hidden'); | |
| canvasProcessing.classList.add('hidden'); | |
| canvasResults.classList.add('hidden'); | |
| }); | |
| // 2. Style Logic | |
| styleBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| styleBtns.forEach(b => { | |
| b.classList.remove('border-dark-accent'); | |
| b.classList.add('border-transparent', 'text-gray-400'); | |
| b.querySelector('span').classList.remove('text-dark-accent'); | |
| }); | |
| btn.classList.add('border-dark-accent'); | |
| btn.classList.remove('border-transparent', 'text-gray-400'); | |
| btn.querySelector('span').classList.add('text-dark-accent'); | |
| selectedStyle = btn.querySelector('span').innerText.trim(); | |
| }); | |
| }); | |
| // 3. Generate Logic | |
| generateBtn.addEventListener('click', async () => { | |
| if(!isImageUploaded) { alert('الرجاء رفع صورة الغرفة أولاً'); return; } | |
| // UI to Processing State | |
| canvasEmpty.classList.add('hidden'); | |
| canvasResults.classList.add('hidden'); | |
| canvasProcessing.classList.remove('hidden'); | |
| canvasProcessing.querySelector('.scan-line').style.display = 'block'; | |
| canvasProcessing.querySelector('.scan-overlay').style.display = 'block'; | |
| canvasProcessing.querySelector('.absolute.bottom-10').style.display = 'flex'; | |
| generateBtn.disabled = true; | |
| generateBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> جاري المعالجة...'; | |
| // Sequence texts | |
| const texts = ["جاري تحليل المساحة...", "استخراج أبعاد الغرفة...", `تطبيق نمط ${selectedStyle}...`, "البحث عن الأثاث المناسب..."]; | |
| let txtIdx = 0; | |
| const txtInt = setInterval(() => { | |
| txtIdx = (txtIdx + 1) % texts.length; | |
| processingText.innerText = texts[txtIdx]; | |
| }, 2000); | |
| try { | |
| const response = await fetch('/api/design/analyze', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| image_base64: base64Image, | |
| style: selectedStyle, | |
| instructions: extraInstructions.value | |
| }) | |
| }); | |
| const result = await response.json(); | |
| clearInterval(txtInt); | |
| if (result && result.success && result.data) { | |
| const data = result.data; | |
| document.getElementById('result-before-img').src = base64Image; | |
| // Use the mock image or generated image url (mocking for now as API returns placeholder) | |
| document.getElementById('result-after-img').src = "https://images.unsplash.com/photo-1600210492486-724fe5c67fb0?w=1200"; | |
| if(data.design_description) document.getElementById('result-desc').innerText = data.design_description; | |
| const furnGrid = document.getElementById('furniture-grid'); | |
| furnGrid.innerHTML = ''; | |
| const icons = {"sofa": "fa-couch", "coffee table": "fa-table", "lamp": "fa-lightbulb", "rug": "fa-map", "art": "fa-palette", "chair": "fa-chair"}; | |
| if(data.furniture) { | |
| data.furniture.forEach(item => { | |
| const fType = (item.furniture_type || '').toLowerCase(); | |
| const defaultIcon = icons[fType] || 'fa-box'; | |
| furnGrid.innerHTML += ` | |
| <div class="bg-dark-surface border border-white/5 rounded-xl p-4 hover:border-dark-accent/50 transition cursor-pointer flex flex-col"> | |
| <div class="h-24 bg-[#05080f] rounded-lg mb-3 flex items-center justify-center text-3xl text-gray-600"> | |
| <i class="fa-solid ${defaultIcon}"></i> | |
| </div> | |
| <div class="text-[10px] text-dark-accent font-bold mb-1">${item.category || ''}</div> | |
| <h4 class="font-bold text-white text-sm mb-2 leading-tight flex-1">${item.name || 'قطعة أثاث'}</h4> | |
| <div class="flex justify-between items-center mt-2 border-t border-white/5 pt-2"> | |
| <span class="font-black text-sm">${item.price ? item.price + ' ج' : ''}</span> | |
| <button class="text-gray-400 hover:text-dark-accent transition"><i class="fa-solid fa-cart-shopping"></i></button> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| } | |
| // Show Results | |
| canvasProcessing.classList.add('hidden'); | |
| canvasResults.classList.remove('hidden'); | |
| canvasResults.classList.add('flex'); | |
| initSlider(); | |
| } else { | |
| alert('حدث خطأ في استخراج التصميم.'); | |
| canvasProcessing.classList.add('hidden'); | |
| canvasEmpty.classList.remove('hidden'); | |
| } | |
| } catch (err) { | |
| clearInterval(txtInt); | |
| alert('تعذر الاتصال بالخادم.'); | |
| canvasProcessing.classList.add('hidden'); | |
| canvasEmpty.classList.remove('hidden'); | |
| } finally { | |
| generateBtn.disabled = false; | |
| generateBtn.innerHTML = '<i class="fa-solid fa-bolt"></i> توليد التصميم'; | |
| } | |
| }); | |
| // 4. Slider Logic | |
| function initSlider() { | |
| const sliderContainer = document.getElementById('comparison-slider'); | |
| const overlay = document.getElementById('slider-overlay'); | |
| const handle = document.getElementById('slider-handle'); | |
| const afterImg = document.getElementById('result-after-img'); | |
| let isSliding = false; | |
| const updateImageSize = () => { afterImg.style.width = sliderContainer.offsetWidth + 'px'; }; | |
| window.addEventListener('resize', updateImageSize); | |
| updateImageSize(); | |
| // reset to middle | |
| overlay.style.width = "50%"; | |
| handle.style.left = "50%"; | |
| const slide = (x) => { | |
| const rect = sliderContainer.getBoundingClientRect(); | |
| let position = ((x - rect.left) / rect.width) * 100; | |
| if(position < 0) position = 0; if(position > 100) position = 100; | |
| overlay.style.width = position + "%"; | |
| handle.style.left = position + "%"; | |
| }; | |
| sliderContainer.addEventListener('mousedown', (e) => { isSliding = true; slide(e.clientX); }); | |
| sliderContainer.addEventListener('mousemove', (e) => { if(isSliding) slide(e.clientX); }); | |
| window.addEventListener('mouseup', () => { isSliding = false; }); | |
| sliderContainer.addEventListener('touchstart', (e) => { isSliding = true; slide(e.touches[0].clientX); }); | |
| sliderContainer.addEventListener('touchmove', (e) => { if(isSliding) slide(e.touches[0].clientX); }); | |
| window.addEventListener('touchend', () => { isSliding = false; }); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |