const { useState, useEffect, useRef, useCallback } = React; // ── helpers ────────────────────────────────────────────────────────────────── function fmt(n) { if (n == null) return "—"; return Number(n).toLocaleString("es-AR"); } // ── ListingCard ─────────────────────────────────────────────────────────────── function ListingCard({ listing, onSimilar }) { return (
{listing.image_url ? {listing.title} :
Sin foto
}
{listing.link ? {listing.title || "Ver propiedad"} : listing.title || "—"}
{listing.barrio || ""} {listing.propiedad && ( {listing.propiedad} )}
USD {fmt(listing.price)} {(listing.surface || listing.m2c) && ( {" · "}{listing.surface || listing.m2c} m² )}
{listing.id && ( )}
); } function ListingGrid({ listings, onSimilar, emptyMsg = "Sin resultados." }) { if (!listings.length) return

{emptyMsg}

; return (
{listings.map((l, i) => )}
); } // ── PriceChart ──────────────────────────────────────────────────────────────── function PriceChart({ data }) { const ref = useRef(null); const chartRef = useRef(null); useEffect(() => { if (!data || !ref.current) return; if (chartRef.current) chartRef.current.destroy(); chartRef.current = new Chart(ref.current, { type: "line", data: { labels: data.surfaces, datasets: [ { label: "Mediana", data: data.median, borderColor: "#1a1a2e", tension: 0.3, pointRadius: 0 }, { label: "md+std", data: data.md_plus_std, borderColor: "#7986cb", borderDash: [4,4], tension: 0.3, pointRadius: 0 }, { label: "md-std", data: data.md_minus_std,borderColor: "#7986cb", borderDash: [4,4], tension: 0.3, pointRadius: 0 }, { label: "Mín", data: data.min, borderColor: "#ccc", borderDash: [2,4], tension: 0.3, pointRadius: 0 }, { label: "Máx", data: data.max, borderColor: "#ccc", borderDash: [2,4], tension: 0.3, pointRadius: 0 }, ], }, options: { responsive: true, plugins: { legend: { position: "bottom" } }, scales: { x: { title: { display: true, text: "m² cubiertos" } }, y: { title: { display: true, text: "Precio USD" } }, }, }, }); return () => chartRef.current?.destroy(); }, [data]); return ; } // ── SimilarPortfolio ────────────────────────────────────────────────────────── function SimilarPortfolio({ listing, onBack }) { const [results, setResults] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [current, setCurrent] = useState(listing); const load = useCallback(async (l) => { setLoading(true); setError(null); try { const res = await fetch(`/similar/${l.id}?top_k=20`); if (!res.ok) throw new Error(`Error ${res.status}`); setResults(await res.json()); } catch (e) { setError(e.message); } finally { setLoading(false); } }, []); useEffect(() => { load(listing); }, [listing]); const handleSimilar = (l) => { setCurrent(l); load(l); }; return (
Similares a: {current.title || current.id}
{loading &&

Buscando similares…

} {error &&

{error}

} {!loading && !error && ( )}
); } // ── Constants ───────────────────────────────────────────────────────────────── const BARRIOS = ["", "nuñez", "colegiales", "villa crespo", "villa urquiza", "san isidro", "paternal"]; const PROPIEDADES = [ { value: "", label: "Tipo" }, { value: "D", label: "Depto" }, { value: "C", label: "Casa" }, { value: "P", label: "PH" }, ]; // ── Main search panel ───────────────────────────────────────────────────────── function SearchPanel({ onSimilar }) { const [query, setQuery] = useState(""); const [filters, setFilters] = useState({ barrio: "", propiedad: "", min_price: "", max_price: "", min_m2c: "", max_m2c: "", min_antiguedad: "", max_antiguedad: "", }); const [results, setResults] = useState([]); const [chartData, setChartData] = useState(null); const [total, setTotal] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const setFilter = (k) => (e) => setFilters(f => ({ ...f, [k]: e.target.value })); const numericFilters = () => { const n = {}; ["min_price","max_price","min_m2c","max_m2c","min_antiguedad","max_antiguedad"].forEach(k => { if (filters[k] !== "") n[k] = Number(filters[k]); }); if (filters.barrio) n.barrio = filters.barrio; if (filters.propiedad) n.propiedad = filters.propiedad; return n; }; const handleSearch = useCallback(async () => { setLoading(true); setError(null); setChartData(null); try { if (query.trim()) { // NLP search — filters applied server-side const payload = { query, top_k: 20, ...numericFilters() }; const res = await fetch("/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error(`Error ${res.status}`); const results = await res.json(); setResults(results); setTotal(results.length); } else { // No query — use filter endpoint (also shows chart) const body = new URLSearchParams(); Object.entries(filters).forEach(([k, v]) => v !== "" && body.append(k, v)); const res = await fetch("/filter_json", { method: "POST", body }); if (!res.ok) throw new Error(`Error ${res.status}`); const data = await res.json(); setChartData(data); setResults(data.matches || []); setTotal(data.total_matches); } } catch (e) { setError(e.message); } finally { setLoading(false); } }, [query, filters]); const onKey = (e) => e.key === "Enter" && handleSearch(); return (
{/* Search bar + filters row */}
{/* NLP search box */}
setQuery(e.target.value)} onKeyDown={onKey} placeholder="Ej: departamento luminoso en palermo 3 ambientes con balcón" />
{/* Barrio */}
{/* Tipo */}
{/* Price range */}
{/* Surface range */}
{/* Search button */}
{error &&

{error}

} {total != null && (

{total} resultado{total !== 1 ? "s" : ""} {query.trim() ? " · filtros aplicados sobre búsqueda semántica" : ""}

)}
{/* Chart (only shown when no NLP query) */} {chartData && (
)} {/* Results */}
); } // ── App ─────────────────────────────────────────────────────────────────────── function App() { const [view, setView] = useState("main"); const [similarListing, setSimilarListing] = useState(null); const handleSimilar = (listing) => { setSimilarListing(listing); setView("similar"); window.scrollTo(0, 0); }; if (view === "similar" && similarListing) { return (
setView("main")} />
); } return ; } ReactDOM.createRoot(document.getElementById("root")).render();