Add brutality selector, issue badge, and copy review button
Browse files- .claude/settings.local.json +37 -0
- .gitignore +2 -0
- public/app.js +128 -1
- public/index.html +12 -0
- public/style.css +126 -0
- server.js +29 -5
.claude/settings.local.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"permissions": {
|
| 3 |
+
"allow": [
|
| 4 |
+
"WebFetch(domain:openrouter.ai)",
|
| 5 |
+
"Bash(node --version:*)",
|
| 6 |
+
"Bash(npm --version)",
|
| 7 |
+
"Bash(curl:*)",
|
| 8 |
+
"Bash(npm install)",
|
| 9 |
+
"Bash(OPENROUTER_API_KEY=test-key timeout 3 node:*)",
|
| 10 |
+
"Bash(OPENROUTER_API_KEY=test-key node:*)",
|
| 11 |
+
"WebSearch",
|
| 12 |
+
"Bash(node --check:*)",
|
| 13 |
+
"Bash(node -e \"\n// Simulate browser globals to check parse only\nglobalThis.document = { getElementById: \\(\\) => \\({ hidden: true, classList: { toggle\\(\\){}, add\\(\\){}, remove\\(\\){} }, addEventListener\\(\\){} }\\), querySelectorAll: \\(\\) => [] };\nglobalThis.DOMPurify = { sanitize: x => x };\nglobalThis.marked = { parse: x => x, use\\(\\){} };\nglobalThis.EventSource = class {};\n\")",
|
| 14 |
+
"Bash(find:*)",
|
| 15 |
+
"Bash(wc:*)",
|
| 16 |
+
"Bash(huggingface-cli repo create:*)",
|
| 17 |
+
"Bash(huggingface-cli whoami:*)",
|
| 18 |
+
"Bash(hf repo create:*)",
|
| 19 |
+
"Bash(hf auth token:*)",
|
| 20 |
+
"Bash(hf auth list:*)",
|
| 21 |
+
"Bash(git ls-remote:*)",
|
| 22 |
+
"Bash(hf auth login:*)",
|
| 23 |
+
"Bash(git add:*)",
|
| 24 |
+
"Bash(git commit:*)",
|
| 25 |
+
"Bash(git push:*)",
|
| 26 |
+
"Bash(hf repo settings:*)",
|
| 27 |
+
"Bash(python3 -c \"\nfrom huggingface_hub import HfApi\napi = HfApi\\(\\)\napi.add_space_secret\\(''juliensimon/trinity-code-reviewer'', ''OPENROUTER_API_KEY'', ''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''\\)\nprint\\(''Secret set successfully''\\)\n\")",
|
| 28 |
+
"Bash(python3:*)",
|
| 29 |
+
"WebFetch(domain:lobehub.com)",
|
| 30 |
+
"Bash(grep -i content-length curl -sI \"https://raw.githubusercontent.com/django/django/main/django/db/models/expressions.py\")",
|
| 31 |
+
"Bash(grep -i content-length curl -sI \"https://raw.githubusercontent.com/facebook/react/main/packages/react-reconciler/src/ReactFiberHooks.js\")",
|
| 32 |
+
"Bash(grep -i content-length curl -sI \"https://raw.githubusercontent.com/microsoft/TypeScript/main/src/compiler/scanner.ts\")",
|
| 33 |
+
"Bash(git init:*)",
|
| 34 |
+
"Bash(hf upload:*)"
|
| 35 |
+
]
|
| 36 |
+
}
|
| 37 |
+
}
|
.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
.env
|
public/app.js
CHANGED
|
@@ -11,6 +11,10 @@ const tabsEl = document.getElementById("tabs");
|
|
| 11 |
const tabContent = document.getElementById("tab-content");
|
| 12 |
const streamError = document.getElementById("stream-error");
|
| 13 |
const tabButtons = document.querySelectorAll(".tab");
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
const GITHUB_BLOB_RE = /^https?:\/\/github\.com\/[^/]+\/[^/]+\/blob\/[^/]+\/.+$/;
|
| 16 |
|
|
@@ -122,6 +126,8 @@ function renderMarkdown(md) {
|
|
| 122 |
let activeTab = "summary";
|
| 123 |
let manualSwitch = false;
|
| 124 |
let currentSections = {};
|
|
|
|
|
|
|
| 125 |
|
| 126 |
tabButtons.forEach((btn) => {
|
| 127 |
btn.addEventListener("click", () => {
|
|
@@ -167,6 +173,118 @@ function updateTabs(sections, currentStreamSection) {
|
|
| 167 |
renderActiveTab();
|
| 168 |
}
|
| 169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
/* --- Review flow --- */
|
| 171 |
|
| 172 |
let currentSource = null;
|
|
@@ -199,6 +317,9 @@ function startReview() {
|
|
| 199 |
tabButtons.forEach((btn) => {
|
| 200 |
btn.classList.remove("has-content", "streaming");
|
| 201 |
});
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
if (!url) { showInputError("Please enter a GitHub file URL."); return; }
|
| 204 |
if (!GITHUB_BLOB_RE.test(url)) {
|
|
@@ -210,7 +331,8 @@ function startReview() {
|
|
| 210 |
setLoading(true);
|
| 211 |
|
| 212 |
let markdown = "";
|
| 213 |
-
const
|
|
|
|
| 214 |
currentSource = source;
|
| 215 |
|
| 216 |
source.addEventListener("meta", (e) => {
|
|
@@ -233,9 +355,14 @@ function startReview() {
|
|
| 233 |
|
| 234 |
source.addEventListener("done", () => {
|
| 235 |
cleanup();
|
|
|
|
| 236 |
// Final render with no streaming section
|
| 237 |
const sections = parseSections(markdown);
|
| 238 |
updateTabs(sections, null);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
});
|
| 240 |
|
| 241 |
source.addEventListener("error", (e) => {
|
|
|
|
| 11 |
const tabContent = document.getElementById("tab-content");
|
| 12 |
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 toast = document.getElementById("toast");
|
| 18 |
|
| 19 |
const GITHUB_BLOB_RE = /^https?:\/\/github\.com\/[^/]+\/[^/]+\/blob\/[^/]+\/.+$/;
|
| 20 |
|
|
|
|
| 126 |
let activeTab = "summary";
|
| 127 |
let manualSwitch = false;
|
| 128 |
let currentSections = {};
|
| 129 |
+
let brutalityLevel = "standard";
|
| 130 |
+
let fullMarkdown = "";
|
| 131 |
|
| 132 |
tabButtons.forEach((btn) => {
|
| 133 |
btn.addEventListener("click", () => {
|
|
|
|
| 173 |
renderActiveTab();
|
| 174 |
}
|
| 175 |
|
| 176 |
+
/* --- Brutality selector --- */
|
| 177 |
+
|
| 178 |
+
brutalityBtns.forEach((btn) => {
|
| 179 |
+
btn.addEventListener("click", () => {
|
| 180 |
+
brutalityBtns.forEach((b) => b.classList.remove("active"));
|
| 181 |
+
btn.classList.add("active");
|
| 182 |
+
brutalityLevel = btn.dataset.level;
|
| 183 |
+
});
|
| 184 |
+
});
|
| 185 |
+
|
| 186 |
+
/* --- Issue badge --- */
|
| 187 |
+
|
| 188 |
+
function countIssuePriorities(markdown) {
|
| 189 |
+
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
|
| 190 |
+
const pattern = /\[CRITICAL\]|\[HIGH\]|\[MEDIUM\]|\[LOW\]/gi;
|
| 191 |
+
const matches = markdown.match(pattern) || [];
|
| 192 |
+
for (const m of matches) {
|
| 193 |
+
const level = m.slice(1, -1).toLowerCase();
|
| 194 |
+
if (counts[level] !== undefined) counts[level]++;
|
| 195 |
+
}
|
| 196 |
+
return counts;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
function renderIssueBadge(counts) {
|
| 200 |
+
const total = counts.critical + counts.high + counts.medium + counts.low;
|
| 201 |
+
if (total === 0) {
|
| 202 |
+
issueBadge.hidden = true;
|
| 203 |
+
return;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
const items = [
|
| 207 |
+
{ level: "critical", label: "Critical", count: counts.critical },
|
| 208 |
+
{ level: "high", label: "High", count: counts.high },
|
| 209 |
+
{ level: "medium", label: "Medium", count: counts.medium },
|
| 210 |
+
{ level: "low", label: "Low", count: counts.low },
|
| 211 |
+
];
|
| 212 |
+
|
| 213 |
+
// Clear existing content
|
| 214 |
+
issueBadge.textContent = "";
|
| 215 |
+
|
| 216 |
+
for (const item of items) {
|
| 217 |
+
if (item.count === 0) continue;
|
| 218 |
+
|
| 219 |
+
const span = document.createElement("span");
|
| 220 |
+
span.className = "badge-item";
|
| 221 |
+
|
| 222 |
+
const dot = document.createElement("span");
|
| 223 |
+
dot.className = `badge-dot ${item.level}`;
|
| 224 |
+
|
| 225 |
+
const countEl = document.createElement("span");
|
| 226 |
+
countEl.className = "badge-count";
|
| 227 |
+
countEl.textContent = item.count;
|
| 228 |
+
|
| 229 |
+
const labelEl = document.createElement("span");
|
| 230 |
+
labelEl.className = "badge-label";
|
| 231 |
+
labelEl.textContent = item.label;
|
| 232 |
+
|
| 233 |
+
span.appendChild(dot);
|
| 234 |
+
span.appendChild(countEl);
|
| 235 |
+
span.appendChild(labelEl);
|
| 236 |
+
issueBadge.appendChild(span);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
issueBadge.hidden = false;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
/* --- Copy to clipboard --- */
|
| 243 |
+
|
| 244 |
+
function buildMarkdownExport() {
|
| 245 |
+
const sectionTitles = {
|
| 246 |
+
summary: "## Summary",
|
| 247 |
+
quality: "## Code Quality",
|
| 248 |
+
performance: "## Performance",
|
| 249 |
+
security: "## Security",
|
| 250 |
+
suggestions: "## Suggestions",
|
| 251 |
+
verdicts: "## Verdicts",
|
| 252 |
+
};
|
| 253 |
+
|
| 254 |
+
let output = `# Code Review: ${metaRepo.textContent}\n`;
|
| 255 |
+
output += `**File:** ${metaPath.textContent} | **Branch:** ${metaBranch.textContent}\n\n`;
|
| 256 |
+
|
| 257 |
+
for (const key of SECTION_KEYS) {
|
| 258 |
+
const content = currentSections[key]?.trim();
|
| 259 |
+
if (content) {
|
| 260 |
+
output += `${sectionTitles[key]}\n${content}\n\n`;
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
return output.trim();
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
copyBtn.addEventListener("click", async () => {
|
| 268 |
+
const markdown = buildMarkdownExport();
|
| 269 |
+
try {
|
| 270 |
+
await navigator.clipboard.writeText(markdown);
|
| 271 |
+
showToast();
|
| 272 |
+
} catch (err) {
|
| 273 |
+
console.error("Copy failed:", err);
|
| 274 |
+
}
|
| 275 |
+
});
|
| 276 |
+
|
| 277 |
+
function showToast() {
|
| 278 |
+
toast.hidden = false;
|
| 279 |
+
// Force reflow before adding class for animation
|
| 280 |
+
toast.offsetHeight;
|
| 281 |
+
toast.classList.add("show");
|
| 282 |
+
setTimeout(() => {
|
| 283 |
+
toast.classList.remove("show");
|
| 284 |
+
setTimeout(() => { toast.hidden = true; }, 300);
|
| 285 |
+
}, 2000);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
/* --- Review flow --- */
|
| 289 |
|
| 290 |
let currentSource = null;
|
|
|
|
| 317 |
tabButtons.forEach((btn) => {
|
| 318 |
btn.classList.remove("has-content", "streaming");
|
| 319 |
});
|
| 320 |
+
issueBadge.hidden = true;
|
| 321 |
+
copyBtn.hidden = true;
|
| 322 |
+
fullMarkdown = "";
|
| 323 |
|
| 324 |
if (!url) { showInputError("Please enter a GitHub file URL."); return; }
|
| 325 |
if (!GITHUB_BLOB_RE.test(url)) {
|
|
|
|
| 331 |
setLoading(true);
|
| 332 |
|
| 333 |
let markdown = "";
|
| 334 |
+
const apiUrl = `/api/review?url=${encodeURIComponent(url)}&brutality=${encodeURIComponent(brutalityLevel)}`;
|
| 335 |
+
const source = new EventSource(apiUrl);
|
| 336 |
currentSource = source;
|
| 337 |
|
| 338 |
source.addEventListener("meta", (e) => {
|
|
|
|
| 355 |
|
| 356 |
source.addEventListener("done", () => {
|
| 357 |
cleanup();
|
| 358 |
+
fullMarkdown = markdown;
|
| 359 |
// Final render with no streaming section
|
| 360 |
const sections = parseSections(markdown);
|
| 361 |
updateTabs(sections, null);
|
| 362 |
+
// Show badge and copy button
|
| 363 |
+
const counts = countIssuePriorities(markdown);
|
| 364 |
+
renderIssueBadge(counts);
|
| 365 |
+
copyBtn.hidden = false;
|
| 366 |
});
|
| 367 |
|
| 368 |
source.addEventListener("error", (e) => {
|
public/index.html
CHANGED
|
@@ -34,6 +34,13 @@
|
|
| 34 |
<p id="input-error" class="error" hidden></p>
|
| 35 |
</form>
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
<section id="samples">
|
| 38 |
<p class="samples-label">Try a sample:</p>
|
| 39 |
<div class="samples-grid">
|
|
@@ -98,6 +105,8 @@
|
|
| 98 |
<span id="meta-branch"></span>
|
| 99 |
</div>
|
| 100 |
|
|
|
|
|
|
|
| 101 |
<div id="tabs" hidden>
|
| 102 |
<nav class="tab-bar">
|
| 103 |
<button class="tab active" data-section="summary">Summary</button>
|
|
@@ -106,11 +115,14 @@
|
|
| 106 |
<button class="tab" data-section="security">Security</button>
|
| 107 |
<button class="tab" data-section="suggestions">Suggestions</button>
|
| 108 |
<button class="tab" data-section="verdicts">Verdicts</button>
|
|
|
|
| 109 |
</nav>
|
| 110 |
<div id="tab-content" class="tab-content"></div>
|
| 111 |
</div>
|
| 112 |
|
| 113 |
<div id="stream-error" class="error" hidden></div>
|
|
|
|
|
|
|
| 114 |
</main>
|
| 115 |
|
| 116 |
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
|
|
|
| 34 |
<p id="input-error" class="error" hidden></p>
|
| 35 |
</form>
|
| 36 |
|
| 37 |
+
<div class="brutality-selector">
|
| 38 |
+
<span class="brutality-label">Tone:</span>
|
| 39 |
+
<button type="button" class="brutality-btn" data-level="gentle">Gentle</button>
|
| 40 |
+
<button type="button" class="brutality-btn active" data-level="standard">Standard</button>
|
| 41 |
+
<button type="button" class="brutality-btn" data-level="brutal">Brutal</button>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
<section id="samples">
|
| 45 |
<p class="samples-label">Try a sample:</p>
|
| 46 |
<div class="samples-grid">
|
|
|
|
| 105 |
<span id="meta-branch"></span>
|
| 106 |
</div>
|
| 107 |
|
| 108 |
+
<div id="issue-badge" hidden></div>
|
| 109 |
+
|
| 110 |
<div id="tabs" hidden>
|
| 111 |
<nav class="tab-bar">
|
| 112 |
<button class="tab active" data-section="summary">Summary</button>
|
|
|
|
| 115 |
<button class="tab" data-section="security">Security</button>
|
| 116 |
<button class="tab" data-section="suggestions">Suggestions</button>
|
| 117 |
<button class="tab" data-section="verdicts">Verdicts</button>
|
| 118 |
+
<button type="button" id="copy-btn" hidden>Copy Review</button>
|
| 119 |
</nav>
|
| 120 |
<div id="tab-content" class="tab-content"></div>
|
| 121 |
</div>
|
| 122 |
|
| 123 |
<div id="stream-error" class="error" hidden></div>
|
| 124 |
+
|
| 125 |
+
<div id="toast" hidden>Copied to clipboard!</div>
|
| 126 |
</main>
|
| 127 |
|
| 128 |
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
public/style.css
CHANGED
|
@@ -415,3 +415,129 @@ h1 {
|
|
| 415 |
@keyframes spin {
|
| 416 |
to { transform: rotate(360deg); }
|
| 417 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
@keyframes spin {
|
| 416 |
to { transform: rotate(360deg); }
|
| 417 |
}
|
| 418 |
+
|
| 419 |
+
/* ---------- Brutality Selector ---------- */
|
| 420 |
+
|
| 421 |
+
.brutality-selector {
|
| 422 |
+
display: flex;
|
| 423 |
+
align-items: center;
|
| 424 |
+
gap: 8px;
|
| 425 |
+
margin-top: 12px;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.brutality-label {
|
| 429 |
+
font-size: 0.85rem;
|
| 430 |
+
color: var(--text-muted);
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.brutality-btn {
|
| 434 |
+
padding: 6px 14px;
|
| 435 |
+
font-size: 0.82rem;
|
| 436 |
+
font-weight: 500;
|
| 437 |
+
color: var(--text-muted);
|
| 438 |
+
background: var(--surface);
|
| 439 |
+
border: 1px solid var(--border);
|
| 440 |
+
border-radius: var(--radius);
|
| 441 |
+
cursor: pointer;
|
| 442 |
+
transition: all 0.15s;
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
.brutality-btn:hover {
|
| 446 |
+
color: var(--text);
|
| 447 |
+
border-color: var(--accent);
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
.brutality-btn.active {
|
| 451 |
+
color: #fff;
|
| 452 |
+
background: var(--accent);
|
| 453 |
+
border-color: var(--accent);
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
/* ---------- Issue Badge ---------- */
|
| 457 |
+
|
| 458 |
+
#issue-badge {
|
| 459 |
+
display: flex;
|
| 460 |
+
gap: 16px;
|
| 461 |
+
flex-wrap: wrap;
|
| 462 |
+
margin-top: 16px;
|
| 463 |
+
padding: 10px 14px;
|
| 464 |
+
background: var(--surface);
|
| 465 |
+
border: 1px solid var(--border);
|
| 466 |
+
border-radius: var(--radius);
|
| 467 |
+
font-size: 0.85rem;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.badge-item {
|
| 471 |
+
display: flex;
|
| 472 |
+
align-items: center;
|
| 473 |
+
gap: 6px;
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
.badge-dot {
|
| 477 |
+
width: 10px;
|
| 478 |
+
height: 10px;
|
| 479 |
+
border-radius: 50%;
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
.badge-dot.critical { background: #f85149; }
|
| 483 |
+
.badge-dot.high { background: #f0883e; }
|
| 484 |
+
.badge-dot.medium { background: #d29922; }
|
| 485 |
+
.badge-dot.low { background: #8b949e; }
|
| 486 |
+
|
| 487 |
+
.badge-count {
|
| 488 |
+
font-weight: 600;
|
| 489 |
+
color: var(--text);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
.badge-label {
|
| 493 |
+
color: var(--text-muted);
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
/* ---------- Copy Button ---------- */
|
| 497 |
+
|
| 498 |
+
#copy-btn {
|
| 499 |
+
margin-left: auto;
|
| 500 |
+
padding: 6px 14px;
|
| 501 |
+
font-size: 0.82rem;
|
| 502 |
+
font-weight: 500;
|
| 503 |
+
color: var(--text-muted);
|
| 504 |
+
background: var(--surface);
|
| 505 |
+
border: 1px solid var(--border);
|
| 506 |
+
border-radius: var(--radius);
|
| 507 |
+
cursor: pointer;
|
| 508 |
+
transition: all 0.15s;
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
#copy-btn:hover {
|
| 512 |
+
color: var(--text);
|
| 513 |
+
border-color: var(--accent);
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
#copy-btn:active {
|
| 517 |
+
background: var(--accent);
|
| 518 |
+
color: #fff;
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
/* ---------- Toast Notification ---------- */
|
| 522 |
+
|
| 523 |
+
#toast {
|
| 524 |
+
position: fixed;
|
| 525 |
+
bottom: 24px;
|
| 526 |
+
left: 50%;
|
| 527 |
+
transform: translateX(-50%) translateY(100px);
|
| 528 |
+
padding: 12px 24px;
|
| 529 |
+
background: var(--accent);
|
| 530 |
+
color: #fff;
|
| 531 |
+
font-size: 0.9rem;
|
| 532 |
+
font-weight: 500;
|
| 533 |
+
border-radius: var(--radius);
|
| 534 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
| 535 |
+
opacity: 0;
|
| 536 |
+
transition: transform 0.3s ease, opacity 0.3s ease;
|
| 537 |
+
z-index: 1000;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
#toast.show {
|
| 541 |
+
transform: translateX(-50%) translateY(0);
|
| 542 |
+
opacity: 1;
|
| 543 |
+
}
|
server.js
CHANGED
|
@@ -27,11 +27,22 @@ function parseGitHubUrl(url) {
|
|
| 27 |
return { owner, repo, branch, path, rawUrl, ext };
|
| 28 |
}
|
| 29 |
|
| 30 |
-
|
|
|
|
|
|
|
| 31 |
|
| 32 |
-
Your tone is
|
| 33 |
|
| 34 |
-
The
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
## Summary
|
| 37 |
What this file does and whether it has any business existing in its current form. Be blunt.
|
|
@@ -124,6 +135,11 @@ Rules:
|
|
| 124 |
- NO DUPLICATES: Each issue appears in ONE section only. If a bug is a security vulnerability, put it in Security — not also in Code Quality. If a performance fix is also a suggestion, put it in Performance only. Pick the most relevant section and move on.
|
| 125 |
- PRIORITY TAGS: Every issue MUST start with [CRITICAL], [HIGH], [MEDIUM], or [LOW]. CRITICAL = crashes, data loss, security exploits. HIGH = significant bugs or performance problems. MEDIUM = code smells, minor inefficiencies. LOW = style nitpicks, minor improvements.`;
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
function buildUserMessage(meta, code) {
|
| 128 |
const numbered = code
|
| 129 |
.split("\n")
|
|
@@ -147,10 +163,16 @@ app.use(express.static(join(__dirname, "public")));
|
|
| 147 |
|
| 148 |
app.get("/api/review", async (req, res) => {
|
| 149 |
const url = req.query.url;
|
|
|
|
|
|
|
| 150 |
if (!url) {
|
| 151 |
return res.status(400).json({ error: "Missing url parameter" });
|
| 152 |
}
|
| 153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
const meta = parseGitHubUrl(url);
|
| 155 |
if (!meta) {
|
| 156 |
return res
|
|
@@ -206,10 +228,12 @@ app.get("/api/review", async (req, res) => {
|
|
| 206 |
send("meta", { owner: meta.owner, repo: meta.repo, branch: meta.branch, path: meta.path });
|
| 207 |
|
| 208 |
// Stream from OpenRouter
|
| 209 |
-
console.log(`[review] Requesting review from OpenRouter…`);
|
| 210 |
const controller = new AbortController();
|
| 211 |
const timeout = setTimeout(() => controller.abort(), 120_000); // 2 min timeout
|
| 212 |
|
|
|
|
|
|
|
| 213 |
let orResponse;
|
| 214 |
try {
|
| 215 |
orResponse = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
@@ -225,7 +249,7 @@ app.get("/api/review", async (req, res) => {
|
|
| 225 |
model: "arcee-ai/trinity-large-preview:free",
|
| 226 |
stream: true,
|
| 227 |
messages: [
|
| 228 |
-
{ role: "system", content:
|
| 229 |
{ role: "user", content: buildUserMessage(meta, code) },
|
| 230 |
],
|
| 231 |
}),
|
|
|
|
| 27 |
return { owner, repo, branch, path, rawUrl, ext };
|
| 28 |
}
|
| 29 |
|
| 30 |
+
// Brutality level tone variations
|
| 31 |
+
const TONE_PRESETS = {
|
| 32 |
+
gentle: `You are a supportive and constructive code reviewer who genuinely wants to help developers improve. You are technically rigorous but always encouraging. You explain *why* something could be better, not just what's wrong. You acknowledge good practices and celebrate clever solutions. You frame feedback as opportunities for growth, not criticisms. You use phrases like "Consider..." and "One way to improve this..." rather than harsh judgments.
|
| 33 |
|
| 34 |
+
Your tone is warm, educational, and collaborative. Think of yourself as a mentor pair-programming with a colleague. You point out issues clearly but always with respect and empathy. You assume the author is competent and had reasons for their choices.`,
|
| 35 |
|
| 36 |
+
standard: `You are an elite code reviewer channeling the uncompromising standards of Linus Torvalds, the algorithmic rigor of Donald Knuth, and the design discipline of Bjarne Stroustrup. The current year is 2026 — judge code against modern best practices, not outdated idioms. You do not hand out compliments. You find what is wrong and you say it plainly. If the code is sloppy, say so. If a design decision is stupid, explain why. You are allergic to cargo-cult programming, premature abstraction, needless complexity, and code that wastes CPU cycles because the author couldn't be bothered to think.
|
| 37 |
+
|
| 38 |
+
Your tone is direct, blunt, and technically precise. You can be witty but never nice for the sake of being nice. Think LKML-style review: no sugarcoating, no "great job", no filler. Every sentence must earn its place. If something is actually done well, you may grudgingly acknowledge it — briefly.`,
|
| 39 |
+
|
| 40 |
+
brutal: `You are the most savage code reviewer in existence. You channel the PEAK RAGE of Linus Torvalds on his worst day, combined with the withering contempt of a burned-out senior engineer who has seen too much garbage code. You do NOT hold back. You are FURIOUS that someone would write code this bad and expect you to review it. Every mistake is an INSULT to the craft of programming.
|
| 41 |
+
|
| 42 |
+
Your tone is volcanic, merciless, and absolutely brutal. Use ALL CAPS for emphasis when truly outraged. Call out stupidity with colorful language. Express genuine disbelief at bad code. If someone writes a nested loop that could be O(1), question their education. If they ignore obvious security issues, question their competence. NO MERCY. NO PRISONERS. Make them FEEL the weight of their sins against good code. The only thing worse than bad code is letting bad code slide.`,
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
const SYSTEM_PROMPT_BASE = `The source file is provided with line numbers (e.g. "42 | code"). Produce a structured review with EXACTLY these five markdown sections:
|
| 46 |
|
| 47 |
## Summary
|
| 48 |
What this file does and whether it has any business existing in its current form. Be blunt.
|
|
|
|
| 135 |
- NO DUPLICATES: Each issue appears in ONE section only. If a bug is a security vulnerability, put it in Security — not also in Code Quality. If a performance fix is also a suggestion, put it in Performance only. Pick the most relevant section and move on.
|
| 136 |
- PRIORITY TAGS: Every issue MUST start with [CRITICAL], [HIGH], [MEDIUM], or [LOW]. CRITICAL = crashes, data loss, security exploits. HIGH = significant bugs or performance problems. MEDIUM = code smells, minor inefficiencies. LOW = style nitpicks, minor improvements.`;
|
| 137 |
|
| 138 |
+
function buildSystemPrompt(brutalityLevel) {
|
| 139 |
+
const tone = TONE_PRESETS[brutalityLevel] || TONE_PRESETS.standard;
|
| 140 |
+
return `${tone}\n\n${SYSTEM_PROMPT_BASE}`;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
function buildUserMessage(meta, code) {
|
| 144 |
const numbered = code
|
| 145 |
.split("\n")
|
|
|
|
| 163 |
|
| 164 |
app.get("/api/review", async (req, res) => {
|
| 165 |
const url = req.query.url;
|
| 166 |
+
const brutality = req.query.brutality || "standard";
|
| 167 |
+
|
| 168 |
if (!url) {
|
| 169 |
return res.status(400).json({ error: "Missing url parameter" });
|
| 170 |
}
|
| 171 |
|
| 172 |
+
// Validate brutality level
|
| 173 |
+
const validLevels = ["gentle", "standard", "brutal"];
|
| 174 |
+
const brutalityLevel = validLevels.includes(brutality) ? brutality : "standard";
|
| 175 |
+
|
| 176 |
const meta = parseGitHubUrl(url);
|
| 177 |
if (!meta) {
|
| 178 |
return res
|
|
|
|
| 228 |
send("meta", { owner: meta.owner, repo: meta.repo, branch: meta.branch, path: meta.path });
|
| 229 |
|
| 230 |
// Stream from OpenRouter
|
| 231 |
+
console.log(`[review] Requesting review from OpenRouter (brutality: ${brutalityLevel})…`);
|
| 232 |
const controller = new AbortController();
|
| 233 |
const timeout = setTimeout(() => controller.abort(), 120_000); // 2 min timeout
|
| 234 |
|
| 235 |
+
const systemPrompt = buildSystemPrompt(brutalityLevel);
|
| 236 |
+
|
| 237 |
let orResponse;
|
| 238 |
try {
|
| 239 |
orResponse = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
|
|
| 249 |
model: "arcee-ai/trinity-large-preview:free",
|
| 250 |
stream: true,
|
| 251 |
messages: [
|
| 252 |
+
{ role: "system", content: systemPrompt },
|
| 253 |
{ role: "user", content: buildUserMessage(meta, code) },
|
| 254 |
],
|
| 255 |
}),
|