const docBody = document.body; window.APP_STATE = { ready: docBody.dataset.appReady === "true", loading: docBody.dataset.appLoading === "true", error: docBody.dataset.appError || null, }; const form = document.getElementById("upload-form"); const fileInput = document.getElementById("file-input"); const dropZone = document.getElementById("drop-zone"); const fileMeta = document.getElementById("file-meta"); const submitBtn = document.getElementById("submit-btn"); const loadingEl = document.getElementById("loading"); const errorEl = document.getElementById("error"); const resultsEl = document.getElementById("results"); const resultsMeta = document.getElementById("results-meta"); const resultsGrid = document.getElementById("results-grid"); const statusBadge = document.getElementById("status-badge"); const header = document.querySelector(".site-header"); let selectedFile = null; /* ---------- Header shadow on scroll ---------- */ function onScroll() { if (window.scrollY > 10) header.classList.add("scrolled"); else header.classList.remove("scrolled"); } window.addEventListener("scroll", onScroll, { passive: true }); onScroll(); /* ---------- Scroll reveal ---------- */ const revealObserver = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add("in"); revealObserver.unobserve(entry.target); } }); }, { threshold: 0.12 } ); document.querySelectorAll(".reveal").forEach((el) => revealObserver.observe(el)); /* ---------- Active nav link on scroll ---------- */ const navLinks = Array.from(document.querySelectorAll(".nav-link")); const sections = navLinks .map((link) => document.querySelector(link.getAttribute("href"))) .filter(Boolean); const navObserver = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const id = entry.target.getAttribute("id"); navLinks.forEach((l) => l.classList.toggle("active", l.getAttribute("href") === `#${id}`) ); } }); }, { rootMargin: "-45% 0px -50% 0px" } ); sections.forEach((s) => navObserver.observe(s)); /* ---------- Expandable model cards ---------- */ document.querySelectorAll(".model-card-head").forEach((head) => { head.addEventListener("click", () => { const card = head.closest(".model-card"); const open = card.classList.toggle("open"); head.setAttribute("aria-expanded", String(open)); }); }); /* ---------- Upload helpers ---------- */ function setError(message) { if (!message) { errorEl.classList.add("hidden"); errorEl.textContent = ""; return; } errorEl.textContent = message; errorEl.classList.remove("hidden"); } function updateFileMeta(file) { selectedFile = file; fileMeta.textContent = `${file.name} · ${(file.size / 1024).toFixed(1)} KB`; fileMeta.classList.remove("hidden"); submitBtn.disabled = !window.APP_STATE.ready; } dropZone.addEventListener("click", () => fileInput.click()); dropZone.addEventListener("dragover", (e) => { e.preventDefault(); dropZone.classList.add("dragover"); }); dropZone.addEventListener("dragleave", () => dropZone.classList.remove("dragover")); dropZone.addEventListener("drop", (e) => { e.preventDefault(); dropZone.classList.remove("dragover"); const file = e.dataTransfer.files[0]; if (file) updateFileMeta(file); }); fileInput.addEventListener("change", () => { if (fileInput.files[0]) updateFileMeta(fileInput.files[0]); }); /* ---------- Submit ---------- */ form.addEventListener("submit", async (e) => { e.preventDefault(); if (!selectedFile) return; if (!window.APP_STATE.ready) { setError("Models are not ready yet. Please wait for startup to finish."); return; } setError(""); resultsEl.classList.add("hidden"); loadingEl.classList.remove("hidden"); submitBtn.disabled = true; const payload = new FormData(); payload.append("file", selectedFile); try { const res = await fetch("/api/summarize", { method: "POST", body: payload }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || "Summarization failed."); renderResults(data); resultsEl.classList.remove("hidden"); resultsEl.scrollIntoView({ behavior: "smooth", block: "nearest" }); } catch (err) { setError(err.message); } finally { loadingEl.classList.add("hidden"); submitBtn.disabled = !window.APP_STATE.ready; } }); /* ---------- Render results ---------- */ function renderResults(data) { resultsMeta.innerHTML = [ ["File", data.filename], ["Methods", data.method_count], ["Statements", data.statement_count], ["Tokens", data.token_count], ["Total time", `${data.total_elapsed_ms} ms`], ] .map(([k, v]) => `${k}: ${escapeHtml(String(v))}`) .join(""); const summaries = data.summaries; const withText = summaries.filter((s) => s.summary && s.summary.trim()); const fastest = summaries.reduce((a, b) => (b.elapsed_ms < a.elapsed_ms ? b : a)); const longest = withText.reduce( (a, b) => (b.summary.length > (a ? a.summary.length : -1) ? b : a), null ); resultsGrid.innerHTML = summaries .map((s, i) => { const accent = s.accent || "#5b9cff"; const isEmpty = !s.summary || !s.summary.trim(); const badges = []; if (s === fastest) badges.push(`⚡ Fastest`); if (longest && s === longest) badges.push(`Most detail`); const isCodet5 = s.model_id === "codet5" && s.methods && s.methods.length > 0; const bodyHtml = isEmpty ? "(no output produced)" : isCodet5 ? renderMethodSummaries(s.methods) : escapeHtml(s.summary); const bodyClass = isEmpty ? "empty" : isCodet5 ? "method-list" : ""; return `
${escapeHtml(glyphFor(s.model_id, s.model))}

${escapeHtml(s.model)}

${escapeHtml(s.tier)}
${escapeHtml(s.approach)}
${bodyHtml}
`; }) .join(""); resultsGrid.querySelectorAll(".copy-btn").forEach((btn, idx) => { btn.addEventListener("click", () => { const text = summaries[idx].summary || ""; navigator.clipboard.writeText(text).then(() => { btn.textContent = "Copied"; btn.classList.add("copied"); setTimeout(() => { btn.textContent = "Copy"; btn.classList.remove("copied"); }, 1600); }); }); }); } function renderMethodSummaries(methods) { if (!methods.length) return ""; return methods .map((m) => { const text = (m.summary || "").trim(); const summaryText = text || "(no output)"; const emptyClass = text ? "" : " empty-method"; return `
${escapeHtml(m.name)}
${escapeHtml(summaryText)}
`; }) .join(""); } function glyphFor(id, name) { const map = { tfidf: "TF", lexrank: "LR", sentence_transformers: "ST", codet5: "T5", }; return map[id] || (name || "?").slice(0, 2).toUpperCase(); } function escapeHtml(text) { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } /* ---------- Health polling ---------- */ function setStatus(state, text) { statusBadge.className = `status-badge ${state}`; statusBadge.querySelector(".status-text").textContent = text; } async function pollHealth() { try { const res = await fetch("/api/health"); const data = await res.json(); window.APP_STATE.ready = data.ready; window.APP_STATE.loading = data.loading; window.APP_STATE.error = data.error; if (data.ready) { setStatus("ready", "Models ready"); if (selectedFile) submitBtn.disabled = false; } else if (data.loading) { setStatus("loading", "Loading models…"); } else { setStatus("error", data.error ? "Load failed" : "Not ready"); if (data.error) setError(data.error); } if (!data.ready && !data.error) setTimeout(pollHealth, 3000); } catch { setTimeout(pollHealth, 5000); } } if (!window.APP_STATE.ready) { pollHealth(); } else if (selectedFile) { submitBtn.disabled = false; }