+ {/* Header */}
+
+
+ {/* Preset-intent quick buttons */}
+
+ {PRESETS.map((p) => (
+
+ ))}
+
+
+ {/* Message thread */}
+
+ {isEmpty && !mutation.isPending && (
+
+ Задайте вопрос или выберите тему выше.
+
+ )}
+
+ {thread.map((turn) =>
+ turn.role === "user" ? (
+
+ ) : (
+
+ ),
+ )}
+
+ {mutation.isPending && (
+
+ Отправляем…
+
+ )}
+
+ {mutation.isError && (
+
+
+
+ Не удалось отправить запрос. Проверьте соединение.
+
+
+
+ )}
+
+
+ {/* Composer */}
+
+
+ );
+}
+
+// ── Bubbles ────────────────────────────────────────────────────────────────────
+
+const visuallyHidden: React.CSSProperties = {
+ position: "absolute",
+ width: 1,
+ height: 1,
+ padding: 0,
+ margin: -1,
+ overflow: "hidden",
+ clip: "rect(0 0 0 0)",
+ whiteSpace: "nowrap",
+ border: 0,
+};
+
+function UserBubble({ content }: { content: string }) {
+ return (
+
+
+ {/* Badge row */}
+ {meta && (meta.advisory || isPending || !meta.llmUsed) && (
+
+ {isPending && (
+
+ Прогноз не рассчитан
+
+ )}
+ {meta.advisory && (
+ }
+ >
+ Advisory
+
+ )}
+ {!meta.llmUsed && (
+
+ Детерминированный ответ
+
+ )}
+
+ )}
+
+ {/* Answer — PLAIN TEXT (backend returns plain text; no HTML injection). */}
+
+ {content}
+
+
+ {/* grounded_in provenance line */}
+ {provenance && (
+
+ {provenance}
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/components/site-finder/__tests__/ChatPanel.test.tsx b/frontend/src/components/site-finder/__tests__/ChatPanel.test.tsx
new file mode 100644
index 00000000..b5b61a54
--- /dev/null
+++ b/frontend/src/components/site-finder/__tests__/ChatPanel.test.tsx
@@ -0,0 +1,149 @@
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import { ChatPanel } from "../ChatPanel";
+import type { ChatAskRequest, ChatAskResponse } from "@/lib/site-finder-api";
+
+// Mock the API client so the mutation resolves with a controlled response and we
+// can assert the request payload (intent / cad_num).
+const postChatAsk = vi.fn<(req: ChatAskRequest) => Promise