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 - 3D Showroom</title> | |
| <script src="https://cdn.tailwindcss.com"></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=Cairo:wght@300;400;700;900&display=swap" rel="stylesheet"> | |
| <!-- Leaflet Map --> | |
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/> | |
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| colors: { | |
| dark: { bg: '#0B0F19', surface: '#161D2B', accent: '#F59E0B' } | |
| }, | |
| fontFamily: { sans: ['Cairo', 'sans-serif'] } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| body { margin: 0; padding: 0; overflow: hidden; font-family: 'Cairo', sans-serif; background: #000; color: #fff; } | |
| .ai-gradient-text { | |
| background: linear-gradient(135deg, #FCD34D, #F59E0B); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| /* Showroom Drawer */ | |
| .showroom-drawer { | |
| background: rgba(11, 15, 25, 0.85); | |
| backdrop-filter: blur(40px); | |
| -webkit-backdrop-filter: blur(40px); | |
| border-top: 1px solid rgba(255, 255, 255, 0.1); | |
| box-shadow: 0 -20px 60px rgba(0,0,0,0.6); | |
| transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| /* Scrollbars */ | |
| .drawer-scroll::-webkit-scrollbar { width: 4px; height: 4px; } | |
| .drawer-scroll::-webkit-scrollbar-track { background: transparent; } | |
| .drawer-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 10px; } | |
| .drawer-scroll::-webkit-scrollbar-thumb:hover { background: #F59E0B; } | |
| /* Map Tweaks */ | |
| #map { width: 100%; height: 100px; border-radius: 1rem; border: 1px solid rgba(255,255,255,0.1); } | |
| .leaflet-control-zoom { display: none; } | |
| </style> | |
| </head> | |
| <body class="w-screen h-screen relative"> | |
| <!-- TOP: THE SHOWROOM (Visuals) --> | |
| <div class="absolute top-0 left-0 right-0 h-[75vh] bg-black z-0 overflow-hidden group"> | |
| <!-- Navbar Overlay --> | |
| <nav class="absolute top-6 left-8 right-8 z-50 flex justify-between items-center pointer-events-none"> | |
| <div class="flex items-center gap-3 cursor-pointer pointer-events-auto hover:scale-105 transition bg-black/40 backdrop-blur border border-white/10 px-4 py-2 rounded-full" onclick="window.location.href='explore.html'"> | |
| <i class="fa-solid fa-arrow-right text-white"></i> | |
| <span class="text-sm font-bold text-white uppercase">العودة للبحث</span> | |
| </div> | |
| <div class="pointer-events-auto flex gap-3"> | |
| <span class="bg-dark-accent text-black font-black px-4 py-2 rounded-full shadow-[0_0_20px_rgba(245,158,11,0.5)]" id="propStatus">للبيع</span> | |
| <span class="bg-black/60 backdrop-blur border border-white/20 text-white font-bold px-4 py-2 rounded-full" id="propType">عقار</span> | |
| </div> | |
| </nav> | |
| <!-- Hero Image --> | |
| <img id="fgImage" src="" class="absolute inset-0 w-full h-full object-cover transition duration-[3s] ease-out group-hover:scale-105 opacity-90"> | |
| <div id="imgOverlay" class="absolute inset-0 bg-gradient-to-t from-[#0B0F19] via-transparent to-black/30 pointer-events-none z-10"></div> | |
| <!-- 3D Iframe (Hidden initially) --> | |
| <iframe id="matterportFrame" class="hidden absolute inset-0 w-full h-full z-20 border-0" allowfullscreen allow="xr-spatial-tracking"></iframe> | |
| <!-- 3D Toggle Button --> | |
| <button id="start3DTourBtn" onclick="start3DTour()" class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-30 group-hover:scale-110 transition duration-300"> | |
| <div class="w-32 h-32 bg-black/50 backdrop-blur-xl border border-white/30 rounded-full flex flex-col items-center justify-center text-white shadow-[0_0_50px_rgba(0,0,0,0.6)] cursor-pointer hover:bg-dark-accent hover:text-black hover:border-dark-accent transition-colors duration-300 group/btn"> | |
| <i class="fa-solid fa-vr-cardboard text-5xl mb-2 group-hover/btn:scale-110 transition"></i> | |
| <span class="font-black text-xs uppercase tracking-widest">بدء الجولة</span> | |
| </div> | |
| </button> | |
| </div> | |
| <!-- BOTTOM: THE DASHBOARD DRAWER --> | |
| <div class="showroom-drawer absolute bottom-0 left-0 right-0 h-[35vh] rounded-t-[3rem] z-40 p-8 pt-10 flex flex-row gap-8 overflow-x-auto drawer-scroll items-stretch"> | |
| <!-- SECTION 1: Property Info (Fixed Width) --> | |
| <div class="w-[400px] shrink-0 flex flex-col justify-between border-l border-white/10 pl-8"> | |
| <div> | |
| <h1 class="text-3xl font-black leading-tight text-white mb-2 line-clamp-2" id="propTitle">جاري التحميل...</h1> | |
| <div class="text-sm text-gray-400 flex items-center gap-1 mb-4"><i class="fa-solid fa-location-dot text-dark-accent"></i> <span id="propLocation">...</span></div> | |
| <div class="text-4xl font-black text-dark-accent tracking-wide" id="propPrice">... <span class="text-lg text-gray-400 font-normal">ج.م</span></div> | |
| </div> | |
| <!-- Specs Grid --> | |
| <div class="grid grid-cols-4 gap-2 mt-4"> | |
| <div class="bg-white/5 border border-white/10 rounded-xl p-2 flex flex-col items-center justify-center text-center"> | |
| <i class="fa-solid fa-bed text-dark-accent text-lg mb-1"></i><span id="specBeds" class="font-bold text-sm">-</span> | |
| </div> | |
| <div class="bg-white/5 border border-white/10 rounded-xl p-2 flex flex-col items-center justify-center text-center"> | |
| <i class="fa-solid fa-bath text-dark-accent text-lg mb-1"></i><span id="specBaths" class="font-bold text-sm">-</span> | |
| </div> | |
| <div class="bg-white/5 border border-white/10 rounded-xl p-2 flex flex-col items-center justify-center text-center"> | |
| <i class="fa-solid fa-ruler-combined text-dark-accent text-lg mb-1"></i><span id="specArea" class="font-bold text-sm">-</span> | |
| </div> | |
| <div class="bg-white/5 border border-white/10 rounded-xl p-2 flex flex-col items-center justify-center text-center"> | |
| <i class="fa-solid fa-car text-dark-accent text-lg mb-1"></i><span id="specParking" class="font-bold text-sm">-</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- SECTION 2: AI Analysis (Fixed Width) --> | |
| <div class="w-[350px] shrink-0 flex flex-col gap-4 border-l border-white/10 pl-8"> | |
| <div class="flex items-center gap-2 mb-2"> | |
| <div class="w-8 h-8 rounded-full bg-dark-accent flex items-center justify-center text-black shadow-[0_0_10px_rgba(245,158,11,0.5)]"><i class="fa-solid fa-robot"></i></div> | |
| <span class="text-xl font-black ai-gradient-text">التحليل الذكي</span> | |
| </div> | |
| <!-- AI Valuation Mini --> | |
| <div class="bg-dark-surface/50 rounded-xl p-4 border border-white/5"> | |
| <div class="text-[10px] font-bold uppercase text-gray-400 mb-1"><i class="fa-solid fa-scale-balanced text-dark-accent"></i> تقييم السعر</div> | |
| <div id="valTitleContainer" class="text-lg font-black text-white"><i class="fa-solid fa-spinner fa-spin"></i> جاري...</div> | |
| </div> | |
| <!-- Investment Scores --> | |
| <div class="bg-dark-surface/50 rounded-xl p-4 border border-white/5 flex-1 flex flex-col justify-center space-y-4"> | |
| <div> | |
| <div class="flex justify-between text-xs font-bold mb-1"><span class="text-gray-300">عائد الاستثمار (ROI)</span><span class="text-white" id="roiText">...</span></div> | |
| <div class="w-full bg-black/50 rounded-full h-1.5"><div class="bg-blue-400 h-full rounded-full transition-all duration-1000 w-0" id="roiBar"></div></div> | |
| </div> | |
| <div> | |
| <div class="flex justify-between text-xs font-bold mb-1"><span class="text-gray-300">النمو الرأسمالي</span><span class="text-white" id="growthText">...</span></div> | |
| <div class="w-full bg-black/50 rounded-full h-1.5"><div class="bg-green-400 h-full rounded-full transition-all duration-1000 w-0" id="growthBar"></div></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- SECTION 3: Communication & Map (Flexible Width) --> | |
| <div class="flex-1 min-w-[400px] flex gap-6"> | |
| <!-- AI Chat --> | |
| <div class="flex-1 flex flex-col bg-white/5 border border-white/10 rounded-2xl overflow-hidden h-full"> | |
| <div class="bg-white/5 p-3 border-b border-white/5 flex items-center gap-2"> | |
| <i class="fa-regular fa-comment-dots text-dark-accent"></i> <span class="text-sm font-bold">اسأل الذكاء الاصطناعي</span> | |
| </div> | |
| <div class="flex-1 overflow-y-auto drawer-scroll p-4 space-y-3" id="chatArea"> | |
| <div class="flex gap-2"> | |
| <div class="bg-white/10 p-2 rounded-xl rounded-tr-none text-xs text-gray-200 border border-white/5 shadow-sm max-w-[90%]"> | |
| أنا هنا للإجابة على أي أسئلة حول العقار. | |
| </div> | |
| </div> | |
| </div> | |
| <div class="p-2 border-t border-white/5 bg-black/20"> | |
| <div class="flex bg-black/50 rounded-lg overflow-hidden border border-white/10 focus-within:border-dark-accent transition"> | |
| <input type="text" id="chatInput" placeholder="اكتب سؤالك..." class="w-full bg-transparent text-xs text-white px-3 py-2 outline-none"> | |
| <button onclick="sendChatMessage()" class="text-dark-accent px-3 hover:text-white transition"><i class="fa-solid fa-paper-plane"></i></button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Mini Map & Desc --> | |
| <div class="w-[250px] shrink-0 flex flex-col gap-4 h-full"> | |
| <div id="map" class="shadow-inner flex-1 max-h-[120px]"></div> | |
| <div class="flex-1 overflow-y-auto drawer-scroll text-xs text-gray-400 leading-relaxed pr-2" id="propDesc"> | |
| ... | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', async () => { | |
| const params = new URLSearchParams(window.location.search); | |
| const propIndex = params.get('index') || 0; | |
| let propData = null; | |
| const storedAiData = sessionStorage.getItem('aiRecommendations'); | |
| if(storedAiData) { | |
| const db = JSON.parse(storedAiData); | |
| propData = db[propIndex]; | |
| } | |
| // Fallback: If no data in session storage (e.g. direct link), fetch from backend | |
| if(!propData) { | |
| try { | |
| const res = await fetch('/api/properties'); | |
| const json = await res.json(); | |
| if(json.properties && json.properties.length > 0) { | |
| const db = json.properties; | |
| propData = db[propIndex] || db[0]; // fallback to first if index invalid | |
| } | |
| } catch (e) { | |
| console.error("Failed to fetch fallback properties", e); | |
| } | |
| } | |
| if(propData) { | |
| document.getElementById('propTitle').innerText = propData.title; | |
| document.getElementById('propLocation').innerText = propData.location; | |
| document.getElementById('propPrice').innerText = propData.price; | |
| document.getElementById('propDesc').innerText = propData.description; | |
| document.getElementById('propType').innerText = propData.type; | |
| document.getElementById('propStatus').innerText = propData.status; | |
| if(propData.image) document.getElementById('fgImage').src = propData.image; | |
| if(propData.lat && propData.lng) { | |
| const map = L.map('map', { zoomControl: false, attributionControl: false }).setView([propData.lat, propData.lng], 14); | |
| L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png').addTo(map); | |
| const icon = L.divIcon({ className: 'custom-div-icon', html: "<div style='width:12px;height:12px;background:#F59E0B;border-radius:50%;border:2px solid #000;'></div>", iconSize: [12, 12], iconAnchor: [6, 6] }); | |
| L.marker([propData.lat, propData.lng], {icon: icon}).addTo(map); | |
| } | |
| fetchInsights(propData); | |
| } | |
| document.getElementById('chatInput').addEventListener('keypress', function (e) { | |
| if (e.key === 'Enter') sendChatMessage(); | |
| }); | |
| }); | |
| function start3DTour() { | |
| const iframe = document.getElementById('matterportFrame'); | |
| const img = document.getElementById('fgImage'); | |
| const overlay = document.getElementById('imgOverlay'); | |
| const btn = document.getElementById('start3DTourBtn'); | |
| const storedAiData = sessionStorage.getItem('aiRecommendations'); | |
| const params = new URLSearchParams(window.location.search); | |
| const propIndex = params.get('index') || 0; | |
| let matterportId = "SxQL3iGyoDo"; | |
| if(storedAiData) { | |
| const db = JSON.parse(storedAiData); | |
| if(db[propIndex] && db[propIndex].matterport_id) matterportId = db[propIndex].matterport_id; | |
| } | |
| iframe.src = `https://my.matterport.com/show/?m=${matterportId}&play=1`; | |
| iframe.classList.remove('hidden'); | |
| img.style.opacity = '0'; | |
| overlay.style.opacity = '0'; | |
| btn.classList.add('hidden'); | |
| } | |
| async function fetchInsights(prop) { | |
| try { | |
| const response = await fetch('/api/property/insight', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ title: prop.title, price: prop.price, location: prop.location, type: prop.type, description: prop.description }) | |
| }); | |
| const result = await response.json(); | |
| if(result.success) { | |
| const data = result.data; | |
| document.getElementById('valTitleContainer').innerHTML = `<span class="text-green-400">${data.valuation_title}</span>`; | |
| document.getElementById('roiText').innerText = data.roi_percentage; | |
| document.getElementById('roiBar').style.width = data.roi_progress + '%'; | |
| document.getElementById('growthText').innerText = data.growth_percentage; | |
| document.getElementById('growthBar').style.width = data.growth_progress + '%'; | |
| if(data.specs) { | |
| document.getElementById('specBeds').innerText = data.specs.beds || '-'; | |
| document.getElementById('specBaths').innerText = data.specs.baths || '-'; | |
| document.getElementById('specArea').innerText = data.specs.area ? data.specs.area + 'm' : '-'; | |
| document.getElementById('specParking').innerText = data.specs.parking || '-'; | |
| } | |
| } | |
| } catch (error) { | |
| document.getElementById('valTitleContainer').innerHTML = `<span class="text-red-400">فشل التحليل</span>`; | |
| } | |
| } | |
| let chatContext = []; | |
| async function sendChatMessage() { | |
| const input = document.getElementById('chatInput'); | |
| const message = input.value.trim(); | |
| if(!message) return; | |
| const chatArea = document.getElementById('chatArea'); | |
| chatArea.insertAdjacentHTML('beforeend', ` | |
| <div class="flex gap-2 flex-row-reverse"> | |
| <div class="bg-dark-accent p-2 rounded-xl rounded-tl-none text-xs text-black border border-transparent max-w-[85%] font-bold shadow-sm"> | |
| ${message} | |
| </div> | |
| </div> | |
| `); | |
| input.value = ''; | |
| chatArea.scrollTop = chatArea.scrollHeight; | |
| const typingId = 'typing-' + Date.now(); | |
| chatArea.insertAdjacentHTML('beforeend', `<div id="${typingId}" class="flex gap-2"> | |
| <div class="bg-white/10 p-2 rounded-xl rounded-tr-none text-xs text-gray-400 shadow-sm border border-white/5"><i class="fa-solid fa-ellipsis fa-fade"></i></div> | |
| </div>`); | |
| chatArea.scrollTop = chatArea.scrollHeight; | |
| if (chatContext.length === 0) { | |
| const title = document.getElementById('propTitle').innerText; | |
| const price = document.getElementById('propPrice').innerText; | |
| const loc = document.getElementById('propLocation').innerText; | |
| const desc = document.getElementById('propDesc').innerText; | |
| chatContext.push({ "role": "system", "content": `أنت مساعد عقاري ذكي. المستخدم يسأل عن العقار التالي: | |
| العنوان: ${title} | |
| السعر: ${price} | |
| المنطقة: ${loc} | |
| الوصف: ${desc} | |
| أجب باختصار وبشكل مباشر ومفيد بناءً على هذه المعلومات فقط.` }); | |
| } | |
| try { | |
| const response = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ message: message, history: chatContext }) | |
| }); | |
| const data = await response.json(); | |
| document.getElementById(typingId).remove(); | |
| const reply = data.reply || "عذراً، حدث خطأ."; | |
| chatArea.insertAdjacentHTML('beforeend', ` | |
| <div class="flex gap-2"> | |
| <div class="bg-white/10 p-2 rounded-xl rounded-tr-none text-xs text-white border border-white/5 max-w-[85%] shadow-sm leading-relaxed"> | |
| ${reply.replace(/\n/g, '<br>')} | |
| </div> | |
| </div> | |
| `); | |
| chatArea.scrollTop = chatArea.scrollHeight; | |
| chatContext.push({"role": "user", "content": message}, {"role": "assistant", "content": reply}); | |
| } catch (e) { | |
| document.getElementById(typingId).remove(); | |
| chatArea.insertAdjacentHTML('beforeend', `<div class="text-red-400 text-[10px]">فشل الاتصال.</div>`); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |