Spaces:
Running on Zero
Running on Zero
| <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"/> | |
| <!-- Gradio JS client for calling @app.api() endpoints --> | |
| <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"> | |
| <!-- LEFT --> | |
| <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> | |
| <!-- RIGHT β hero git diagram --> | |
| <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> | |
| <!-- branch lines --> | |
| <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"/> | |
| <!-- main branch commits --> | |
| <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> | |
| <!-- feature branch commits --> | |
| <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> | |
| <!-- fix branch commits --> | |
| <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> | |
| <!-- labels --> | |
| <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> | |
| <!-- HEAD label pill --> | |
| <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> | |
| <!-- AI review indicator --> | |
| <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> | |
| <!-- Ticker --> | |
| <div class="ticker"> | |
| <div class="tk-track" id="tk"></div> | |
| </div> | |
| </div> | |
| <!-- Detail overlay --> | |
| <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> | |
| // ββ Ticker ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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(''); | |
| // ββ Canvas particles βββββββββββββββββββββββββββββββββββββββββ | |
| 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); | |
| // ββ Progress bar βββββββββββββββββββββββββββββββββββββββββββββ | |
| let prog = 0; | |
| const progEl = document.getElementById('prog'); | |
| setInterval(() => { | |
| prog = (prog + .4) % 100; | |
| progEl.setAttribute('width', prog * 1.4); | |
| }, 60); | |
| // ββ Tabs βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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'); | |
| } | |
| // ββ Status helper βββββββββββββββββββββββββββββββββββββββββββββ | |
| function setStatus(msg, cls = '') { | |
| const el = document.getElementById('status'); | |
| el.textContent = msg; | |
| el.className = 'status ' + cls; | |
| } | |
| // ββ Dot color by extension ββββββββββββββββββββββββββββββββββββ | |
| function dotColor(name) { | |
| const ext = name.split('.').pop().toLowerCase(); | |
| if (['ts','tsx','js','jsx'].includes(ext)) return '#58a6ff'; // blue β JS/TS | |
| if (['py','rb','go','rs'].includes(ext)) return '#3fb950'; // green β backend | |
| if (['yml','yaml','json','toml','env'].includes(ext)) return '#f85149'; // red β config | |
| if (['md','txt','rst'].includes(ext)) return '#e3b341'; // yellow β docs | |
| return '#8a9ab0'; // grey β other | |
| } | |
| // ββ Render results ββββββββββββββββββββββββββββββββββββββββββββ | |
| 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,'>'); | |
| } | |
| // ββ Detail overlay ββββββββββββββββββββββββββββββββββββββββββββ | |
| 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'); | |
| } | |
| // ββ Main: call Gradio API βββββββββββββββββββββββββββββββββββββ | |
| 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'); | |
| // Reset panes to loading state | |
| 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 { | |
| // gradio.Server exposes @app.api() functions at /gradio/api/<name> | |
| // We use @gradio/client for proper queuing support | |
| const Client = window._gradioClient; | |
| // Connect to this same origin (gradio.Server is the host) | |
| 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, | |
| }); | |
| // result.data[0] is the returned dict: { files: [...], report: "..." } | |
| 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'; | |
| } | |
| } | |
| // Allow Enter key to submit | |
| document.getElementById('ri').addEventListener('keydown', e => { | |
| if (e.key === 'Enter') go(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |