Spaces:
Sleeping
Sleeping
| <html lang="es"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>InmoGuard V49: Precision</title> | |
| <style> | |
| :root { --p: #0f172a; --a: #3b82f6; --s: #10b981; --d: #ef4444; --w: #f59e0b; --bg: #f8fafc; } | |
| body { font-family: 'Segoe UI', sans-serif; background: var(--bg); padding: 20px; color: #334155; } | |
| .container { max-width: 1450px; margin: 0 auto; } | |
| .header { text-align: center; margin-bottom: 25px; } | |
| .header h1 { margin: 0; color: var(--p); font-size: 2rem; } | |
| .dropzone { background: white; border: 2px dashed #cbd5e1; padding: 30px; text-align: center; cursor: pointer; border-radius: 10px; transition: 0.3s; } | |
| .dropzone:hover { border-color: var(--a); background: #eff6ff; } | |
| .btn-group { text-align: center; margin-top: 20px; display:flex; justify-content:center; gap:10px; flex-wrap:wrap; } | |
| button { padding: 12px 20px; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; color: white; font-size: 0.95rem; } | |
| .btn-go { background: var(--a); } .btn-word { background: var(--p); display: none; } | |
| .btn-pdf { background: var(--d); display: none; } .btn-json { background: #8b5cf6; display: none; } | |
| .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 20px; margin-top: 30px; display: none; } | |
| .card { background: white; padding: 20px; border-radius: 8px; border-top: 5px solid #94a3b8; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); } | |
| .card h3 { margin-top: 0; color: var(--p); border-bottom: 1px solid #e2e8f0; padding-bottom: 10px; font-size: 1.15rem; } | |
| .score-circle { width: 90px; height: 90px; border-radius: 50%; border: 8px solid #e2e8f0; display: flex; align-items: center; justify-content: center; margin: 0 auto; font-size: 1.8rem; font-weight: bold; color: var(--p); } | |
| .score-good { border-color: var(--s); color: var(--s); } .score-mid { border-color: var(--w); color: var(--w); } .score-bad { border-color: var(--d); color: var(--d); } | |
| .c-sae { border-top-color: var(--d); background: #fff5f5; } | |
| .c-inv { border-top-color: var(--s); background: #f0fdf4; } | |
| .c-vis { border-top-color: #8b5cf6; background: #f5f3ff; } | |
| .c-forense { border-top-color: #6366f1; background: #eef2ff; } | |
| .c-hist { border-top-color: var(--a); grid-column: span 2; } | |
| .c-vur { border-top-color: var(--w); grid-column: 1 / -1; } | |
| .c-res { border-top-color: var(--p); background: #e2e8f0; grid-column: 1 / -1; } | |
| .lbl { font-weight: bold; font-size: 0.85rem; color: #64748b; display: block; margin-top: 8px; text-transform: uppercase; } | |
| .val { font-weight: 500; font-size: 1rem; color: #1e293b; } | |
| .risk-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px; font-size: 0.9em; } | |
| .bar-bg { flex: 1; height: 8px; background: #e2e8f0; border-radius: 4px; margin-left: 10px; overflow: hidden; } | |
| .bar-fill { height: 100%; transition: width 0.5s; } | |
| .bg-low { background: var(--s); width: 33%; } .bg-med { background: var(--w); width: 66%; } .bg-high { background: var(--d); width: 100%; } | |
| .timeline { position: relative; margin-top: 20px; padding-left: 20px; border-left: 2px solid #e2e8f0; } | |
| .tl-item { margin-bottom: 20px; position: relative; padding-left: 20px; } | |
| .tl-dot { position: absolute; left: -26px; top: 0; width: 14px; height: 14px; border-radius: 50%; background: var(--a); border: 2px solid white; box-shadow: 0 0 0 2px var(--a); } | |
| .tl-date { font-size: 0.85rem; color: var(--a); font-weight: bold; } | |
| .tl-title { font-weight: bold; font-size: 1rem; color: var(--p); margin: 2px 0; } | |
| .tl-desc { font-size: 0.9rem; color: #64748b; } | |
| table { width: 100%; border-collapse: collapse; font-size: 0.9rem; margin-top: 10px; } | |
| th { text-align: left; padding: 8px; background: #f8fafc; color: #475569; border-bottom: 2px solid #e2e8f0; } | |
| td { padding: 8px; border-bottom: 1px solid #e2e8f0; } | |
| .tag-vig { color: #b91c1c; font-weight: bold; background: #fee2e2; padding: 2px 6px; border-radius: 4px; } | |
| .tag-cancel { color: #15803d; background: #dcfce7; padding: 2px 6px; border-radius: 4px; font-style: italic; } | |
| iframe { width: 100%; height: 250px; border: none; background: #e2e8f0; border-radius: 6px; } | |
| #status { text-align: center; margin-top: 15px; font-weight: bold; color: #64748b; } | |
| #err-box { background: #fee2e2; color: #b91c1c; padding: 15px; border-radius: 8px; margin-top: 20px; display: none; text-align: center; border: 1px solid #fecaca; } | |
| .sv-btn { display: inline-block; background: #fbbf24; color: #78350f; padding: 8px 16px; border-radius: 6px; font-size: 0.9rem; font-weight: bold; text-decoration: none; margin-top: 10px; cursor: pointer; border: 1px solid #f59e0b; transition: 0.2s; } | |
| .sv-btn:hover { background: #f59e0b; color: white; transform: translateY(-1px); } | |
| .audio-btn { background: var(--p); color: white; border: none; padding: 5px 10px; border-radius: 20px; cursor: pointer; font-size: 0.9rem; margin-left: 10px; } | |
| .chat-w { position: fixed; bottom: 30px; right: 30px; width: 320px; background: white; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.1); display: none; border: 1px solid #e2e8f0; z-index: 999; } | |
| .chat-h { background: var(--p); color: white; padding: 15px; border-radius: 12px 12px 0 0; cursor: pointer; } | |
| .chat-b { height: 250px; overflow-y: auto; padding: 15px; } | |
| .chat-f { padding: 10px; border-top: 1px solid #e2e8f0; } | |
| .chat-f input { width: 90%; padding: 8px; border: 1px solid #cbd5e1; border-radius: 6px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>🛡️ InmoGuard <span>AI V49</span></h1> | |
| <p style="color:#64748b">Análisis Catastral Preciso & Localización</p> | |
| </div> | |
| <div class="dropzone" onclick="document.getElementById('files').click()"> | |
| <h3>📂 Cargar Expediente + Fotos</h3> | |
| <p id="prev-txt">Sube PDFs y FOTOS del inmueble</p> | |
| <input type="file" id="files" multiple style="display:none" onchange="document.getElementById('prev-txt').innerText = `${this.files.length} Archivos`"> | |
| </div> | |
| <div class="btn-group"> | |
| <button class="btn-go" id="btnGo" onclick="run()">🔍 Analizar Todo</button> | |
| <button class="btn-word" id="btnWord" onclick="dl('word')">📝 Word</button> | |
| <button class="btn-pdf" id="btnPdf" onclick="dl('pdf')">📄 PDF</button> | |
| <button class="btn-json" id="btnJson" onclick="dl('json')">💾 JSON</button> | |
| <button onclick="toggleChat()" style="background:var(--p)">💬 Chat</button> | |
| </div> | |
| <div id="status"></div> | |
| <div id="err-box"></div> | |
| <div id="dash" class="grid"> | |
| <div class="card" style="border-top-color: #3b82f6;"> | |
| <h3>🏠 Identificación Legal</h3> | |
| <span class="lbl">FMI Detectados:</span> <span class="val" id="fmi" style="color:var(--a); font-weight:bold; display:block;"></span> | |
| <span class="lbl">Municipio / Depto:</span> <span class="val" id="muni"></span> | |
| <span class="lbl">Vereda / Barrio:</span> <span class="val" id="vereda"></span> | |
| <div style="margin-top:10px; padding:8px; background:#f8fafc; border-radius:6px; border:1px solid #e2e8f0;"> | |
| <span class="lbl" style="margin-top:0">Ref. Catastral Anterior:</span> <span class="val" id="ref_ant" style="font-size:0.9em; word-break:break-all;"></span> | |
| <span class="lbl">NUPRE:</span> <span class="val" id="nupre" style="font-weight:bold;"></span> | |
| <span class="lbl">Cédula Catastral:</span> <span class="val" id="cedula"></span> | |
| </div> | |
| <span class="lbl">Dirección:</span> <span class="val" id="dir"></span> | |
| </div> | |
| <div class="card"> | |
| <h3>📍 Ubicación Detectada</h3> | |
| <div id="loc-name" style="font-size:0.8em; color:#999; margin-bottom:5px"></div> | |
| <div id="map-box"></div> | |
| <div id="sv-container" style="text-align:center;"></div> | |
| </div> | |
| <div class="card"> | |
| <h3>🏆 InmoScore</h3> | |
| <div id="score-circle" class="score-circle">--</div> | |
| <div id="score-details" style="margin-top:15px; font-size:0.85rem; color:#64748b; text-align:center;"></div> | |
| </div> | |
| <div class="card c-inv"> | |
| <h3>💰 Inversión & Renta</h3> | |
| <span class="lbl">Venta Estimada:</span><h2 id="fin-venta" style="color:#166534; margin:2px 0;"></h2> | |
| <span class="lbl">Renta Estimada:</span><span class="val" id="fin-renta"></span> | |
| <span class="lbl">Cap Rate:</span><span class="val" id="fin-cap"></span> | |
| </div> | |
| <div class="card c-vis"> | |
| <h3>👁️ Inspección Visual AI</h3> | |
| <span class="lbl">Estado Físico:</span><span class="val" id="vis-est"></span> | |
| <span class="lbl">Observaciones:</span><p class="val" id="vis-obs" style="font-style:italic"></p> | |
| </div> | |
| <div class="card c-sae"> | |
| <h3>⚖️ SAE / Ley 1708</h3> | |
| <span class="lbl">Estado:</span> <span class="val" id="sae-st"></span> | |
| <span class="lbl">Concepto:</span> <p class="val" id="sae-leg" style="font-size:0.9rem; font-style:italic"></p> | |
| <span class="lbl">Viabilidad:</span> <span class="val" id="sae-via" style="font-weight:bold;"></span> | |
| </div> | |
| <div class="card c-forense"> | |
| <h3>🕵️♂️ Forense & Cruce</h3> | |
| <div id="forensic-list"></div> | |
| <span class="lbl" style="margin-top:10px">Inconsistencias:</span><span class="val" id="cruce-inc"></span> | |
| </div> | |
| <div class="card c-riesgo"><h3>🚥 Riesgos</h3><div id="risk-box"></div></div> | |
| <div class="card c-hist"><h3>📜 Línea de Tiempo</h3><div id="timeline-box" class="timeline"></div></div> | |
| <div class="card c-vur"> | |
| <h3>📑 VUR Detallado</h3> | |
| <div id="vur-table"></div> | |
| <span class="lbl" style="color:#c2410c; margin-top:10px;">Falsa Tradición:</span> <span class="val" id="falsa"></span> | |
| </div> | |
| <div class="card c-res" style="grid-column: 1 / -1; background:#e2e8f0; border-top-color:var(--p)"> | |
| <h3>🏁 Dictamen Final <button class="audio-btn" onclick="speak()">🔈</button></h3> | |
| <h2 id="res-t" style="text-align:center;"></h2> | |
| <p id="res-d" style="padding:0 15px; font-size:1.05rem; line-height:1.6"></p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="chat-w" id="chat"> | |
| <div class="chat-h" onclick="toggleChat()">💬 Chat</div> | |
| <div class="chat-b" id="msgs"></div> | |
| <div class="chat-f"><input id="q" placeholder="Pregunta..." onkeypress="if(event.key=='Enter')ask()"></div> | |
| </div> | |
| <script> | |
| let gData = null; | |
| function safeSet(id, val) { let el=document.getElementById(id); if(el) el.innerText=val||'---'; } | |
| function toggleChat() { let w=document.getElementById('chat'); w.style.display=w.style.display==='block'?'none':'block'; } | |
| async function run() { | |
| let f = document.getElementById('files').files; | |
| if(!f.length) return alert("Sube archivos"); | |
| document.getElementById('status').innerText = "⏳ Extrayendo Catastro y Geolocalizando..."; | |
| document.getElementById('btnGo').disabled = true; | |
| document.getElementById('err-box').style.display = 'none'; | |
| let fd = new FormData(); | |
| for(let x of f) fd.append('files', x); | |
| try { | |
| let req = await fetch('/analyze', { method: 'POST', body: fd }); | |
| let res = await req.json(); | |
| if(res.error) { | |
| document.getElementById('err-box').innerText = "❌ " + res.msg; | |
| document.getElementById('err-box').style.display = 'block'; | |
| document.getElementById('status').innerText = ""; | |
| } else { | |
| gData = res; | |
| render(res); | |
| document.getElementById('status').innerText = "✅ Listo"; | |
| } | |
| } catch(e) { | |
| document.getElementById('err-box').innerText = "❌ Error Red"; | |
| document.getElementById('err-box').style.display = 'block'; | |
| } | |
| document.getElementById('btnGo').disabled = false; | |
| } | |
| function render(d) { | |
| document.getElementById('dash').style.display = 'grid'; | |
| document.getElementById('btnPdf').style.display = 'inline-block'; | |
| document.getElementById('btnWord').style.display = 'inline-block'; | |
| document.getElementById('btnJson').style.display = 'inline-block'; | |
| const get = (o, k) => (o && o[k]) ? o[k] : '---'; | |
| const m=d.meta||{}, sae=d.analisis_sae_ley||{}, hist=d.historial_propiedad||[], fin=d.rentabilidad||{}, vis=d.inspeccion_visual||{}; | |
| const v=d.vur||{}, end=d.dic||{}, sem=d.semaforo_riesgos||{}, forense=d.forense_digital||[], cruce=d.cruce_documentos||{}, score=d.inmoscore||{}, ent=d.entorno_urbano||{}; | |
| // NEW FIELDS V49 | |
| safeSet('muni', `${get(m,'municipio')} / ${get(m,'departamento')}`); | |
| safeSet('vereda', get(m,'vereda')); | |
| safeSet('nupre', get(m,'nupre')); | |
| safeSet('ref_ant', get(m,'ref_catastral_ant')); | |
| let sc = score.puntaje || 0; | |
| let scDiv = document.getElementById('score-circle'); | |
| scDiv.innerText = sc; | |
| scDiv.className = `score-circle ${sc > 75 ? 'score-good' : (sc > 40 ? 'score-mid' : 'score-bad')}`; | |
| safeSet('score-details', (score.detalles || []).join(', ')); | |
| safeSet('fin-venta', get(fin, 'valor_venta')); | |
| safeSet('fin-renta', get(fin, 'valor_renta')); | |
| safeSet('fin-cap', get(fin, 'cap_rate')); | |
| safeSet('vis-est', get(vis, 'estado_fisico')); | |
| safeSet('vis-obs', get(vis, 'observaciones')); | |
| safeSet('fmi', get(m,'fmi_detected')); | |
| let ced = m.cedula_catastral; | |
| safeSet('cedula', (typeof ced === 'object') ? JSON.stringify(ced) : (ced || '---')); | |
| safeSet('dir', get(m,'dir_legal')); | |
| let fHtml = ""; | |
| forense.forEach(f => { | |
| let color = f.datos.riesgo === "ALTO" ? "red" : "green"; | |
| fHtml += `<div style="font-size:0.85em;"><b>${f.archivo}</b>: <span style="color:${color}">${f.datos.alerta}</span></div>`; | |
| }); | |
| document.getElementById('forensic-list').innerHTML = fHtml || "Sin datos."; | |
| safeSet('cruce-inc', get(cruce, 'inconsistencias')); | |
| let rHtml = ""; | |
| for(let k in sem) { | |
| let s = (sem[k]||"BAJO").toUpperCase(); | |
| let cls = s.includes("ALTO")?"bg-high":(s.includes("MEDIO")?"bg-med":"bg-low"); | |
| rHtml += `<div class="risk-row"><span>${k.toUpperCase()}</span><div class="bar-bg"><div class="bar-fill ${cls}"></div></div></div>`; | |
| } | |
| document.getElementById('risk-box').innerHTML = rHtml; | |
| safeSet('sae-st', get(sae,'estado_proceso')); | |
| safeSet('sae-via', get(sae,'viabilidad_comercializacion')); | |
| safeSet('sae-leg', get(sae,'fundamento_legal')); | |
| let tlHtml = ""; | |
| hist.forEach(h => { | |
| tlHtml += `<div class="tl-item"><div class="tl-dot"></div><div class="tl-date">${h.fecha||'S/F'}</div><div class="tl-title">${h.acto||'Registro'}</div><div class="tl-desc">${h.detalles||''}</div></div>`; | |
| }); | |
| document.getElementById('timeline-box').innerHTML = tlHtml || "<p>Sin historial.</p>"; | |
| safeSet('falsa', get(v,'falsa_tradicion')); | |
| let hv = "<table><tr><th>Nro</th><th>Desc</th><th>Estado</th></tr>"; | |
| (v.anotaciones_detalle||[]).forEach(x => { | |
| hv += `<tr><td>${x.nro}</td><td>${x.desc}</td><td>${x.estado}</td></tr>`; | |
| }); | |
| document.getElementById('vur-table').innerHTML = hv + "</table>"; | |
| safeSet('loc-name', d.ubicacion_detectada); | |
| if(d.mapa) { | |
| let fr = document.createElement('iframe'); fr.srcdoc = d.mapa; | |
| document.getElementById('map-box').innerHTML = ''; document.getElementById('map-box').appendChild(fr); | |
| if(d.coords && d.coords.lat) { | |
| let svUrl = `http://googleusercontent.com/maps.google.com/layer=c&cbll=${d.coords.lat},${d.coords.lon}`; | |
| document.getElementById('sv-container').innerHTML = `<a href="http://googleusercontent.com/maps.google.com/?q=${d.coords.lat},${d.coords.lon}" target="_blank" class="sv-btn">👁️ Ver en Street View</a>`; | |
| } else { | |
| document.getElementById('sv-container').innerHTML = ""; | |
| } | |
| } | |
| let r = get(end,'res'); | |
| let el = document.getElementById('res-t'); | |
| if(el) { el.innerText = r; el.style.color = r.includes('NO') ? 'var(--d)' : 'var(--s)'; } | |
| safeSet('res-d', get(end,'txt')); | |
| } | |
| function speak() { | |
| if(!gData || !gData.dic) return; | |
| let msg = new SpeechSynthesisUtterance("Dictamen: " + gData.dic.txt); | |
| msg.lang = 'es-ES'; window.speechSynthesis.speak(msg); | |
| } | |
| async function dl(t) { | |
| if(!gData) return; | |
| let ep = t === 'pdf' ? '/print-pdf' : (t === 'word' ? '/download-word' : '/download-json'); | |
| let req = await fetch(ep, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(gData)}); | |
| let b = await req.blob(); | |
| let url = window.URL.createObjectURL(b); | |
| let a = document.createElement('a'); a.href=url; a.download = `InmoGuard.${t === 'word' ? 'docx' : t}`; a.click(); | |
| } | |
| async function ask() { | |
| let q = document.getElementById('q').value; | |
| if(!q) return; | |
| document.getElementById('msgs').innerHTML += `<div style='text-align:right; margin:5px; background:#e2e8f0; padding:5px; border-radius:5px'>${q}</div>`; | |
| document.getElementById('q').value = ""; | |
| let req = await fetch('/ask', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({question:q})}); | |
| let res = await req.json(); | |
| document.getElementById('msgs').innerHTML += `<div style='text-align:left; margin:5px; background:var(--p); color:white; padding:5px; border-radius:5px'>${res.answer}</div>`; | |
| } | |
| </script> | |
| </body> | |
| </html> |