/* --- DOM refs --- */ const form = document.getElementById("review-form"); const urlInput = document.getElementById("url-input"); const submitBtn = document.getElementById("submit-btn"); const inputError = document.getElementById("input-error"); const metaEl = document.getElementById("meta"); const metaRepo = document.getElementById("meta-repo"); const metaPath = document.getElementById("meta-path"); const metaBranch = document.getElementById("meta-branch"); const tabsEl = document.getElementById("tabs"); const tabContent = document.getElementById("tab-content"); const streamError = document.getElementById("stream-error"); const tabButtons = document.querySelectorAll(".tab"); const GITHUB_BLOB_RE = /^https?:\/\/github\.com\/[^/]+\/[^/]+\/blob\/[^/]+\/.+$/; /* --- Section parsing --- */ const SECTION_KEYS = ["summary", "quality", "performance", "security", "suggestions", "verdicts"]; // Maps heading text (lowercased) to our section key const HEADING_MAP = { summary: "summary", "code quality": "quality", performance: "performance", security: "security", suggestions: "suggestions", verdicts: "verdicts", }; function parseSections(markdown) { const sections = {}; let currentKey = null; for (const line of markdown.split("\n")) { const m = line.match(/^## (.+)$/); if (m) { const title = m[1].trim().toLowerCase(); const matched = Object.entries(HEADING_MAP).find(([kw]) => title.includes(kw)); if (matched) { currentKey = matched[1]; sections[currentKey] = ""; continue; } } if (currentKey) { sections[currentKey] = (sections[currentKey] || "") + line + "\n"; } } return sections; } // Detect which section the model is currently writing to (last matched heading) function detectCurrentSection(markdown) { let last = null; for (const line of markdown.split("\n")) { const m = line.match(/^## (.+)$/); if (m) { const title = m[1].trim().toLowerCase(); const matched = Object.entries(HEADING_MAP).find(([kw]) => title.includes(kw)); if (matched) last = matched[1]; } } return last; } /* --- Diff-aware marked renderer --- */ function escapeHtml(str) { return str.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function isDiffContent(text) { const lines = text.split("\n"); let diffLines = 0; for (const l of lines) { if (l.startsWith("+") || l.startsWith("-") || l.startsWith("@@")) diffLines++; } return diffLines >= 2; } function renderDiffBlock(text) { const lines = text.split("\n").map((line) => { const escaped = escapeHtml(line); if (line.startsWith("+++") || line.startsWith("---")) { return `${escaped}`; } if (line.startsWith("+")) { return `${escaped}`; } if (line.startsWith("-")) { return `${escaped}`; } if (line.startsWith("@@")) { return `${escaped}`; } return `${escaped}`; }); return `
${lines.join("")}
`; } const renderer = { code({ text, lang, language }) { const codeLang = (lang || language || "").toLowerCase().trim(); // Render as diff if tagged as diff OR if content looks like a diff if (codeLang === "diff" || (!codeLang && isDiffContent(text))) { return renderDiffBlock(text); } const langClass = codeLang ? ` class="language-${escapeHtml(codeLang)}"` : ""; return `
${escapeHtml(text)}
`; }, }; marked.use({ renderer }); function renderMarkdown(md) { return DOMPurify.sanitize(marked.parse(md)); } /* --- Tab management --- */ let activeTab = "summary"; let manualSwitch = false; let currentSections = {}; tabButtons.forEach((btn) => { btn.addEventListener("click", () => { manualSwitch = true; setActiveTab(btn.dataset.section); renderActiveTab(); }); }); function setActiveTab(section) { activeTab = section; tabButtons.forEach((btn) => { btn.classList.toggle("active", btn.dataset.section === section); }); } function renderActiveTab() { const md = currentSections[activeTab] || ""; if (md.trim()) { tabContent.innerHTML = renderMarkdown(md); tabContent.classList.remove("tab-content-empty"); } else { tabContent.textContent = "Waiting for content\u2026"; tabContent.classList.add("tab-content-empty"); } } function updateTabs(sections, currentStreamSection) { currentSections = sections; // Mark tabs that have content + which one is streaming tabButtons.forEach((btn) => { const key = btn.dataset.section; btn.classList.toggle("has-content", !!sections[key]?.trim()); btn.classList.toggle("streaming", key === currentStreamSection); }); // Auto-switch to the section currently being written (unless user clicked a tab) if (!manualSwitch && currentStreamSection) { setActiveTab(currentStreamSection); } renderActiveTab(); } /* --- Review flow --- */ let currentSource = null; form.addEventListener("submit", (e) => { e.preventDefault(); startReview(); }); /* --- Sample buttons --- */ document.querySelectorAll(".sample-btn").forEach((btn) => { btn.addEventListener("click", () => { urlInput.value = btn.dataset.url; startReview(); }); }); function startReview() { const url = urlInput.value.trim(); // Reset inputError.hidden = true; streamError.hidden = true; metaEl.hidden = true; tabsEl.hidden = true; tabContent.textContent = ""; manualSwitch = false; setActiveTab("summary"); tabButtons.forEach((btn) => { btn.classList.remove("has-content", "streaming"); }); if (!url) { showInputError("Please enter a GitHub file URL."); return; } if (!GITHUB_BLOB_RE.test(url)) { showInputError("URL must be a GitHub blob URL (e.g. https://github.com/owner/repo/blob/main/file.js)."); return; } if (currentSource) { currentSource.close(); currentSource = null; } setLoading(true); let markdown = ""; const source = new EventSource(`/api/review?url=${encodeURIComponent(url)}`); currentSource = source; source.addEventListener("meta", (e) => { const data = JSON.parse(e.data); metaRepo.textContent = `${data.owner}/${data.repo}`; metaPath.textContent = data.path; metaBranch.textContent = data.branch; metaEl.hidden = false; tabsEl.hidden = false; tabContent.classList.add("is-streaming"); }); source.addEventListener("content", (e) => { const data = JSON.parse(e.data); markdown += data.text; const sections = parseSections(markdown); const current = detectCurrentSection(markdown); updateTabs(sections, current); }); source.addEventListener("done", () => { cleanup(); // Final render with no streaming section const sections = parseSections(markdown); updateTabs(sections, null); }); source.addEventListener("error", (e) => { if (e.data) { const data = JSON.parse(e.data); showStreamError(data.message); } else { showStreamError("Connection lost. Please try again."); } cleanup(); }); function cleanup() { source.close(); currentSource = null; setLoading(false); tabContent.classList.remove("is-streaming"); tabButtons.forEach((btn) => btn.classList.remove("streaming")); } } /* --- Helpers --- */ function setLoading(on) { submitBtn.disabled = on; submitBtn.textContent = on ? "Reviewing\u2026" : "Review Code"; document.body.classList.toggle("loading", on); } function showInputError(msg) { inputError.textContent = msg; inputError.hidden = false; } function showStreamError(msg) { streamError.textContent = msg; streamError.hidden = false; }