Julien Simon
Add Verdicts tab with Linus/Donald/Bjarne opinions
77de2c1
/* --- 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
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 `<span class="diff-line diff-info">${escaped}</span>`;
}
if (line.startsWith("+")) {
return `<span class="diff-line diff-add">${escaped}</span>`;
}
if (line.startsWith("-")) {
return `<span class="diff-line diff-del">${escaped}</span>`;
}
if (line.startsWith("@@")) {
return `<span class="diff-line diff-info">${escaped}</span>`;
}
return `<span class="diff-line diff-neutral">${escaped}</span>`;
});
return `<pre class="diff-block"><code>${lines.join("")}</code></pre>`;
}
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 `<pre><code${langClass}>${escapeHtml(text)}</code></pre>`;
},
};
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;
}