| <!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 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> |
|
|
| |
| <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"> |
|
|
| |
| <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 Β· |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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" |
| > |
| |
| <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 & 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> |
|
|
| |
| <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> |
|
|
| |
| <input |
| type="file" |
| id="pdf-input" |
| accept=".pdf,application/pdf" |
| class="absolute inset-0 cursor-pointer opacity-0" |
| /> |
| </div> |
| </div> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
| |
| 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"; |
| |
| |
| |
| 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(); |
| }); |
| |
| |
| |
| 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; |
| } |
| |
| |
| |
| 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(); |
| } |
| }); |
| |
| |
| ["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); |
| } |
| }); |
| |
| |
| |
| 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 Β· <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); |
| |
| |
| |
| 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(); |
| } |
| |
| |
| |
| 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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); |
| 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(); |
| } |
| |
| |
| |
| 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(); |
| }); |
| |
| |
| |
| chatInput.addEventListener("keydown", function (e) { |
| if (e.key === "Enter" && !e.shiftKey) { |
| e.preventDefault(); |
| if (!sendBtn.disabled) chatForm.requestSubmit(); |
| } |
| }); |
| |
| |
| |
| 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> |
|
|