/* --- 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;
}