decodingdatascience's picture
Upload 15 files
8bd78d1 verified
Raw
History Blame Contribute Delete
27.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI Research Paper Explainer</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = { darkMode: "class" };
</script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
.drop-zone-active {
border-color: #6366f1 !important;
background-color: #eef2ff !important;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.25);
}
.dark .drop-zone-active {
background-color: #1e1b4b !important;
}
</style>
</head>
<body class="min-h-screen bg-stone-100 text-stone-900 antialiased transition-colors dark:bg-stone-950 dark:text-stone-100">
<div class="mx-auto flex min-h-screen max-w-3xl flex-col px-4 py-6">
<!-- Header -->
<header class="mb-5 flex flex-shrink-0 items-center justify-between">
<div>
<h1 class="font-serif text-2xl font-bold tracking-tight text-stone-800 dark:text-stone-100 md:text-3xl">
AI Research Paper Explainer
</h1>
<p class="mt-0.5 text-sm text-stone-500 dark:text-stone-400">
Powered by Gemini 2.5 Pro Β· Google ADK
</p>
</div>
<div class="flex items-center gap-2">
<button
type="button"
id="theme-toggle"
class="rounded-lg border border-stone-300 bg-white px-3 py-2 text-sm text-stone-600 shadow-sm transition hover:bg-stone-50 dark:border-stone-600 dark:bg-stone-800 dark:text-stone-300 dark:hover:bg-stone-700"
title="Toggle dark mode"
>
<span id="theme-toggle-label">Dark</span>
</button>
<button
type="button"
id="new-chat-btn"
class="rounded-lg border border-stone-300 bg-white px-3 py-2 text-sm text-stone-600 shadow-sm transition hover:bg-stone-50 dark:border-stone-600 dark:bg-stone-800 dark:text-stone-300 dark:hover:bg-stone-700"
>
New chat
</button>
</div>
</header>
<!-- Chat panel -->
<div class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-2xl border border-stone-200 bg-white shadow-sm dark:border-stone-700 dark:bg-stone-900">
<!-- Message list -->
<div
id="chat-messages"
class="flex-1 space-y-5 overflow-y-auto p-5"
aria-live="polite"
>
<div id="chat-empty" class="flex flex-col items-center justify-center py-16 text-center">
<div class="mb-3 flex h-14 w-14 items-center justify-center rounded-full bg-indigo-50 dark:bg-indigo-950">
<svg class="h-7 w-7 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
</svg>
</div>
<p class="text-sm font-medium text-stone-700 dark:text-stone-300">Upload a paper and start asking questions</p>
<p class="mt-1 text-xs text-stone-400 dark:text-stone-500">
Press <kbd class="rounded bg-stone-100 px-1.5 py-0.5 text-stone-600 dark:bg-stone-800 dark:text-stone-300">Enter</kbd> to send &nbsp;Β·&nbsp;
<kbd class="rounded bg-stone-100 px-1.5 py-0.5 text-stone-600 dark:bg-stone-800 dark:text-stone-300">Shift+Enter</kbd> for a new line
</p>
</div>
</div>
<!-- Typing indicator -->
<div
id="typing-indicator"
class="hidden border-t border-stone-100 px-5 py-2.5 text-sm text-stone-500 dark:border-stone-800 dark:text-stone-400"
>
<span class="inline-flex items-center gap-2">
<span class="flex gap-0.5">
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-indigo-400 [animation-delay:-0.3s]"></span>
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-indigo-400 [animation-delay:-0.15s]"></span>
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-indigo-400"></span>
</span>
Agent is thinking…
</span>
</div>
<!-- Input area -->
<form id="chat-form" class="border-t border-stone-200 bg-stone-50 p-4 dark:border-stone-700 dark:bg-stone-900/60" novalidate>
<!-- PDF drop zone (hidden after first turn) -->
<div id="pdf-zone-wrap" class="mb-3">
<div
id="pdf-drop-zone"
class="relative flex cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed border-stone-300 bg-white px-4 py-5 transition dark:border-stone-600 dark:bg-stone-800 hover:border-indigo-400 dark:hover:border-indigo-500"
>
<!-- Default (no file selected) -->
<div id="pdf-drop-default" class="flex flex-col items-center gap-1 text-center">
<svg class="h-8 w-8 text-stone-400 dark:text-stone-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5" />
</svg>
<p class="text-sm font-medium text-stone-600 dark:text-stone-300">
Drag &amp; drop your PDF here, or <span class="text-indigo-600 underline dark:text-indigo-400">click to upload</span>
</p>
<p class="text-xs text-stone-400 dark:text-stone-500">One PDF per conversation Β· Max 25 MB</p>
</div>
<!-- File selected state -->
<div id="pdf-drop-selected" class="hidden w-full items-center justify-between gap-3">
<div class="flex min-w-0 items-center gap-2">
<svg class="h-5 w-5 shrink-0 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
</svg>
<span id="pdf-filename" class="truncate text-sm font-medium text-stone-700 dark:text-stone-200"></span>
</div>
<button
type="button"
id="pdf-clear-btn"
class="shrink-0 rounded-full p-1 text-stone-400 transition hover:bg-stone-100 hover:text-stone-600 dark:hover:bg-stone-700 dark:hover:text-stone-200"
title="Remove file"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Hidden file input -->
<input
type="file"
id="pdf-input"
accept=".pdf,application/pdf"
class="absolute inset-0 cursor-pointer opacity-0"
/>
</div>
</div>
<!-- Locked PDF banner (shown after first turn) -->
<div id="pdf-locked-banner" class="mb-3 hidden items-center gap-2 rounded-xl border border-stone-200 bg-stone-50 px-3 py-2 text-xs text-stone-500 dark:border-stone-700 dark:bg-stone-800 dark:text-stone-400">
<svg class="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
</svg>
<span id="pdf-locked-name"></span>
<span class="text-stone-400 dark:text-stone-500">Β· Click <strong>New chat</strong> to use a different paper.</span>
</div>
<!-- Text input + send -->
<div class="flex items-end gap-2">
<textarea
id="chat-input"
rows="2"
class="min-h-[4rem] flex-1 resize-none rounded-xl border border-stone-300 bg-white px-3.5 py-2.5 text-sm text-stone-800 shadow-inner placeholder:text-stone-400 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/25 dark:border-stone-600 dark:bg-stone-800 dark:text-stone-100 dark:placeholder:text-stone-500"
placeholder="Ask about the paper…"
></textarea>
<button
type="submit"
id="send-btn"
class="inline-flex h-10 shrink-0 items-center justify-center gap-1.5 rounded-xl bg-indigo-600 px-4 text-sm font-semibold text-white shadow transition hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-stone-50 disabled:cursor-not-allowed disabled:opacity-50 dark:focus:ring-offset-stone-900"
>
<span id="send-label">Send</span>
<span
id="send-spinner"
class="hidden h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent"
aria-hidden="true"
></span>
</button>
</div>
<p id="validation-msg" class="mt-2 hidden text-xs text-red-600 dark:text-red-400" role="alert"></p>
</form>
</div>
</div>
<!-- Lightbox -->
<div
id="lightbox"
class="fixed inset-0 z-[100] hidden cursor-zoom-out items-center justify-center bg-black/85 p-4"
role="dialog"
aria-modal="true"
aria-label="Enlarged image"
>
<button
type="button"
id="lightbox-close"
class="absolute right-4 top-4 rounded-full bg-white/10 px-3 py-1 text-sm text-white hover:bg-white/20"
>
Close
</button>
<img id="lightbox-img" src="" alt="" class="max-h-[92vh] max-w-full rounded object-contain shadow-2xl" />
</div>
<script>
// TODO: After deploying the backend to Cloud Run, replace this with your Cloud Run HTTPS URL.
const BACKEND_URL = "/api/explain";
const SESSION_KEY = "research-explainer-session-id";
const THEME_KEY = "research-explainer-theme";
const chatMessages = document.getElementById("chat-messages");
const chatForm = document.getElementById("chat-form");
const chatInput = document.getElementById("chat-input");
const sendBtn = document.getElementById("send-btn");
const sendLabel = document.getElementById("send-label");
const sendSpinner = document.getElementById("send-spinner");
const typingIndicator = document.getElementById("typing-indicator");
const validationMsg = document.getElementById("validation-msg");
const newChatBtn = document.getElementById("new-chat-btn");
const pdfInput = document.getElementById("pdf-input");
const pdfDropZone = document.getElementById("pdf-drop-zone");
const pdfZoneWrap = document.getElementById("pdf-zone-wrap");
const pdfDropDefault = document.getElementById("pdf-drop-default");
const pdfDropSelected = document.getElementById("pdf-drop-selected");
const pdfFilename = document.getElementById("pdf-filename");
const pdfClearBtn = document.getElementById("pdf-clear-btn");
const pdfLockedBanner = document.getElementById("pdf-locked-banner");
const pdfLockedName = document.getElementById("pdf-locked-name");
const lightbox = document.getElementById("lightbox");
const lightboxImg = document.getElementById("lightbox-img");
const lightboxClose = document.getElementById("lightbox-close");
const themeToggle = document.getElementById("theme-toggle");
const themeToggleLabel = document.getElementById("theme-toggle-label");
var hasCompletedFirstTurn = false;
const mdClasses =
"prose-msg space-y-2 leading-relaxed text-stone-800 dark:text-stone-100 [&_a]:text-indigo-600 dark:[&_a]:text-indigo-400 [&_a]:underline [&_code]:rounded [&_code]:bg-stone-100 dark:[&_code]:bg-stone-900 [&_code]:px-1 [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:bg-stone-100 dark:[&_pre]:bg-stone-900 [&_pre]:p-3 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 [&_h1]:text-lg [&_h2]:text-base [&_h3]:text-sm [&_blockquote]:border-l-4 [&_blockquote]:border-stone-300 dark:[&_blockquote]:border-stone-600 [&_blockquote]:pl-4 [&_blockquote]:italic";
// ── Theme ──────────────────────────────────────────────────────────────
function isDarkMode() { return document.documentElement.classList.contains("dark"); }
function syncThemeUi() {
var dark = isDarkMode();
themeToggleLabel.textContent = dark ? "Light" : "Dark";
}
function applyStoredTheme() {
if (localStorage.getItem(THEME_KEY) === "dark") {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
syncThemeUi();
}
applyStoredTheme();
themeToggle.addEventListener("click", function () {
document.documentElement.classList.toggle("dark");
localStorage.setItem(THEME_KEY, isDarkMode() ? "dark" : "light");
syncThemeUi();
});
// ── Session ────────────────────────────────────────────────────────────
function getSessionId() {
var id = sessionStorage.getItem(SESSION_KEY);
if (!id) {
id = typeof crypto !== "undefined" && crypto.randomUUID
? crypto.randomUUID()
: "sess-" + Date.now() + "-" + String(Math.random()).slice(2, 10);
sessionStorage.setItem(SESSION_KEY, id);
}
return id;
}
// ── PDF zone state ─────────────────────────────────────────────────────
function showFileSelected(name) {
pdfDropDefault.classList.add("hidden");
pdfDropSelected.classList.remove("hidden");
pdfDropSelected.classList.add("flex");
pdfFilename.textContent = name;
}
function showFileDefault() {
pdfDropDefault.classList.remove("hidden");
pdfDropSelected.classList.add("hidden");
pdfDropSelected.classList.remove("flex");
pdfFilename.textContent = "";
}
function lockPdfZone(name) {
pdfZoneWrap.classList.add("hidden");
pdfLockedBanner.classList.remove("hidden");
pdfLockedBanner.classList.add("flex");
pdfLockedName.textContent = name || "PDF attached";
}
function unlockPdfZone() {
pdfZoneWrap.classList.remove("hidden");
pdfLockedBanner.classList.add("hidden");
pdfLockedBanner.classList.remove("flex");
showFileDefault();
}
pdfClearBtn.addEventListener("click", function (e) {
e.stopPropagation();
pdfInput.value = "";
showFileDefault();
});
pdfInput.addEventListener("change", function () {
if (pdfInput.files && pdfInput.files[0]) {
showFileSelected(pdfInput.files[0].name);
} else {
showFileDefault();
}
});
// Drag-and-drop
["dragenter", "dragover"].forEach(function (evt) {
pdfDropZone.addEventListener(evt, function (e) {
e.preventDefault();
pdfDropZone.classList.add("drop-zone-active");
});
});
["dragleave", "drop"].forEach(function (evt) {
pdfDropZone.addEventListener(evt, function (e) {
e.preventDefault();
pdfDropZone.classList.remove("drop-zone-active");
});
});
pdfDropZone.addEventListener("drop", function (e) {
var files = e.dataTransfer && e.dataTransfer.files;
if (files && files[0]) {
var dt = new DataTransfer();
dt.items.add(files[0]);
pdfInput.files = dt.files;
showFileSelected(files[0].name);
}
});
// ── New chat ───────────────────────────────────────────────────────────
function startNewChat() {
sessionStorage.removeItem(SESSION_KEY);
hasCompletedFirstTurn = false;
pdfInput.value = "";
unlockPdfZone();
chatMessages.innerHTML = "";
var empty = document.createElement("div");
empty.id = "chat-empty";
empty.className = "flex flex-col items-center justify-center py-16 text-center";
empty.innerHTML =
'<div class="mb-3 flex h-14 w-14 items-center justify-center rounded-full bg-indigo-50 dark:bg-indigo-950">' +
'<svg class="h-7 w-7 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" /></svg>' +
'</div>' +
'<p class="text-sm font-medium text-stone-700 dark:text-stone-300">Upload a paper and start asking questions</p>' +
'<p class="mt-1 text-xs text-stone-400 dark:text-stone-500">Press <kbd class="rounded bg-stone-100 px-1.5 py-0.5 text-stone-600 dark:bg-stone-800 dark:text-stone-300">Enter</kbd> to send &nbsp;Β·&nbsp; <kbd class="rounded bg-stone-100 px-1.5 py-0.5 text-stone-600 dark:bg-stone-800 dark:text-stone-300">Shift+Enter</kbd> for a new line</p>';
chatMessages.appendChild(empty);
chatInput.focus();
}
newChatBtn.addEventListener("click", startNewChat);
// ── Loading state ──────────────────────────────────────────────────────
function scrollToBottom() { chatMessages.scrollTop = chatMessages.scrollHeight; }
function setLoading(on) {
sendBtn.disabled = on;
chatInput.disabled = on;
pdfInput.disabled = on;
if (on) {
sendSpinner.classList.remove("hidden");
sendLabel.textContent = "…";
typingIndicator.classList.remove("hidden");
} else {
sendSpinner.classList.add("hidden");
sendLabel.textContent = "Send";
typingIndicator.classList.add("hidden");
}
scrollToBottom();
}
// ── Chat bubbles ───────────────────────────────────────────────────────
function appendUserBubble(text, withPdf) {
var emptyEl = document.getElementById("chat-empty");
if (emptyEl) emptyEl.remove();
var wrap = document.createElement("div");
wrap.className = "flex justify-end";
var bubble = document.createElement("div");
bubble.className =
"max-w-[82%] rounded-2xl rounded-br-md bg-indigo-600 px-4 py-2.5 text-sm text-white shadow-sm dark:bg-indigo-700";
bubble.textContent = text || "(PDF only)";
if (withPdf) {
var note = document.createElement("div");
note.className = "mt-1.5 flex items-center gap-1 border-t border-indigo-400/50 pt-1.5 text-xs text-indigo-200";
note.innerHTML = '<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"/></svg> PDF attached';
bubble.appendChild(note);
}
wrap.appendChild(bubble);
chatMessages.appendChild(wrap);
scrollToBottom();
}
function appendAssistantBubble(html, images) {
var wrap = document.createElement("div");
wrap.className = "flex justify-start";
var bubble = document.createElement("div");
bubble.className =
"max-w-[88%] rounded-2xl rounded-bl-md border border-stone-200 bg-stone-50 px-4 py-3 text-sm shadow-sm dark:border-stone-700 dark:bg-stone-800";
var textEl = document.createElement("div");
textEl.className = mdClasses;
textEl.innerHTML = html;
bubble.appendChild(textEl);
if (images && images.length > 0) {
var grid = document.createElement("div");
grid.className = "mt-3 grid gap-3 sm:grid-cols-2";
images.forEach(function (src, i) {
if (!src) return;
var img = document.createElement("img");
img.src = src;
img.alt = "Diagram " + (i + 1);
img.className =
"diagram-img max-h-80 w-full cursor-zoom-in rounded-xl border border-stone-200 bg-white object-contain shadow-sm transition hover:ring-2 hover:ring-indigo-300 dark:border-stone-600 dark:bg-stone-900 dark:hover:ring-indigo-500";
img.loading = "lazy";
img.title = "Click to enlarge";
grid.appendChild(img);
});
bubble.appendChild(grid);
}
wrap.appendChild(bubble);
chatMessages.appendChild(wrap);
scrollToBottom();
}
function appendErrorBubble(msg) {
var safe = String(msg).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
var wrap = document.createElement("div");
wrap.className = "flex justify-start";
var bubble = document.createElement("div");
bubble.className =
"max-w-[88%] rounded-2xl rounded-bl-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800 shadow-sm dark:border-red-900 dark:bg-red-950/40 dark:text-red-300";
bubble.innerHTML = "<p>" + safe + "</p>";
wrap.appendChild(bubble);
chatMessages.appendChild(wrap);
scrollToBottom();
}
// ── Lightbox ───────────────────────────────────────────────────────────
chatMessages.addEventListener("click", function (e) {
if (e.target && e.target.classList && e.target.classList.contains("diagram-img")) {
lightboxImg.src = e.target.src;
lightboxImg.alt = e.target.alt || "Diagram";
lightbox.classList.remove("hidden");
lightbox.classList.add("flex");
}
});
function closeLightbox() {
lightbox.classList.add("hidden");
lightbox.classList.remove("flex");
lightboxImg.src = "";
}
lightboxClose.addEventListener("click", closeLightbox);
lightbox.addEventListener("click", function (e) {
if (e.target === lightbox || e.target === lightboxImg) closeLightbox();
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !lightbox.classList.contains("hidden")) closeLightbox();
});
// ── Keyboard shortcut ──────────────────────────────────────────────────
chatInput.addEventListener("keydown", function (e) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
if (!sendBtn.disabled) chatForm.requestSubmit();
}
});
// ── Submit ─────────────────────────────────────────────────────────────
function formatDetail(detail) {
if (detail == null) return "";
if (typeof detail === "string") return detail;
if (Array.isArray(detail)) return detail.map(function (x) { return x && x.msg ? x.msg : JSON.stringify(x); }).join("; ");
return JSON.stringify(detail);
}
chatForm.addEventListener("submit", async function (e) {
e.preventDefault();
validationMsg.classList.add("hidden");
var text = chatInput.value.trim();
var file = !hasCompletedFirstTurn && pdfInput.files && pdfInput.files[0] ? pdfInput.files[0] : null;
if (!text && !file) {
validationMsg.textContent = "Enter a message and/or attach a PDF.";
validationMsg.classList.remove("hidden");
return;
}
appendUserBubble(text, !!file);
chatInput.value = "";
setLoading(true);
var fd = new FormData();
fd.append("session_id", getSessionId());
fd.append("user_input", text);
if (file) fd.append("file", file, file.name);
var parseMd = typeof marked.parse === "function" ? marked.parse.bind(marked) : marked;
try {
var response = await fetch(BACKEND_URL, { method: "POST", body: fd });
var rawBody = await response.text();
var data = null;
try { data = rawBody ? JSON.parse(rawBody) : null; } catch (_) {}
if (!response.ok) {
var msg = "Request failed (" + response.status + " " + response.statusText + ").";
if (data && data.detail !== undefined) msg += " " + formatDetail(data.detail);
else if (rawBody && !data) msg += " " + rawBody.slice(0, 200);
appendErrorBubble(msg);
return;
}
if (!data || typeof data !== "object") { appendErrorBubble("Unexpected response from server."); return; }
var md = data.text != null ? String(data.text) : "";
var images = Array.isArray(data.images) ? data.images : [];
if (!md.trim() && images.length > 0) md = "*The model returned diagrams without explanation text.*";
appendAssistantBubble(parseMd(md, { breaks: true }), images);
if (!hasCompletedFirstTurn) {
hasCompletedFirstTurn = true;
lockPdfZone(file ? file.name : "");
}
} catch (err) {
appendErrorBubble(err && err.message ? err.message : "Network error β€” check the backend URL and CORS.");
} finally {
setLoading(false);
chatInput.focus();
}
});
getSessionId();
</script>
</body>
</html>