/* --- 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 brutalityBtns = document.querySelectorAll(".brutality-btn"); const issueBadge = document.getElementById("issue-badge"); const reviewMetrics = document.getElementById("review-metrics"); const saveBtn = document.getElementById("save-btn"); const toast = document.getElementById("toast"); 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 = {}; let brutalityLevel = "standard"; let fullMarkdown = ""; 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(); } /* --- Brutality selector --- */ brutalityBtns.forEach((btn) => { btn.addEventListener("click", () => { brutalityBtns.forEach((b) => b.classList.remove("active")); btn.classList.add("active"); brutalityLevel = btn.dataset.level; }); }); /* --- Issue badge --- */ function countIssuePriorities(markdown) { const counts = { critical: 0, high: 0, medium: 0, low: 0 }; const pattern = /\[CRITICAL\]|\[HIGH\]|\[MEDIUM\]|\[LOW\]/gi; const matches = markdown.match(pattern) || []; for (const m of matches) { const level = m.slice(1, -1).toLowerCase(); if (counts[level] !== undefined) counts[level]++; } return counts; } function renderIssueBadge(counts) { const total = counts.critical + counts.high + counts.medium + counts.low; if (total === 0) { issueBadge.hidden = true; return; } const items = [ { level: "critical", label: "Critical", count: counts.critical }, { level: "high", label: "High", count: counts.high }, { level: "medium", label: "Medium", count: counts.medium }, { level: "low", label: "Low", count: counts.low }, ]; // Clear existing content issueBadge.textContent = ""; for (const item of items) { if (item.count === 0) continue; const span = document.createElement("span"); span.className = "badge-item"; const dot = document.createElement("span"); dot.className = `badge-dot ${item.level}`; const countEl = document.createElement("span"); countEl.className = "badge-count"; countEl.textContent = item.count; const labelEl = document.createElement("span"); labelEl.className = "badge-label"; labelEl.textContent = item.label; span.appendChild(dot); span.appendChild(countEl); span.appendChild(labelEl); issueBadge.appendChild(span); } issueBadge.hidden = false; } /* --- Review metrics --- */ function computeMetrics(markdown, totalLines, elapsedMs) { // Count ```diff blocks (concrete fixes) const diffBlocks = (markdown.match(/```diff/g) || []).length; // Count total issues by priority tag const issueCount = (markdown.match(/\[CRITICAL\]|\[HIGH\]|\[MEDIUM\]|\[LOW\]/gi) || []).length; return { issues: issueCount, fixes: diffBlocks, totalLines, elapsedSec: (elapsedMs / 1000).toFixed(1), }; } function renderMetrics(m) { reviewMetrics.textContent = ""; const items = [ { value: m.issues, label: "issues found" }, { value: m.fixes, label: "concrete fixes" }, { value: m.totalLines.toLocaleString(), label: "lines analyzed" }, { value: `${m.elapsedSec}s`, label: "review time" }, ]; for (const item of items) { const span = document.createElement("span"); span.className = "metric-item"; const val = document.createElement("span"); val.className = "metric-value"; val.textContent = item.value; const lbl = document.createElement("span"); lbl.className = "metric-label"; lbl.textContent = item.label; span.appendChild(val); span.appendChild(lbl); reviewMetrics.appendChild(span); } reviewMetrics.hidden = false; } /* --- Copy to clipboard --- */ function buildMarkdownExport() { const sectionTitles = { summary: "## Summary", quality: "## Code Quality", performance: "## Performance", security: "## Security", suggestions: "## Suggestions", verdicts: "## Verdicts", }; let output = `# Code Review: ${metaRepo.textContent}\n`; output += `**File:** ${metaPath.textContent} | **Branch:** ${metaBranch.textContent}\n\n`; for (const key of SECTION_KEYS) { const content = currentSections[key]?.trim(); if (content) { output += `${sectionTitles[key]}\n${content}\n\n`; } } return output.trim(); } saveBtn.addEventListener("click", () => { const markdown = buildMarkdownExport(); const filename = `${metaRepo.textContent.replace(/\//g, '-')}-${metaPath.textContent.split('/').pop()}-${brutalityLevel}.md`; const blob = new Blob([markdown], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast(); }); function showToast() { toast.hidden = false; // Force reflow before adding class for animation toast.offsetHeight; toast.classList.add("show"); setTimeout(() => { toast.classList.remove("show"); setTimeout(() => { toast.hidden = true; }, 300); }, 2000); } /* --- 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"); }); issueBadge.hidden = true; reviewMetrics.hidden = true; saveBtn.hidden = true; fullMarkdown = ""; 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 = ""; let reviewStartTime = performance.now(); let fileTotalLines = 0; const apiUrl = `/api/review?url=${encodeURIComponent(url)}&brutality=${encodeURIComponent(brutalityLevel)}`; const source = new EventSource(apiUrl); 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; fileTotalLines = data.lines || 0; 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(); fullMarkdown = markdown; // Final render with no streaming section const sections = parseSections(markdown); updateTabs(sections, null); // Show badge, metrics, and copy button const counts = countIssuePriorities(markdown); renderIssueBadge(counts); const elapsed = performance.now() - reviewStartTime; const metrics = computeMetrics(markdown, fileTotalLines, elapsed); renderMetrics(metrics); saveBtn.hidden = false; }); 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; }