// App.jsx — main shell tying chat + map + detail + tweaks together. const { useState: useState_a, useEffect: useEffect_a, useRef: useRef_a, useMemo: useMemo_a } = React; // Backend API. Same origin: FastAPI serves the frontend statically at /. const API_BASE = ""; function getOrCreateSessionId() { let sid = null; try { sid = localStorage.getItem("mesto_session_id"); } catch (_) {} if (!sid) { sid = (typeof crypto !== "undefined" && crypto.randomUUID) ? crypto.randomUUID() : `sid-${Math.random().toString(36).slice(2)}-${Date.now()}`; try { localStorage.setItem("mesto_session_id", sid); } catch (_) {} } return sid; } function decorateListing(l) { if (l && l.image_hue == null) { let h = 0; for (const c of (l.id || "")) h = (h * 31 + c.charCodeAt(0)) % 360; return { ...l, image_hue: h }; } return l; } const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "palette": "terracotta", "mapStyle": "warm", "cardLayout": "vertical", "showTrace": true, "mobile": false }/*EDITMODE-END*/; const PALETTES = { terracotta: { accent: "#C8553D", accentSoft: "#F2D7CD", accentInk: "#7A2A18", bg: "#FAF7F2", panel: "#FFFFFF", ink: "#1A1A1A", muted: "#6B6660", line: "rgba(26,26,26,0.08)" }, forest: { accent: "#3F6B4A", accentSoft: "#D4E2D5", accentInk: "#1F3D27", bg: "#F7F5EE", panel: "#FFFFFF", ink: "#1A1A1A", muted: "#6B6660", line: "rgba(26,26,26,0.08)" }, navy: { accent: "#234A8A", accentSoft: "#D4DEEF", accentInk: "#0F2752", bg: "#F6F4EF", panel: "#FFFFFF", ink: "#1A1A1A", muted: "#6B6660", line: "rgba(26,26,26,0.08)" }, mono: { accent: "#1A1A1A", accentSoft: "#E8E5DF", accentInk: "#1A1A1A", bg: "#FAF7F2", panel: "#FFFFFF", ink: "#1A1A1A", muted: "#6B6660", line: "rgba(26,26,26,0.08)" }, }; function applyPalette(name) { const p = PALETTES[name] || PALETTES.terracotta; const r = document.documentElement; Object.entries(p).forEach(([k, v]) => r.style.setProperty(`--${k}`, v)); } function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [messages, setMessages] = useState_a([]); // {role, content, listings?, trace?} const [activeListings, setActiveListings] = useState_a([]); // currently shown on map const [activeId, setActiveId] = useState_a(null); const [hoverId, setHoverId] = useState_a(null); const [input, setInput] = useState_a(""); const [pending, setPending] = useState_a(null); // {trace: [{name,args,ms,done}]} const [detail, setDetail] = useState_a(null); const [showResetConfirm, setShowResetConfirm] = useState_a(false); const chatScrollRef = useRef_a(null); const inputRef = useRef_a(null); const sessionId = useMemo_a(getOrCreateSessionId, []); // Apply palette useEffect_a(() => { applyPalette(t.palette); }, [t.palette]); // Autoscroll chat useEffect_a(() => { const el = chatScrollRef.current; if (el) el.scrollTop = el.scrollHeight; }, [messages, pending]); const send = (text) => { const trimmed = text.trim(); if (!trimmed || pending) return; setMessages((m) => [...m, { role: "user", content: trimmed }]); setInput(""); runAgent(trimmed); }; const runAgent = async (userText) => { setPending({ trace: [] }); try { const res = await fetch(`${API_BASE}/api/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: sessionId, message: userText }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); const listings = (data.listings || []).map(decorateListing); setMessages((m) => [ ...m, { role: "assistant", content: data.response || "(пустой ответ)", listings, trace: data.tool_calls || [], }, ]); if (listings.length > 0) { setActiveListings(listings); } } catch (err) { setMessages((m) => [ ...m, { role: "assistant", content: `Ошибка связи с сервером: ${err.message}`, listings: [], trace: [], }, ]); } finally { setPending(null); } }; const handleReset = async () => { try { await fetch(`${API_BASE}/api/chat/reset`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_id: sessionId }), }); } catch (_) { // best-effort: историю UI всё равно чистим } setMessages([]); setActiveListings([]); setActiveId(null); setDetail(null); setPending(null); setShowResetConfirm(false); }; const onDetails = (listing) => { setDetail(listing); send(`Расскажи подробнее про ${listing.id}`); }; const onCardClick = (id) => { setActiveId(id === activeId ? null : id); }; const isEmpty = messages.length === 0 && !pending; return (
{/* LEFT: Chat */}