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 - AI Analytics</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"> | |
| <!-- Chart.js --> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| colors: { | |
| dark: { bg: '#0B0F19', surface: '#161D2B', accent: '#F59E0B', panel: '#111827' } | |
| }, | |
| 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; | |
| } | |
| /* Scrollbars */ | |
| .dash-scroll::-webkit-scrollbar { width: 4px; } | |
| .dash-scroll::-webkit-scrollbar-track { background: transparent; } | |
| .dash-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; } | |
| .dash-scroll::-webkit-scrollbar-thumb:hover { background: #F59E0B; } | |
| .terminal-panel { | |
| background: rgba(17, 24, 39, 0.8); | |
| border: 1px solid rgba(255,255,255,0.05); | |
| border-radius: 1rem; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.5); | |
| backdrop-filter: blur(10px); | |
| } | |
| .highlight-win { color: #4ade80 ; font-weight: 900; } | |
| .highlight-lose { color: #f87171 ; } | |
| .vs-badge { | |
| background: linear-gradient(135deg, #FCD34D, #F59E0B); | |
| color: black; | |
| box-shadow: 0 0 20px rgba(245, 158, 11, 0.4); | |
| animation: pulseGlow 2s infinite alternate; | |
| } | |
| @keyframes pulseGlow { | |
| from { box-shadow: 0 0 10px rgba(245, 158, 11, 0.4); } | |
| to { box-shadow: 0 0 30px rgba(245, 158, 11, 0.8); } | |
| } | |
| </style> | |
| </head> | |
| <body class="w-screen h-screen flex flex-col bg-[#050810] relative"> | |
| <!-- Background Grid Pattern --> | |
| <div class="absolute inset-0 pointer-events-none opacity-20" style="background-image: radial-gradient(rgba(245, 158, 11, 0.1) 1px, transparent 1px); background-size: 30px 30px;"></div> | |
| <!-- Top Navigation --> | |
| <nav class="h-16 shrink-0 border-b border-white/5 bg-dark-bg/50 backdrop-blur-md flex justify-between items-center px-6 z-50"> | |
| <div class="flex items-center gap-3 cursor-pointer hover:scale-105 transition" onclick="window.location.href='index.html'"> | |
| <div class="w-8 h-8 bg-dark-accent rounded flex items-center justify-center shadow-[0_0_10px_rgba(245,158,11,0.5)]"> | |
| <i class="fa-solid fa-chart-line text-black"></i> | |
| </div> | |
| <span class="text-sm font-black tracking-widest text-white uppercase">DARAK <span class="text-dark-accent">Analytics</span></span> | |
| </div> | |
| <button onclick="window.location.href='explore.html'" class="text-gray-400 hover:text-white transition text-sm font-bold flex items-center gap-2"> | |
| العودة للبحث <i class="fa-solid fa-arrow-left"></i> | |
| </button> | |
| </nav> | |
| <!-- Main Dashboard --> | |
| <main class="flex-1 flex gap-4 p-4 overflow-hidden z-10"> | |
| <!-- COLUMN 1: Property A --> | |
| <div class="w-1/4 shrink-0 terminal-panel flex flex-col overflow-hidden relative group"> | |
| <!-- Selector --> | |
| <div class="p-4 border-b border-white/5 bg-black/20"> | |
| <label class="block text-[10px] text-gray-500 font-bold uppercase tracking-widest mb-1">العقار الأول (A)</label> | |
| <select id="selector-1" onchange="updateProperty(1, this.value)" class="w-full bg-dark-surface border border-white/10 text-white rounded p-2 text-sm outline-none focus:border-blue-500 transition cursor-pointer"> | |
| <option value="0">جاري التحميل...</option> | |
| </select> | |
| </div> | |
| <!-- Image --> | |
| <div class="h-[25vh] w-full relative overflow-hidden shrink-0 border-b border-white/5"> | |
| <img id="img-1" src="" class="absolute inset-0 w-full h-full object-cover opacity-70 group-hover:scale-105 transition duration-700"> | |
| <div class="absolute top-2 right-2 bg-blue-500 text-black font-black text-xs px-2 py-1 rounded">A</div> | |
| </div> | |
| <!-- Stats --> | |
| <div class="flex-1 p-4 overflow-y-auto dash-scroll space-y-4"> | |
| <div> | |
| <h2 class="text-xl font-black text-blue-400 leading-tight mb-1" id="title-1">...</h2> | |
| <div class="text-xs text-gray-400"><i class="fa-solid fa-location-dot"></i> <span id="loc-1">...</span></div> | |
| </div> | |
| <table class="w-full text-sm"> | |
| <tr class="border-b border-white/5"><td class="py-3 text-gray-400 w-1/2">السعر</td><td class="py-3 font-bold text-left" id="price-1">...</td></tr> | |
| <tr class="border-b border-white/5"><td class="py-3 text-gray-400">المساحة</td><td class="py-3 font-bold text-left" id="area-1">...</td></tr> | |
| <tr class="border-b border-white/5"><td class="py-3 text-gray-400">الغرف</td><td class="py-3 font-bold text-left" id="rooms-1">...</td></tr> | |
| <tr class="border-b border-white/5"><td class="py-3 text-gray-400">التشطيب</td><td class="py-3 font-bold text-left" id="finish-1">...</td></tr> | |
| <tr><td class="py-3 text-gray-400">عائد الاستثمار</td><td class="py-3 font-bold text-left" id="roi-1" data-val="0">...</td></tr> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- COLUMN 2: Analytical Radar & AI Verdict --> | |
| <div class="flex-1 terminal-panel flex flex-col overflow-hidden relative"> | |
| <div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 vs-badge w-10 h-10 rounded-full flex items-center justify-center font-black text-sm z-0">VS</div> | |
| <div class="p-4 border-b border-white/5 flex items-center justify-between bg-black/20 z-10"> | |
| <div class="text-sm font-bold ai-gradient-text uppercase tracking-widest"><i class="fa-solid fa-chart-radar"></i> تحليل المقارنة</div> | |
| <div id="compareStatus" class="text-xs bg-white/10 px-2 py-1 rounded text-gray-300 font-bold"><i class="fa-solid fa-check text-green-400"></i> محدث</div> | |
| </div> | |
| <!-- Radar Chart Container --> | |
| <div class="h-[50vh] w-full p-4 shrink-0 flex items-center justify-center z-10 relative"> | |
| <canvas id="radarChart"></canvas> | |
| </div> | |
| <!-- AI Summary Output --> | |
| <div class="flex-1 p-6 border-t border-white/5 bg-gradient-to-t from-dark-accent/5 to-transparent overflow-y-auto dash-scroll z-10"> | |
| <div class="flex items-start gap-4"> | |
| <div class="w-10 h-10 rounded-xl bg-dark-accent shrink-0 flex items-center justify-center text-black text-xl shadow-[0_0_15px_rgba(245,158,11,0.3)]"><i class="fa-solid fa-robot"></i></div> | |
| <div> | |
| <h3 class="text-sm font-bold text-dark-accent mb-2 tracking-widest uppercase">توصية الذكاء الاصطناعي</h3> | |
| <p id="ai-opinion-text" class="text-sm text-gray-300 leading-relaxed font-bold">جاري تحليل البيانات واستخراج التوصيات...</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- COLUMN 3: Property B --> | |
| <div class="w-1/4 shrink-0 terminal-panel flex flex-col overflow-hidden relative group"> | |
| <!-- Selector --> | |
| <div class="p-4 border-b border-white/5 bg-black/20"> | |
| <label class="block text-[10px] text-gray-500 font-bold uppercase tracking-widest mb-1">العقار الثاني (B)</label> | |
| <select id="selector-2" onchange="updateProperty(2, this.value)" class="w-full bg-dark-surface border border-white/10 text-white rounded p-2 text-sm outline-none focus:border-dark-accent transition cursor-pointer"> | |
| <option value="1">جاري التحميل...</option> | |
| </select> | |
| </div> | |
| <!-- Image --> | |
| <div class="h-[25vh] w-full relative overflow-hidden shrink-0 border-b border-white/5"> | |
| <img id="img-2" src="" class="absolute inset-0 w-full h-full object-cover opacity-70 group-hover:scale-105 transition duration-700"> | |
| <div class="absolute top-2 right-2 bg-dark-accent text-black font-black text-xs px-2 py-1 rounded">B</div> | |
| </div> | |
| <!-- Stats --> | |
| <div class="flex-1 p-4 overflow-y-auto dash-scroll space-y-4"> | |
| <div> | |
| <h2 class="text-xl font-black text-dark-accent leading-tight mb-1" id="title-2">...</h2> | |
| <div class="text-xs text-gray-400"><i class="fa-solid fa-location-dot"></i> <span id="loc-2">...</span></div> | |
| </div> | |
| <table class="w-full text-sm"> | |
| <tr class="border-b border-white/5"><td class="py-3 font-bold text-right" id="price-2">...</td><td class="py-3 text-gray-400 w-1/2 text-left">السعر</td></tr> | |
| <tr class="border-b border-white/5"><td class="py-3 font-bold text-right" id="area-2">...</td><td class="py-3 text-gray-400 text-left">المساحة</td></tr> | |
| <tr class="border-b border-white/5"><td class="py-3 font-bold text-right" id="rooms-2">...</td><td class="py-3 text-gray-400 text-left">الغرف</td></tr> | |
| <tr class="border-b border-white/5"><td class="py-3 font-bold text-right" id="finish-2">...</td><td class="py-3 text-gray-400 text-left">التشطيب</td></tr> | |
| <tr><td class="py-3 font-bold text-right" id="roi-2" data-val="0">...</td><td class="py-3 text-gray-400 text-left">عائد الاستثمار</td></tr> | |
| </table> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| let dbProperties = []; | |
| let radarChartInstance = null; | |
| async function initCompare() { | |
| try { | |
| // Try session storage first for immediate load, fallback to fetch | |
| const storedAiData = sessionStorage.getItem('aiRecommendations'); | |
| if(storedAiData) { | |
| dbProperties = JSON.parse(storedAiData); | |
| if(dbProperties.length >= 2) { | |
| setupUI(); | |
| return; | |
| } | |
| } | |
| const response = await fetch('/api/properties?limit=20'); | |
| const data = await response.json(); | |
| if (data.properties && data.properties.length >= 2) { | |
| dbProperties = data.properties; | |
| setupUI(); | |
| } | |
| } catch (error) { | |
| console.error("Failed to load properties for compare", error); | |
| } | |
| } | |
| function setupUI() { | |
| initChart(); | |
| populateDropdowns(); | |
| document.getElementById('selector-1').value = 0; | |
| document.getElementById('selector-2').value = 1; | |
| updateProperty(1, 0, false); | |
| updateProperty(2, 1, true); // only trigger AI on the second load to avoid double trigger | |
| } | |
| function initChart() { | |
| const ctx = document.getElementById('radarChart').getContext('2d'); | |
| Chart.defaults.color = '#9ca3af'; | |
| Chart.defaults.font.family = 'Cairo'; | |
| radarChartInstance = new Chart(ctx, { | |
| type: 'radar', | |
| data: { | |
| labels: ['العائد (ROI)', 'المساحة', 'جودة الموقع', 'حالة التشطيب', 'السعر (معكوس)'], | |
| datasets: [ | |
| { | |
| label: 'عقار A', | |
| data: [50, 50, 50, 50, 50], | |
| backgroundColor: 'rgba(59, 130, 246, 0.2)', | |
| borderColor: '#3b82f6', | |
| pointBackgroundColor: '#3b82f6', | |
| borderWidth: 2, | |
| fill: true | |
| }, | |
| { | |
| label: 'عقار B', | |
| data: [50, 50, 50, 50, 50], | |
| backgroundColor: 'rgba(245, 158, 11, 0.2)', | |
| borderColor: '#F59E0B', | |
| pointBackgroundColor: '#F59E0B', | |
| borderWidth: 2, | |
| fill: true | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| r: { | |
| angleLines: { color: 'rgba(255, 255, 255, 0.1)' }, | |
| grid: { color: 'rgba(255, 255, 255, 0.1)' }, | |
| pointLabels: { font: { size: 14, weight: 'bold' }, color: '#fff' }, | |
| ticks: { display: false, min: 0, max: 100 } | |
| } | |
| }, | |
| plugins: { | |
| legend: { position: 'bottom', labels: { color: '#fff', font: {size: 14} } } | |
| } | |
| } | |
| }); | |
| } | |
| function populateDropdowns() { | |
| const selectors = [document.getElementById('selector-1'), document.getElementById('selector-2')]; | |
| selectors.forEach((select, index) => { | |
| select.innerHTML = ''; | |
| dbProperties.forEach((prop, i) => { | |
| const option = document.createElement('option'); | |
| option.value = i; | |
| option.innerText = prop.title; | |
| select.appendChild(option); | |
| }); | |
| }); | |
| } | |
| function extractArabicNum(str) { | |
| if(!str) return 0; | |
| let englishStr = str.toString().replace(/[٠-٩]/g, d => '٠١٢٣٤٥٦٧٨٩'.indexOf(d)); | |
| return parseFloat(englishStr.replace(/[^0-9.]/g, '')) || 0; | |
| } | |
| function updateProperty(colIndex, dbIndex, triggerAI = true) { | |
| const data = dbProperties[dbIndex]; | |
| if(!data) return; | |
| document.getElementById(`img-${colIndex}`).src = data.image || ''; | |
| document.getElementById(`title-${colIndex}`).innerText = data.title; | |
| document.getElementById(`loc-${colIndex}`).innerText = data.location; | |
| document.getElementById(`price-${colIndex}`).innerText = data.price; | |
| // Show loading spinners for AI generated data | |
| document.getElementById(`area-${colIndex}`).innerHTML = '<i class="fa-solid fa-spinner fa-spin text-gray-600"></i>'; | |
| document.getElementById(`rooms-${colIndex}`).innerHTML = '<i class="fa-solid fa-spinner fa-spin text-gray-600"></i>'; | |
| document.getElementById(`finish-${colIndex}`).innerHTML = '<i class="fa-solid fa-spinner fa-spin text-gray-600"></i>'; | |
| document.getElementById(`roi-${colIndex}`).innerHTML = '<i class="fa-solid fa-spinner fa-spin text-gray-600"></i>'; | |
| document.getElementById(`roi-${colIndex}`).dataset.val = "0"; | |
| const r1 = document.getElementById('roi-1'); | |
| const r2 = document.getElementById('roi-2'); | |
| if(r1 && r2) { | |
| r1.classList.remove('highlight-win', 'highlight-lose'); | |
| r2.classList.remove('highlight-win', 'highlight-lose'); | |
| } | |
| if(triggerAI) triggerAiCompare(); | |
| } | |
| function updateChart(data1, data2) { | |
| if(!radarChartInstance) return; | |
| // Extract numeric values safely | |
| const r1 = extractArabicNum(data1.roi); const r2 = extractArabicNum(data2.roi); | |
| const a1 = extractArabicNum(data1.area); const a2 = extractArabicNum(data2.area); | |
| const p1 = extractArabicNum(dbProperties[document.getElementById('selector-1').value].price); | |
| const p2 = extractArabicNum(dbProperties[document.getElementById('selector-2').value].price); | |
| // Normalize (0-100 relative to each other) | |
| const maxR = Math.max(r1, r2, 1); | |
| const maxA = Math.max(a1, a2, 1); | |
| const maxP = Math.max(p1, p2, 1); | |
| // Price is inverted (lower price = higher score) | |
| const pScore1 = (maxP / Math.max(p1, 1)) * 50; | |
| const pScore2 = (maxP / Math.max(p2, 1)) * 50; | |
| // Generate dummy scores for Location/Condition since AI doesn't return them directly | |
| const loc1 = 60 + Math.random()*40; const loc2 = 60 + Math.random()*40; | |
| const fin1 = 70 + Math.random()*30; const fin2 = 70 + Math.random()*30; | |
| radarChartInstance.data.datasets[0].data = [(r1/maxR)*100, (a1/maxA)*100, loc1, fin1, Math.min(pScore1, 100)]; | |
| radarChartInstance.data.datasets[1].data = [(r2/maxR)*100, (a2/maxA)*100, loc2, fin2, Math.min(pScore2, 100)]; | |
| radarChartInstance.update(); | |
| } | |
| let compareTimeout; | |
| function triggerAiCompare() { | |
| clearTimeout(compareTimeout); | |
| document.getElementById('ai-opinion-text').innerHTML = 'جاري المعالجة والتحليل <i class="fa-solid fa-spinner fa-spin text-dark-accent ml-2"></i>'; | |
| document.getElementById('compareStatus').innerHTML = '<i class="fa-solid fa-circle-notch fa-spin text-dark-accent"></i> جاري الحساب'; | |
| compareTimeout = setTimeout(async () => { | |
| const s1 = document.getElementById('selector-1').value; | |
| const s2 = document.getElementById('selector-2').value; | |
| const prop1 = dbProperties[s1]; | |
| const prop2 = dbProperties[s2]; | |
| if(!prop1 || !prop2) return; | |
| try { | |
| const response = await fetch('/api/property/compare', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ prop1: prop1, prop2: prop2 }) | |
| }); | |
| const result = await response.json(); | |
| if(result.success && result.data) { | |
| const data = result.data; | |
| document.getElementById('ai-opinion-text').innerHTML = data.summary.replace(/\n/g, '<br>'); | |
| document.getElementById('area-1').innerText = data.prop1.area; | |
| document.getElementById('rooms-1').innerText = data.prop1.rooms; | |
| document.getElementById('finish-1').innerText = data.prop1.finish; | |
| document.getElementById('roi-1').innerText = data.prop1.roi; | |
| document.getElementById('roi-1').dataset.val = extractArabicNum(data.prop1.roi); | |
| document.getElementById('area-2').innerText = data.prop2.area; | |
| document.getElementById('rooms-2').innerText = data.prop2.rooms; | |
| document.getElementById('finish-2').innerText = data.prop2.finish; | |
| document.getElementById('roi-2').innerText = data.prop2.roi; | |
| document.getElementById('roi-2').dataset.val = extractArabicNum(data.prop2.roi); | |
| // Highlight logic | |
| const r1 = parseFloat(document.getElementById('roi-1').dataset.val); | |
| const r2 = parseFloat(document.getElementById('roi-2').dataset.val); | |
| if(r1 > r2) { document.getElementById('roi-1').classList.add('highlight-win'); document.getElementById('roi-2').classList.add('highlight-lose'); } | |
| else if(r2 > r1) { document.getElementById('roi-2').classList.add('highlight-win'); document.getElementById('roi-1').classList.add('highlight-lose'); } | |
| // Update Chart | |
| updateChart(data.prop1, data.prop2); | |
| document.getElementById('compareStatus').innerHTML = '<i class="fa-solid fa-check text-green-400"></i> محدث'; | |
| } | |
| } catch(e) { | |
| document.getElementById('ai-opinion-text').innerHTML = '<span class="text-red-400">فشل الاتصال بمحرك الذكاء الاصطناعي.</span>'; | |
| document.getElementById('compareStatus').innerHTML = '<i class="fa-solid fa-xmark text-red-400"></i> خطأ'; | |
| } | |
| }, 1000); | |
| } | |
| document.addEventListener('DOMContentLoaded', initCompare); | |
| </script> | |
| </body> | |
| </html> | |