ImmoGuardIAAvanzado / index.html
jcalbornoz's picture
Update index.html
6bd2ca3 verified
<!DOCTYPE html>
<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>