Julien Simon
Claude Opus 4.5
commited on
Commit
·
faddc33
1
Parent(s):
fb139a8
Remove Share on X button, add review quality metrics bar
Browse filesReplace the Twitter share button with a metrics bar that displays
after each review: issues found, concrete fixes, lines analyzed,
review time, and line coverage percentage.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- public/app.js +62 -24
- public/index.html +1 -5
- public/style.css +20 -15
- server.js +2 -1
public/app.js
CHANGED
|
@@ -13,8 +13,8 @@ const streamError = document.getElementById("stream-error");
|
|
| 13 |
const tabButtons = document.querySelectorAll(".tab");
|
| 14 |
const brutalityBtns = document.querySelectorAll(".brutality-btn");
|
| 15 |
const issueBadge = document.getElementById("issue-badge");
|
|
|
|
| 16 |
const copyBtn = document.getElementById("copy-btn");
|
| 17 |
-
const shareXBtn = document.getElementById("share-x-btn");
|
| 18 |
const toast = document.getElementById("toast");
|
| 19 |
|
| 20 |
const GITHUB_BLOB_RE = /^https?:\/\/github\.com\/[^/]+\/[^/]+\/blob\/[^/]+\/.+$/;
|
|
@@ -240,30 +240,63 @@ function renderIssueBadge(counts) {
|
|
| 240 |
issueBadge.hidden = false;
|
| 241 |
}
|
| 242 |
|
| 243 |
-
/* ---
|
| 244 |
|
| 245 |
-
function
|
| 246 |
-
|
| 247 |
-
|
| 248 |
|
| 249 |
-
|
| 250 |
-
const
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
const
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
}
|
| 265 |
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
/* --- Copy to clipboard --- */
|
| 269 |
|
|
@@ -344,8 +377,8 @@ function startReview() {
|
|
| 344 |
btn.classList.remove("has-content", "streaming");
|
| 345 |
});
|
| 346 |
issueBadge.hidden = true;
|
|
|
|
| 347 |
copyBtn.hidden = true;
|
| 348 |
-
shareXBtn.hidden = true;
|
| 349 |
fullMarkdown = "";
|
| 350 |
|
| 351 |
if (!url) { showInputError("Please enter a GitHub file URL."); return; }
|
|
@@ -358,6 +391,8 @@ function startReview() {
|
|
| 358 |
setLoading(true);
|
| 359 |
|
| 360 |
let markdown = "";
|
|
|
|
|
|
|
| 361 |
const apiUrl = `/api/review?url=${encodeURIComponent(url)}&brutality=${encodeURIComponent(brutalityLevel)}`;
|
| 362 |
const source = new EventSource(apiUrl);
|
| 363 |
currentSource = source;
|
|
@@ -367,6 +402,7 @@ function startReview() {
|
|
| 367 |
metaRepo.textContent = `${data.owner}/${data.repo}`;
|
| 368 |
metaPath.textContent = data.path;
|
| 369 |
metaBranch.textContent = data.branch;
|
|
|
|
| 370 |
metaEl.hidden = false;
|
| 371 |
tabsEl.hidden = false;
|
| 372 |
tabContent.classList.add("is-streaming");
|
|
@@ -386,11 +422,13 @@ function startReview() {
|
|
| 386 |
// Final render with no streaming section
|
| 387 |
const sections = parseSections(markdown);
|
| 388 |
updateTabs(sections, null);
|
| 389 |
-
// Show badge and copy button
|
| 390 |
const counts = countIssuePriorities(markdown);
|
| 391 |
renderIssueBadge(counts);
|
|
|
|
|
|
|
|
|
|
| 392 |
copyBtn.hidden = false;
|
| 393 |
-
shareXBtn.hidden = false;
|
| 394 |
});
|
| 395 |
|
| 396 |
source.addEventListener("error", (e) => {
|
|
|
|
| 13 |
const tabButtons = document.querySelectorAll(".tab");
|
| 14 |
const brutalityBtns = document.querySelectorAll(".brutality-btn");
|
| 15 |
const issueBadge = document.getElementById("issue-badge");
|
| 16 |
+
const reviewMetrics = document.getElementById("review-metrics");
|
| 17 |
const copyBtn = document.getElementById("copy-btn");
|
|
|
|
| 18 |
const toast = document.getElementById("toast");
|
| 19 |
|
| 20 |
const GITHUB_BLOB_RE = /^https?:\/\/github\.com\/[^/]+\/[^/]+\/blob\/[^/]+\/.+$/;
|
|
|
|
| 240 |
issueBadge.hidden = false;
|
| 241 |
}
|
| 242 |
|
| 243 |
+
/* --- Review metrics --- */
|
| 244 |
|
| 245 |
+
function computeMetrics(markdown, totalLines, elapsedMs) {
|
| 246 |
+
// Count ```diff blocks (concrete fixes)
|
| 247 |
+
const diffBlocks = (markdown.match(/```diff/g) || []).length;
|
| 248 |
|
| 249 |
+
// Count total issues by priority tag
|
| 250 |
+
const issueCount = (markdown.match(/\[CRITICAL\]|\[HIGH\]|\[MEDIUM\]|\[LOW\]/gi) || []).length;
|
| 251 |
+
|
| 252 |
+
// Extract unique line numbers referenced (e.g. "Line 42", "Lines 10-20")
|
| 253 |
+
const lineRefs = new Set();
|
| 254 |
+
for (const m of markdown.matchAll(/Lines?\s+(\d+)(?:\s*[-–]\s*(\d+))?/gi)) {
|
| 255 |
+
const start = Number(m[1]);
|
| 256 |
+
const end = m[2] ? Number(m[2]) : start;
|
| 257 |
+
for (let i = start; i <= end && i <= totalLines; i++) lineRefs.add(i);
|
| 258 |
+
}
|
| 259 |
+
const coverage = totalLines > 0 ? Math.round((lineRefs.size / totalLines) * 100) : 0;
|
| 260 |
+
|
| 261 |
+
return {
|
| 262 |
+
issues: issueCount,
|
| 263 |
+
fixes: diffBlocks,
|
| 264 |
+
totalLines,
|
| 265 |
+
elapsedSec: (elapsedMs / 1000).toFixed(1),
|
| 266 |
+
coverage,
|
| 267 |
+
};
|
| 268 |
}
|
| 269 |
|
| 270 |
+
function renderMetrics(m) {
|
| 271 |
+
reviewMetrics.textContent = "";
|
| 272 |
+
|
| 273 |
+
const items = [
|
| 274 |
+
{ value: m.issues, label: "issues found" },
|
| 275 |
+
{ value: m.fixes, label: "concrete fixes" },
|
| 276 |
+
{ value: m.totalLines.toLocaleString(), label: "lines analyzed" },
|
| 277 |
+
{ value: `${m.elapsedSec}s`, label: "review time" },
|
| 278 |
+
{ value: `${m.coverage}%`, label: "line coverage" },
|
| 279 |
+
];
|
| 280 |
+
|
| 281 |
+
for (const item of items) {
|
| 282 |
+
const span = document.createElement("span");
|
| 283 |
+
span.className = "metric-item";
|
| 284 |
+
|
| 285 |
+
const val = document.createElement("span");
|
| 286 |
+
val.className = "metric-value";
|
| 287 |
+
val.textContent = item.value;
|
| 288 |
+
|
| 289 |
+
const lbl = document.createElement("span");
|
| 290 |
+
lbl.className = "metric-label";
|
| 291 |
+
lbl.textContent = item.label;
|
| 292 |
+
|
| 293 |
+
span.appendChild(val);
|
| 294 |
+
span.appendChild(lbl);
|
| 295 |
+
reviewMetrics.appendChild(span);
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
reviewMetrics.hidden = false;
|
| 299 |
+
}
|
| 300 |
|
| 301 |
/* --- Copy to clipboard --- */
|
| 302 |
|
|
|
|
| 377 |
btn.classList.remove("has-content", "streaming");
|
| 378 |
});
|
| 379 |
issueBadge.hidden = true;
|
| 380 |
+
reviewMetrics.hidden = true;
|
| 381 |
copyBtn.hidden = true;
|
|
|
|
| 382 |
fullMarkdown = "";
|
| 383 |
|
| 384 |
if (!url) { showInputError("Please enter a GitHub file URL."); return; }
|
|
|
|
| 391 |
setLoading(true);
|
| 392 |
|
| 393 |
let markdown = "";
|
| 394 |
+
let reviewStartTime = performance.now();
|
| 395 |
+
let fileTotalLines = 0;
|
| 396 |
const apiUrl = `/api/review?url=${encodeURIComponent(url)}&brutality=${encodeURIComponent(brutalityLevel)}`;
|
| 397 |
const source = new EventSource(apiUrl);
|
| 398 |
currentSource = source;
|
|
|
|
| 402 |
metaRepo.textContent = `${data.owner}/${data.repo}`;
|
| 403 |
metaPath.textContent = data.path;
|
| 404 |
metaBranch.textContent = data.branch;
|
| 405 |
+
fileTotalLines = data.lines || 0;
|
| 406 |
metaEl.hidden = false;
|
| 407 |
tabsEl.hidden = false;
|
| 408 |
tabContent.classList.add("is-streaming");
|
|
|
|
| 422 |
// Final render with no streaming section
|
| 423 |
const sections = parseSections(markdown);
|
| 424 |
updateTabs(sections, null);
|
| 425 |
+
// Show badge, metrics, and copy button
|
| 426 |
const counts = countIssuePriorities(markdown);
|
| 427 |
renderIssueBadge(counts);
|
| 428 |
+
const elapsed = performance.now() - reviewStartTime;
|
| 429 |
+
const metrics = computeMetrics(markdown, fileTotalLines, elapsed);
|
| 430 |
+
renderMetrics(metrics);
|
| 431 |
copyBtn.hidden = false;
|
|
|
|
| 432 |
});
|
| 433 |
|
| 434 |
source.addEventListener("error", (e) => {
|
public/index.html
CHANGED
|
@@ -258,6 +258,7 @@
|
|
| 258 |
</div>
|
| 259 |
|
| 260 |
<div id="issue-badge" hidden></div>
|
|
|
|
| 261 |
|
| 262 |
<div id="tabs" hidden>
|
| 263 |
<nav class="tab-bar">
|
|
@@ -267,11 +268,6 @@
|
|
| 267 |
<button class="tab" data-section="security">Security</button>
|
| 268 |
<button class="tab" data-section="suggestions">Suggestions</button>
|
| 269 |
<button class="tab" data-section="verdicts">Verdicts</button>
|
| 270 |
-
<button type="button" id="share-x-btn" hidden title="Share on X">
|
| 271 |
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
| 272 |
-
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
| 273 |
-
</svg>
|
| 274 |
-
</button>
|
| 275 |
<button type="button" id="copy-btn" hidden>Copy Review</button>
|
| 276 |
</nav>
|
| 277 |
<div id="tab-content" class="tab-content"></div>
|
|
|
|
| 258 |
</div>
|
| 259 |
|
| 260 |
<div id="issue-badge" hidden></div>
|
| 261 |
+
<div id="review-metrics" hidden></div>
|
| 262 |
|
| 263 |
<div id="tabs" hidden>
|
| 264 |
<nav class="tab-bar">
|
|
|
|
| 268 |
<button class="tab" data-section="security">Security</button>
|
| 269 |
<button class="tab" data-section="suggestions">Suggestions</button>
|
| 270 |
<button class="tab" data-section="verdicts">Verdicts</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
<button type="button" id="copy-btn" hidden>Copy Review</button>
|
| 272 |
</nav>
|
| 273 |
<div id="tab-content" class="tab-content"></div>
|
public/style.css
CHANGED
|
@@ -493,29 +493,34 @@ h1 {
|
|
| 493 |
color: var(--text-muted);
|
| 494 |
}
|
| 495 |
|
| 496 |
-
/* ----------
|
| 497 |
|
| 498 |
-
#
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
|
|
|
|
|
|
| 502 |
background: var(--surface);
|
| 503 |
border: 1px solid var(--border);
|
| 504 |
border-radius: var(--radius);
|
| 505 |
-
|
| 506 |
-
transition: all 0.15s;
|
| 507 |
-
display: inline-flex;
|
| 508 |
-
align-items: center;
|
| 509 |
}
|
| 510 |
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
|
|
|
| 514 |
}
|
| 515 |
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
color:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 519 |
}
|
| 520 |
|
| 521 |
/* ---------- Copy Button ---------- */
|
|
|
|
| 493 |
color: var(--text-muted);
|
| 494 |
}
|
| 495 |
|
| 496 |
+
/* ---------- Review Metrics ---------- */
|
| 497 |
|
| 498 |
+
#review-metrics {
|
| 499 |
+
display: flex;
|
| 500 |
+
gap: 20px;
|
| 501 |
+
flex-wrap: wrap;
|
| 502 |
+
margin-top: 10px;
|
| 503 |
+
padding: 10px 14px;
|
| 504 |
background: var(--surface);
|
| 505 |
border: 1px solid var(--border);
|
| 506 |
border-radius: var(--radius);
|
| 507 |
+
font-size: 0.85rem;
|
|
|
|
|
|
|
|
|
|
| 508 |
}
|
| 509 |
|
| 510 |
+
.metric-item {
|
| 511 |
+
display: flex;
|
| 512 |
+
align-items: baseline;
|
| 513 |
+
gap: 5px;
|
| 514 |
}
|
| 515 |
|
| 516 |
+
.metric-value {
|
| 517 |
+
font-weight: 700;
|
| 518 |
+
color: var(--accent);
|
| 519 |
+
font-variant-numeric: tabular-nums;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
.metric-label {
|
| 523 |
+
color: var(--text-muted);
|
| 524 |
}
|
| 525 |
|
| 526 |
/* ---------- Copy Button ---------- */
|
server.js
CHANGED
|
@@ -225,7 +225,8 @@ app.get("/api/review", async (req, res) => {
|
|
| 225 |
}
|
| 226 |
|
| 227 |
// Send file metadata to the client
|
| 228 |
-
|
|
|
|
| 229 |
|
| 230 |
// Stream from OpenRouter
|
| 231 |
console.log(`[review] Requesting review from OpenRouter (brutality: ${brutalityLevel})…`);
|
|
|
|
| 225 |
}
|
| 226 |
|
| 227 |
// Send file metadata to the client
|
| 228 |
+
const totalLines = code.split("\n").length;
|
| 229 |
+
send("meta", { owner: meta.owner, repo: meta.repo, branch: meta.branch, path: meta.path, lines: totalLines });
|
| 230 |
|
| 231 |
// Stream from OpenRouter
|
| 232 |
console.log(`[review] Requesting review from OpenRouter (brutality: ${brutalityLevel})…`);
|