const { useMemo, useRef, useState, useEffect } = React;
function normalizeInputMessage(raw) {
return (raw || "")
.replace(/\r\n/g, "\n")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
function clip(value, max = 80) {
if (!value || value.length <= max) {
return value || "";
}
return `${value.slice(0, max - 3)}...`;
}
function titleCase(text) {
return (text || "")
.split(" ")
.filter(Boolean)
.map((token) => token.charAt(0).toUpperCase() + token.slice(1))
.join(" ");
}
function inferTitleFromPath(sourcePath) {
const raw = (sourcePath || "").split("/").pop() || "";
const withoutExt = raw.replace(/\.md$/i, "");
if (!withoutExt) {
return "";
}
const segments = withoutExt.split("_").filter(Boolean);
let core = segments.length > 1 ? segments[1] : segments[0];
const platform = segments.length > 2 ? segments[segments.length - 1] : "";
core = core.replace(/[-_]+/g, " ").trim();
const prettyCore = titleCase(core);
if (platform && /^[a-z0-9-]+$/i.test(platform)) {
return `${prettyCore} (${titleCase(platform.replace(/-/g, " "))})`;
}
return prettyCore;
}
function formatReferenceLabel(item, index) {
const heading = (item.heading || "").trim();
let label = "";
if (heading && heading.toLowerCase() !== "unknown heading") {
const parts = heading
.split(">")
.map((part) => part.trim())
.filter(Boolean);
if (parts.length >= 2) {
label = `${parts[0]} - ${parts[1]}`;
} else if (parts.length === 1) {
label = parts[0];
}
}
if (!label) {
label = inferTitleFromPath(item.source_path);
}
if (!label) {
label = `Reference ${index}`;
}
return `[${index}] ${clip(label, 64)}`;
}
function dedupeCitations(citations) {
const seen = new Set();
const list = [];
(citations || []).forEach((item) => {
const key = `${item.source_link || ""}::${item.heading || ""}`;
if (seen.has(key)) {
return;
}
seen.add(key);
list.push(item);
});
return list;
}
function TypingBubble() {
return (
);
}
function MessageItem({ turn, meta, index }) {
const isUser = turn.role === "user";
const citations = useMemo(() => dedupeCitations(meta?.citations), [meta?.citations]);
return (
{(turn.content || "").trim()}
{!isUser && citations.length > 0 && (
References
{Number.isFinite(meta?.latency_ms) && (
latency {meta.latency_ms} ms
)}
)}
{isUser ? "You" : "DocAgent"} {isUser ? "" : `• #${index + 1}`}
);
}
function ChatApp() {
const [sessionId, setSessionId] = useState(null);
const [input, setInput] = useState("");
const [history, setHistory] = useState([]);
const [assistantMetaHistory, setAssistantMetaHistory] = useState([]);
const [sending, setSending] = useState(false);
const listRef = useRef(null);
useEffect(() => {
if (listRef.current) {
listRef.current.scrollTop = listRef.current.scrollHeight;
}
}, [history, sending]);
function alignAssistantMeta(nextHistory, appendedMeta) {
const nextAssistantCount = nextHistory.filter((turn) => turn.role === "assistant").length;
let nextMeta = [...assistantMetaHistory];
if (typeof appendedMeta !== "undefined") {
nextMeta = [...nextMeta, appendedMeta];
}
if (nextAssistantCount === 0) {
return [];
}
if (nextMeta.length > nextAssistantCount) {
return nextMeta.slice(-nextAssistantCount);
}
if (nextMeta.length < nextAssistantCount) {
return [
...Array.from({ length: nextAssistantCount - nextMeta.length }, () => null),
...nextMeta,
];
}
return nextMeta;
}
async function callApi(path, body) {
const resp = await fetch(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!resp.ok) {
const text = await resp.text();
throw new Error(`HTTP ${resp.status}: ${text}`);
}
return resp.json();
}
async function sendMessage(event) {
if (event) {
event.preventDefault();
}
const message = normalizeInputMessage(input);
if (!message || sending) {
return;
}
const optimisticHistory = [...history, { role: "user", content: message }];
setHistory(optimisticHistory);
setInput("");
setSending(true);
try {
const data = await callApi("/api/chat", {
message,
session_id: sessionId,
});
const nextHistory = Array.isArray(data.history) ? data.history : optimisticHistory;
setSessionId(data.session_id || sessionId);
setHistory(nextHistory);
setAssistantMetaHistory(
alignAssistantMeta(nextHistory, {
citations: data.citations || [],
latency_ms: data.latency_ms,
})
);
} catch (err) {
const failHistory = [
...optimisticHistory,
{ role: "assistant", content: `Request failed: ${err.message}` },
];
setHistory(failHistory);
setAssistantMetaHistory(alignAssistantMeta(failHistory, null));
} finally {
setSending(false);
}
}
async function resetSession() {
if (sending) {
return;
}
try {
if (sessionId) {
await callApi("/api/chat/reset", { session_id: sessionId });
}
setSessionId(null);
setHistory([]);
setAssistantMetaHistory([]);
} catch {}
}
let assistantIndex = 0;
const showLanding = history.length === 0 && !sending;
const composer = (
);
return (
{showLanding ? (
What can I help with?
{composer}
) : (
<>
{history.map((turn, idx) => {
const meta =
turn.role === "assistant" ? assistantMetaHistory[assistantIndex++] : null;
return (
);
})}
{sending && }
{composer}
>
)}
);
}
const root = ReactDOM.createRoot(document.getElementById("chat-root"));
root.render();