// Chat components: ChatMessage, ListingCard, TraceOverlay, TypingIndicator const { useState, useEffect, useRef } = React; // Tiny markdown renderer — handles **bold**, lists, and line breaks. // Good enough for our scripted responses; no external deps. function renderMarkdown(text) { if (!text) return null; const lines = text.split("\n"); const out = []; let listBuffer = []; const flushList = () => { if (listBuffer.length) { out.push(
    {listBuffer.map((item, i) => (
  1. {renderInline(item)}
  2. ))}
); listBuffer = []; } }; lines.forEach((line, i) => { const ol = line.match(/^\s*(\d+)\.\s+(.*)$/); const ul = line.match(/^\s*[-•]\s+(.*)$/); if (ol) { listBuffer.push(ol[2]); } else if (ul) { listBuffer.push(ul[1]); } else { flushList(); if (line.trim() === "") { out.push(
); } else { out.push(

{renderInline(line)}

); } } }); flushList(); return out; } function renderInline(text) { // **bold** segments const parts = text.split(/(\*\*[^*]+\*\*)/g); return parts.map((p, i) => p.startsWith("**") && p.endsWith("**") ? ( {p.slice(2, -2)} ) : ( {p} ) ); } function formatPriceFull(rub) { return rub.toLocaleString("ru-RU") + " ₽"; } function formatPriceShort(rub) { const mln = rub / 1_000_000; return mln.toFixed(mln < 10 ? 1 : 0).replace(".", ",") + " млн ₽"; } function ListingCard({ listing, layout, isActive, isHover, onClick, onHover, onDetails }) { const ppm = Math.round(listing.price / listing.area / 1000); return (
onHover && onHover(listing.id)} onMouseLeave={() => onHover && onHover(null)} onClick={() => onClick && onClick(listing.id)} >
{listing.id}
{listing.name}
{formatPriceShort(listing.price)}
{listing.rooms}-комн. · {listing.area} м² · {ppm} тыс/м²
{listing.district} · м. {listing.metro_name}, {listing.metro_walk_min} мин {listing.distance_km != null && ( <> · {listing.distance_km} км от центра )}
{layout !== "minimal" && (
)}
); } function TypingIndicator({ traceItems, showTrace }) { return (
{showTrace && traceItems && traceItems.length > 0 && (
{traceItems.map((t, i) => (
{t.done ? "✓" : "›"} {t.name} {formatArgs(t.args)}
))}
)}
); } function formatArgs(args) { if (!args) return ""; const entries = Object.entries(args).map(([k, v]) => { let val = typeof v === "string" ? `"${v}"` : Array.isArray(v) ? `[${v.join(",")}]` : v; return `${k}: ${val}`; }); return entries.join(", "); } function TraceBlock({ items }) { return (
tool calls
{items.map((t, i) => (
{t.name} {formatArgs(t.args)} {t.ms} ms
))}
); } Object.assign(window, { renderMarkdown, ListingCard, TypingIndicator, TraceBlock, formatPriceFull, formatPriceShort, });