| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> |
| <meta charset="UTF-8"/> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
| <title>CommitLens β AI Code Review</title> |
| <link rel="preconnect" href="https://fonts.googleapis.com"/> |
| <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,900;1,400&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet"/> |
| |
| <script type="module"> |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; |
| window._gradioClient = Client; |
| </script> |
| <style> |
| *{margin:0;padding:0;box-sizing:border-box} |
| html,body{background:#06060e;width:100%;min-height:100vh;font-family:'Space Mono',monospace;color:#e6edf3} |
|
|
| /* ββ Layout ββ */ |
| #root{width:100%;min-height:100vh;display:flex;flex-direction:column;position:relative;overflow:hidden} |
| canvas{position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:0} |
|
|
| .ui{flex:1;display:grid;grid-template-columns:1fr 1fr;z-index:2;position:relative;padding-bottom:44px} |
| @media(max-width:780px){.ui{grid-template-columns:1fr}} |
|
|
| /* ββ Left panel ββ */ |
| .left{padding:52px 44px;display:flex;flex-direction:column;justify-content:center;gap:28px} |
|
|
| .pill{display:inline-flex;align-items:center;gap:8px;border:0.5px solid #1a3a1a;border-radius:100px;padding:6px 16px;width:fit-content} |
| .pill-dot{width:5px;height:5px;border-radius:50%;background:#3fb950;animation:blink 2s infinite} |
| .pill-text{font-size:9px;letter-spacing:.2em;color:#3fb950} |
| @keyframes blink{0%,100%{opacity:1}50%{opacity:.15}} |
|
|
| .headline{font-family:'Playfair Display',serif;line-height:1} |
| .hl1{display:block;font-style:italic;font-weight:400;font-size:46px;color:#555;letter-spacing:-.01em} |
| .hl2{display:block;font-style:normal;font-weight:900;font-size:58px;color:#fff;letter-spacing:-.02em;margin-top:2px} |
|
|
| .desc{font-size:11px;color:#444;line-height:2;max-width:290px;letter-spacing:.02em} |
|
|
| /* ββ Form ββ */ |
| .form-wrap{display:flex;flex-direction:column;gap:10px} |
| .field-lbl{font-size:9px;letter-spacing:.2em;color:#666;text-transform:uppercase;margin-bottom:4px} |
| .irow{display:flex;border:0.5px solid #1a1a2a;border-radius:9px;overflow:hidden;background:#0a0a14} |
| .irow input{flex:1;height:44px;background:transparent;border:none;color:#e6edf3;font-size:12px;padding:0 16px;font-family:'Space Mono',monospace;outline:none} |
| .irow input::placeholder{color:#2a2a3a} |
| .run-btn{height:44px;padding:0 22px;background:#238636;border:none;color:#fff;font-size:10px;font-family:'Space Mono',monospace;cursor:pointer;letter-spacing:.1em;transition:background .15s,opacity .15s;white-space:nowrap} |
| .run-btn:hover:not(:disabled){background:#2ea043} |
| .run-btn:disabled{opacity:.45;cursor:not-allowed} |
| .tok-row{display:flex;align-items:center;gap:10px} |
| .tok-lbl{font-size:9px;color:#444;letter-spacing:.15em;flex-shrink:0} |
| .tok-row input{flex:1;height:36px;background:#0a0a14;border:0.5px solid #1a1a2a;border-radius:7px;color:#888;font-size:11px;padding:0 12px;font-family:'Space Mono',monospace;outline:none} |
| .tok-row input::placeholder{color:#252535} |
|
|
| /* ββ Status bar ββ */ |
| .status{font-size:9px;letter-spacing:.15em;color:#1e3e2e;height:18px;transition:color .3s} |
| .status.active{color:#3fb950} |
| .status.error{color:#f85149} |
|
|
| /* ββ Output area ββ */ |
| .out-area{display:flex;flex-direction:column;gap:0;min-height:320px;background:rgba(10,10,20,.75);border:1px solid #1f2937;border-radius:12px;padding:14px;margin-top:8px} |
| .tabs{display:flex;gap:0;border-bottom:0.5px solid #0e0e18;margin-bottom:14px} |
| .tb{font-size:10px;letter-spacing:.16em;color:#8b949e;cursor:pointer;padding-bottom:10px;margin-right:22px;border-bottom:1px solid transparent;transition:color .15s;text-transform:uppercase;background:none;border-top:none;border-left:none;border-right:none;font-family:'Space Mono',monospace} |
| .tb.on{color:#58a6ff;border-bottom-color:#58a6ff} |
|
|
| /* Per-file pane */ |
| #pane-f{display:none} |
| #pane-f.on{display:block;max-height:420px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#30363d transparent} |
| .frow{display:flex;align-items:flex-start;gap:9px;padding:8px 0;border-bottom:0.5px solid #0b0b14;cursor:pointer;transition:background .1s;border-radius:4px} |
| .frow:last-child{border:none} |
| .frow:hover{background:#0d0d18} |
| .dot{width:5px;height:5px;border-radius:50%;flex-shrink:0;margin-top:3px} |
| .file-body{display:flex;flex-direction:column;gap:3px;flex:1;min-width:0} |
| .fn{font-size:12px;color:#e6edf3;letter-spacing:.02em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:700} |
| .fd{font-size:11px;color:#b8c5d6;font-style:italic;line-height:1.7;white-space:pre-wrap;word-break:break-word} |
| .fd.expanded{color:#d7e3f4} |
|
|
| /* Report pane */ |
| #pane-r{display:none} |
| #pane-r.on{display:block;max-height:420px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#30363d transparent} |
| .rp{font-size:12px;color:#d6dee8;line-height:1.9;white-space:pre-wrap;word-break:break-word} |
| .rp em{color:#58a6ff;font-style:normal} |
|
|
| /* Empty state */ |
| .empty-state{display:flex;flex-direction:column;gap:6px;padding:20px 0} |
| .es-line{height:10px;border-radius:3px;background:#0d0d18;animation:shimmer 2s infinite} |
| .es-line:nth-child(1){width:80%} |
| .es-line:nth-child(2){width:60%} |
| .es-line:nth-child(3){width:70%} |
| @keyframes shimmer{0%,100%{opacity:.3}50%{opacity:.6}} |
|
|
| /* ββ Right panel β hero git diagram ββ */ |
| .right{position:relative;display:flex;align-items:center;justify-content:center;overflow:hidden} |
| #hero-svg{position:relative;z-index:2} |
|
|
| /* ββ Ticker ββ */ |
| .ticker{position:fixed;bottom:0;left:0;right:0;z-index:10;padding:10px 0;border-top:0.5px solid #0c0c16;overflow:hidden;background:#06060e} |
| .tk-track{display:flex;white-space:nowrap;animation:slide 36s linear infinite} |
| .ti{font-size:9px;letter-spacing:.18em;color:#222;padding:0 30px;text-transform:uppercase;flex-shrink:0} |
| .ti b{color:#1e2e3e} |
| @keyframes slide{from{transform:translateX(0)}to{transform:translateX(-50%)}} |
|
|
| /* ββ Detail overlay ββ */ |
| #overlay{display:none;position:fixed;inset:0;background:rgba(6,6,14,.92);z-index:100;padding:40px;overflow-y:auto} |
| #overlay.on{display:flex;flex-direction:column;gap:20px} |
| .ov-head{display:flex;align-items:center;justify-content:space-between} |
| .ov-title{font-size:14px;color:#58a6ff;letter-spacing:.05em} |
| .ov-close{background:none;border:0.5px solid #1a1a2a;border-radius:6px;color:#666;font-size:11px;font-family:'Space Mono',monospace;padding:6px 14px;cursor:pointer;letter-spacing:.1em} |
| .ov-close:hover{color:#e6edf3;border-color:#3a3a5a} |
| .ov-body{font-size:12px;color:#8a9ab0;line-height:1.9;white-space:pre-wrap;word-break:break-word;max-width:760px} |
|
|
| .report-actions{ |
| display:flex; |
| justify-content:flex-end; |
| margin-bottom:12px; |
| } |
|
|
| .report-actions button{ |
| background:#238636; |
| border:none; |
| color:white; |
| padding:8px 14px; |
| border-radius:6px; |
| cursor:pointer; |
| font-family:'Space Mono',monospace; |
| font-size:10px; |
| letter-spacing:.08em; |
| } |
|
|
| .report-actions button:hover{ |
| background:#2ea043; |
| } |
|
|
| .markdown-body{ |
| color:#d6dee8; |
| line-height:1.8; |
| font-size:13px; |
| } |
|
|
| .markdown-body h2{ |
| color:#58a6ff; |
| margin:20px 0 10px; |
| font-size:18px; |
| } |
|
|
| .markdown-body h3{ |
| color:#3fb950; |
| margin:16px 0 8px; |
| font-size:15px; |
| } |
|
|
| .markdown-body ul{ |
| padding-left:20px; |
| } |
|
|
| .markdown-body li{ |
| margin:6px 0; |
| } |
|
|
| .markdown-body p{ |
| margin:10px 0; |
| } |
|
|
| .markdown-body code{ |
| background:#0d1117; |
| padding:2px 6px; |
| border-radius:4px; |
| } |
| </style> |
| </head> |
| <body> |
|
|
| <div id="root"> |
| <canvas id="cv"></canvas> |
|
|
| <div class="ui"> |
| |
| <div class="left"> |
| <div class="pill"><span class="pill-dot"></span><span class="pill-text">AI Β· CODE REVIEW Β· LIVE</span></div> |
|
|
| <div class="headline"> |
| <span class="hl1">the commits are</span> |
| <span class="hl2">already<br>judged.</span> |
| </div> |
|
|
| <p class="desc">Drop any GitHub repo. AI fetches the latest commit, reviews every changed file, returns structured markdown β instantly.</p> |
|
|
| <div class="form-wrap"> |
| <div> |
| <div class="field-lbl">Repository</div> |
| <div class="irow"> |
| <input type="text" placeholder="https://github.com/owner/repo" id="ri"/> |
| <button class="run-btn" id="runBtn" onclick="go()">β RUN</button> |
| </div> |
| </div> |
| <div class="tok-row"> |
| <span class="tok-lbl">TOKEN</span> |
| <input type="password" id="tok" placeholder="ghp_β’β’β’β’β’β’β’β’β’β’ (optional)"/> |
| </div> |
| </div> |
|
|
| <div class="status" id="status">READY</div> |
|
|
| <div class="out-area"> |
| <div class="tabs"> |
| <button class="tb on" onclick="sw(this,'f')">Per-file</button> |
| <button class="tb" onclick="sw(this,'r')">Report</button> |
| </div> |
|
|
| <div id="pane-f" class="on"> |
| <div class="empty-state" id="emptyF"> |
| <div class="es-line"></div> |
| <div class="es-line"></div> |
| <div class="es-line"></div> |
| </div> |
| </div> |
|
|
| <div id="pane-r"> |
| <div class="empty-state" id="emptyR"> |
| <div class="es-line"></div> |
| <div class="es-line"></div> |
| <div class="es-line"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="right"> |
| <svg id="hero-svg" viewBox="0 0 340 500" width="340" height="500" xmlns="http://www.w3.org/2000/svg"> |
| <defs> |
| <radialGradient id="ng" cx="50%" cy="50%" r="50%"> |
| <stop offset="0%" stop-color="#1a4a2e" stop-opacity="1"/> |
| <stop offset="100%" stop-color="#0a1a12" stop-opacity="1"/> |
| </radialGradient> |
| <radialGradient id="bg2" cx="50%" cy="50%" r="50%"> |
| <stop offset="0%" stop-color="#1a2a4a" stop-opacity="1"/> |
| <stop offset="100%" stop-color="#0a1020" stop-opacity="1"/> |
| </radialGradient> |
| <radialGradient id="rg" cx="50%" cy="50%" r="50%"> |
| <stop offset="0%" stop-color="#4a1a1a" stop-opacity="1"/> |
| <stop offset="100%" stop-color="#200a0a" stop-opacity="1"/> |
| </radialGradient> |
| </defs> |
| |
| <line x1="170" y1="40" x2="170" y2="460" stroke="#0f1f0f" stroke-width="1.5"/> |
| <line x1="170" y1="140" x2="260" y2="220" stroke="#0f1a0f" stroke-width="1"/> |
| <line x1="260" y1="220" x2="260" y2="340" stroke="#0f1a0f" stroke-width="1"/> |
| <line x1="260" y1="340" x2="170" y2="380" stroke="#0f1a0f" stroke-width="1"/> |
| <line x1="170" y1="200" x2="90" y2="260" stroke="#0f0f1a" stroke-width="1"/> |
| <line x1="90" y1="260" x2="90" y2="320" stroke="#0f0f1a" stroke-width="1"/> |
| <line x1="90" y1="320" x2="170" y2="340" stroke="#0f0f1a" stroke-width="1"/> |
| |
| <circle cx="170" cy="60" r="14" fill="url(#ng)" stroke="#1a3a1a" stroke-width="0.5"/> |
| <text x="170" y="64" text-anchor="middle" font-size="8" fill="#3fb950" font-family="monospace">a1b2c</text> |
| <circle cx="170" cy="140" r="14" fill="url(#ng)" stroke="#1a3a1a" stroke-width="0.5"/> |
| <text x="170" y="144" text-anchor="middle" font-size="8" fill="#3fb950" font-family="monospace">d3e4f</text> |
| <circle cx="170" cy="200" r="14" fill="url(#ng)" stroke="#1a3a1a" stroke-width="0.5"/> |
| <text x="170" y="204" text-anchor="middle" font-size="8" fill="#3fb950" font-family="monospace">g5h6i</text> |
| <circle cx="170" cy="340" r="14" fill="url(#ng)" stroke="#1a3a1a" stroke-width="0.5"/> |
| <text x="170" y="344" text-anchor="middle" font-size="8" fill="#3fb950" font-family="monospace">merge</text> |
| <circle cx="170" cy="420" r="14" fill="url(#ng)" stroke="#1a3a1a" stroke-width="0.5"/> |
| <text x="170" y="424" text-anchor="middle" font-size="8" fill="#3fb950" font-family="monospace">HEAD</text> |
| |
| <circle cx="260" cy="240" r="12" fill="url(#bg2)" stroke="#1a2a4a" stroke-width="0.5"/> |
| <text x="260" y="244" text-anchor="middle" font-size="7" fill="#58a6ff" font-family="monospace">feat</text> |
| <circle cx="260" cy="300" r="12" fill="url(#bg2)" stroke="#1a2a4a" stroke-width="0.5"/> |
| <text x="260" y="304" text-anchor="middle" font-size="7" fill="#58a6ff" font-family="monospace">feat</text> |
| |
| <circle cx="90" cy="270" r="12" fill="url(#rg)" stroke="#4a1a1a" stroke-width="0.5"/> |
| <text x="90" y="274" text-anchor="middle" font-size="7" fill="#f85149" font-family="monospace">fix</text> |
| <circle cx="90" cy="310" r="12" fill="url(#rg)" stroke="#4a1a1a" stroke-width="0.5"/> |
| <text x="90" y="314" text-anchor="middle" font-size="7" fill="#f85149" font-family="monospace">fix</text> |
| |
| <text x="186" y="56" font-size="9" fill="#1a3a1a" font-family="monospace">main</text> |
| <text x="276" y="236" font-size="9" fill="#1a2a4a" font-family="monospace">feature/auth</text> |
| <text x="36" y="266" font-size="9" fill="#3a1a1a" font-family="monospace">fix/jwt</text> |
| |
| <rect x="136" y="438" width="68" height="18" rx="9" fill="#0f2f1f" stroke="#1a4a2a" stroke-width="0.5"/> |
| <text x="170" y="451" text-anchor="middle" font-size="8" fill="#3fb950" font-family="monospace" letter-spacing=".08em">β HEAD</text> |
| |
| <rect x="100" y="10" width="140" height="22" rx="5" fill="#0a0a14" stroke="#1a1a2a" stroke-width="0.5"/> |
| <text x="170" y="25" text-anchor="middle" font-size="8" fill="#333" font-family="monospace" letter-spacing=".12em">AI REVIEWING...</text> |
| <rect id="prog" x="100" y="10" width="0" height="22" rx="5" fill="#238636" opacity=".15"/> |
| </svg> |
| </div> |
| </div> |
|
|
| |
| <div class="ticker"> |
| <div class="tk-track" id="tk"></div> |
| </div> |
| </div> |
|
|
| |
| <div id="overlay"> |
| <div class="ov-head"> |
| <span class="ov-title" id="ovTitle"></span> |
| <button class="ov-close" onclick="closeOverlay()">β CLOSE</button> |
| </div> |
| <pre class="ov-body" id="ovBody"></pre> |
| </div> |
|
|
| <script> |
| |
| const tks = [ |
| 'git commit -m "reviewed by AI"', |
| '<b>β</b> live code review', |
| 'feat: bulk import Β· fix: JWT expiry Β· chore: remove yaml', |
| 'diff --git a/src b/src', |
| 'HEAD~1 Β· origin/main Β· refs/heads/feat', |
| 'mellum 2 Β· per-file summaries Β· final report', |
| 'git push Β· <b>shipped</b>', |
| 'five files Β· zero context switching' |
| ]; |
| const tkEl = document.getElementById('tk'); |
| tkEl.innerHTML = [...tks, ...tks, ...tks, ...tks, ...tks, ...tks] |
| .map(s => `<span class="ti">${s} /</span>`).join(''); |
| |
| |
| const cv = document.getElementById('cv'), cc = cv.getContext('2d'); |
| let W, H; |
| function rsz() { W = cv.width = window.innerWidth; H = cv.height = window.innerHeight; } |
| |
| const CMDS = ['feat: auth refactor','fix: JWT expiry','chore: rm yaml','docs: update readme','test: add coverage','perf: batch queries','refactor: middleware','feat: bulk import','ci: update actions','build: bump deps']; |
| const EXTS = ['.ts','.py','.yml','.go','.rs','.tsx','.sql','.json','.sh','.md']; |
| |
| class Particle { |
| constructor(atBottom) { this.reset(atBottom); } |
| reset(atBottom) { |
| this.x = Math.random() * W; |
| this.y = atBottom ? H + 30 : Math.random() * H; |
| this.vx = (Math.random() - .5) * .25; |
| this.vy = -(Math.random() * .35 + .06); |
| this.a = Math.random() * .4 + .05; |
| this.type = Math.floor(Math.random() * 4); |
| this.ph = Math.random() * Math.PI * 2; |
| this.ps = Math.random() * .015 + .004; |
| this.sz = Math.random() * 2 + 1; |
| this.hue = [210, 142, 38, 275][Math.floor(Math.random() * 4)]; |
| this.txt = this.type === 1 ? CMDS[Math.floor(Math.random() * CMDS.length)] : |
| this.type === 2 ? `src/module${EXTS[Math.floor(Math.random() * EXTS.length)]}` : |
| this.type === 3 ? `+${Math.floor(Math.random() * 40) + 1} -${Math.floor(Math.random() * 20) + 1}` : null; |
| this.w = this.txt ? (this.txt.length * 6.5 + 20) : 0; |
| } |
| draw(ctx) { |
| this.ph += this.ps; |
| const a = this.a * (.5 + .5 * Math.sin(this.ph)); |
| ctx.save(); ctx.globalAlpha = a; |
| if (this.type === 0) { |
| ctx.beginPath(); ctx.arc(this.x, this.y, this.sz, 0, Math.PI * 2); |
| ctx.fillStyle = `hsl(${this.hue},40%,35%)`; ctx.fill(); |
| } else { |
| const col = this.type === 1 ? 'hsl(142,30%,22%)' : this.type === 2 ? 'hsl(210,30%,16%)' : 'hsl(38,30%,18%)'; |
| const tcol = this.type === 1 ? '#1a3a1a' : this.type === 2 ? '#1a2a3a' : '#3a2a0a'; |
| ctx.fillStyle = col; |
| const h = 16, r = 4; |
| ctx.beginPath(); |
| ctx.moveTo(this.x + r, this.y - h / 2); |
| ctx.lineTo(this.x + this.w - r, this.y - h / 2); |
| ctx.arcTo(this.x + this.w, this.y - h / 2, this.x + this.w, this.y, r); |
| ctx.arcTo(this.x + this.w, this.y + h / 2, this.x + this.w - r, this.y + h / 2, r); |
| ctx.lineTo(this.x + r, this.y + h / 2); |
| ctx.arcTo(this.x, this.y + h / 2, this.x, this.y, r); |
| ctx.arcTo(this.x, this.y - h / 2, this.x + r, this.y - h / 2, r); |
| ctx.closePath(); ctx.fill(); |
| ctx.strokeStyle = tcol; ctx.lineWidth = .5; ctx.stroke(); |
| ctx.fillStyle = tcol; ctx.font = '9px monospace'; |
| ctx.fillText(this.txt, this.x + 10, this.y + 3); |
| } |
| ctx.restore(); |
| this.x += this.vx; this.y += this.vy; |
| } |
| dead() { return this.y < -30 || this.x < -220 || this.x > W + 220; } |
| } |
| |
| let pts = []; |
| rsz(); |
| for (let i = 0; i < 80; i++) pts.push(new Particle(false)); |
| function frame() { |
| cc.clearRect(0, 0, W, H); |
| pts.forEach(p => { p.draw(cc); if (p.dead()) p.reset(true); }); |
| requestAnimationFrame(frame); |
| } |
| frame(); |
| window.addEventListener('resize', rsz); |
| |
| |
| let prog = 0; |
| const progEl = document.getElementById('prog'); |
| setInterval(() => { |
| prog = (prog + .4) % 100; |
| progEl.setAttribute('width', prog * 1.4); |
| }, 60); |
| |
| |
| function sw(el, t) { |
| document.querySelectorAll('.tb').forEach(x => x.classList.remove('on')); |
| document.querySelectorAll('#pane-f,#pane-r').forEach(x => x.classList.remove('on')); |
| el.classList.add('on'); |
| document.getElementById('pane-' + t).classList.add('on'); |
| } |
| |
| |
| function setStatus(msg, cls = '') { |
| const el = document.getElementById('status'); |
| el.textContent = msg; |
| el.className = 'status ' + cls; |
| } |
| |
| |
| function dotColor(name) { |
| const ext = name.split('.').pop().toLowerCase(); |
| if (['ts','tsx','js','jsx'].includes(ext)) return '#58a6ff'; |
| if (['py','rb','go','rs'].includes(ext)) return '#3fb950'; |
| if (['yml','yaml','json','toml','env'].includes(ext)) return '#f85149'; |
| if (['md','txt','rst'].includes(ext)) return '#e3b341'; |
| return '#8a9ab0'; |
| } |
| |
| |
| function renderFiles(files) { |
| const pane = document.getElementById('pane-f'); |
| if (!files || files.length === 0) { |
| pane.innerHTML = '<div class="rp" style="color:#2a3a2a">No files changed.</div>'; |
| return; |
| } |
| pane.innerHTML = files.map((f, i) => ` |
| <div class="frow" onclick="showDetail(${i})" data-idx="${i}"> |
| <span class="dot" style="background:${dotColor(f.name)}"></span> |
| <div class="file-body"> |
| <span class="fn">${f.name}</span> |
| <span class="fd">${(f.summary || '').slice(0, 120)}${(f.summary || '').length > 120 ? 'β¦' : ''}</span> |
| </div> |
| </div> |
| `).join(''); |
| } |
| |
| window._currentReport = ""; |
| |
| function downloadReport() { |
| const html = ` |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <title>CommitLens Report</title> |
| |
| <style> |
| body{ |
| max-width:900px; |
| margin:40px auto; |
| padding:20px; |
| font-family:system-ui,sans-serif; |
| line-height:1.7; |
| } |
| h2{ |
| color:#2563eb; |
| } |
| h3{ |
| color:#16a34a; |
| } |
| code{ |
| background:#f3f4f6; |
| padding:2px 6px; |
| border-radius:4px; |
| } |
| </style> |
| |
| </head> |
| <body> |
| ${marked.parse(window._currentReport)} |
| </body> |
| </html> |
| `; |
| |
| const blob = new Blob( |
| [html], |
| { type: "text/html" } |
| ); |
| |
| const url = URL.createObjectURL(blob); |
| |
| const a = document.createElement("a"); |
| a.href = url; |
| a.download = "commitlens-report.html"; |
| a.click(); |
| |
| URL.revokeObjectURL(url); |
| } |
| |
| function renderReport(md) { |
| window._currentReport = md || ""; |
| |
| const pane = document.getElementById('pane-r'); |
| |
| pane.innerHTML = ` |
| <div class="report-actions"> |
| <button onclick="downloadReport()"> |
| β¬ DOWNLOAD REPORT |
| </button> |
| </div> |
| |
| <div class="rp markdown-body"> |
| ${marked.parse(md || "")} |
| </div> |
| `; |
| } |
| |
| function escHtml(s) { |
| return (s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); |
| } |
| |
| |
| let _currentFiles = []; |
| function showDetail(idx) { |
| const f = _currentFiles[idx]; |
| if (!f) return; |
| document.getElementById('ovTitle').textContent = f.name; |
| document.getElementById('ovBody').textContent = f.summary || ''; |
| document.getElementById('overlay').classList.add('on'); |
| } |
| function closeOverlay() { |
| document.getElementById('overlay').classList.remove('on'); |
| } |
| |
| |
| async function go() { |
| const url = document.getElementById('ri').value.trim(); |
| if (!url) return; |
| const token = document.getElementById('tok').value.trim(); |
| |
| const btn = document.getElementById('runBtn'); |
| btn.disabled = true; |
| btn.textContent = 'β RUNNING'; |
| setStatus('CONNECTING TO API...', 'active'); |
| |
| |
| document.getElementById('pane-f').innerHTML = ` |
| <div class="empty-state"> |
| <div class="es-line"></div><div class="es-line"></div><div class="es-line"></div> |
| </div>`; |
| document.getElementById('pane-r').innerHTML = ` |
| <div class="empty-state"> |
| <div class="es-line"></div><div class="es-line"></div><div class="es-line"></div> |
| </div>`; |
| |
| try { |
| |
| |
| const Client = window._gradioClient; |
| |
| |
| const client = await Client.connect(window.location.origin); |
| |
| setStatus('PIPELINE RUNNING β THIS MAY TAKE A FEW MINUTES...', 'active'); |
| |
| const result = await client.predict("/process_repo", { |
| repo_url: url, |
| token: token, |
| }); |
| |
| |
| const data = result.data[0]; |
| _currentFiles = data.files || []; |
| |
| renderFiles(_currentFiles); |
| renderReport(data.report || ''); |
| |
| setStatus(`β DONE β ${_currentFiles.length} FILE(S) REVIEWED`, 'active'); |
| } catch (err) { |
| console.error(err); |
| const msg = err?.message || String(err); |
| document.getElementById('pane-f').innerHTML = `<div class="rp" style="color:#f85149">${escHtml(msg)}</div>`; |
| document.getElementById('pane-r').innerHTML = `<div class="rp" style="color:#f85149">${escHtml(msg)}</div>`; |
| setStatus('ERROR: ' + msg.slice(0, 60), 'error'); |
| } finally { |
| btn.disabled = false; |
| btn.textContent = 'β RUN'; |
| } |
| } |
| |
| |
| document.getElementById('ri').addEventListener('keydown', e => { |
| if (e.key === 'Enter') go(); |
| }); |
| </script> |
| </body> |
| </html> |