pkheria commited on
Commit
ee5892f
Β·
1 Parent(s): cc05a9a

custom UI added

Browse files
Files changed (3) hide show
  1. .DS_Store +0 -0
  2. app.py +61 -113
  3. index.html +463 -0
.DS_Store CHANGED
Binary files a/.DS_Store and b/.DS_Store differ
 
app.py CHANGED
@@ -1,23 +1,23 @@
1
  """
2
- CommitLens β€” Gradio UI
3
- =======================
4
- - Asks for a GitHub repo URL (and optional PAT)
5
- - Runs commitlens.run_pipeline() to get per-file prompts
6
- - Feeds each prompt to Mellum 2 for a per-file summary
7
- - Combines all summaries and asks the model for a final .md report
8
- - Displays everything in the browser
9
  """
10
 
11
  from __future__ import annotations
12
 
13
  import logging
14
  import sys
 
15
 
16
- import gradio as gr
17
- import spaces
18
  import torch
 
 
19
  from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
20
 
 
21
  from commitlens import run_pipeline
22
 
23
  logging.basicConfig(
@@ -61,22 +61,18 @@ def _get_llm():
61
  global _model, _tokenizer
62
  if _model is None:
63
  log.info("Starting model load from %s ...", MODEL_REPO_ID)
64
- # 8-bit quantization is required to bypass the 16GB ZeroGPU CPU RAM limit
65
- quantization_config = BitsAndBytesConfig(
66
- load_in_8bit=True,
67
- )
68
 
69
  log.info("Loading tokenizer ...")
70
  _tokenizer = AutoTokenizer.from_pretrained(MODEL_REPO_ID)
71
  log.info("Tokenizer loaded.")
72
 
73
- # flash_attention_2 removed. PyTorch will automatically use native SDPA.
74
- log.info("Loading model with 8-bit quantization and device_map='auto' ...")
75
  _model = AutoModelForCausalLM.from_pretrained(
76
  MODEL_REPO_ID,
77
  quantization_config=quantization_config,
78
  device_map="auto",
79
- torch_dtype=torch.bfloat16, # ZeroGPU RTX 6000 natively supports bfloat16
80
  )
81
  log.info("Model loaded successfully.")
82
  return _model, _tokenizer
@@ -86,140 +82,92 @@ def _extract_filename(prompt: str) -> str:
86
  for line in prompt.splitlines():
87
  if line.startswith("Filename :"):
88
  name = line.split(":", 1)[1].strip()
89
- log.debug("Extracted filename: %s", name)
90
  return name
91
- log.warning("Could not extract filename from prompt")
92
  return "unknown"
93
 
94
 
95
  def _generate_response(system_prompt: str, user_prompt: str, max_tokens: int) -> str:
96
- log.info("Generating response (max_tokens=%d) ...", max_tokens)
97
  model, tokenizer = _get_llm()
98
-
99
  messages = [
100
  {"role": "system", "content": system_prompt},
101
  {"role": "user", "content": user_prompt},
102
  ]
103
-
104
- # Format the prompt using the model's chat template
105
- log.debug("Applying chat template ...")
106
  formatted_prompt = tokenizer.apply_chat_template(
107
  messages, tokenize=False, add_generation_prompt=True
108
  )
109
-
110
- log.debug("Tokenizing input ...")
111
  inputs = tokenizer(formatted_prompt, return_tensors="pt").to("cuda")
112
- log.debug("Input shape: %s", inputs.input_ids.shape)
113
-
114
- log.info("Running model.generate ...")
115
  outputs = model.generate(
116
  **inputs,
117
  max_new_tokens=max_tokens,
118
- use_cache=True,
119
- do_sample=False,
120
- pad_token_id=tokenizer.eos_token_id
 
 
 
121
  )
122
- log.info("Generation complete.")
123
-
124
- # Decode and return just the generated response
125
- response = tokenizer.decode(outputs[0][inputs.input_ids.shape[-1]:], skip_special_tokens=True)
126
- log.debug("Response length: %d characters", len(response))
127
  return response.strip()
128
 
129
 
130
  def _summarize(prompt: str) -> str:
131
- log.info("Summarizing file ...")
132
- result = _generate_response(SUMMARY_SYSTEM_PROMPT, prompt, max_tokens=1024)
133
- log.info("File summarization result:\n%s", result)
134
- return result
135
 
136
 
137
  def _final_md(combined: str) -> str:
138
- log.info("Generating final markdown report ...")
139
- result = _generate_response(FINAL_SYSTEM_PROMPT, combined, max_tokens=2048)
140
- log.info("Final markdown report result:\n%s", result)
141
- return result
142
 
143
 
144
  # ---------------------------------------------------------------------------
145
- # Pipeline
146
  # ---------------------------------------------------------------------------
147
 
 
 
 
 
 
 
 
 
 
 
 
148
  @spaces.GPU(duration=300)
149
- def process_repo(repo_url: str, token: str, progress: gr.Progress = gr.Progress()):
150
- try:
151
- log.info("Pipeline started for repo: %s", repo_url)
152
- progress(0, desc="Running CommitLens pipeline...")
153
- prompts = run_pipeline(repo_url, token.strip() or None)
154
- log.info("CommitLens pipeline returned %d prompts.", len(prompts))
155
-
156
- if not prompts:
157
- log.warning("No source-code files changed in the latest commit.")
158
- raise ValueError("No source-code files changed in the latest commit.")
159
-
160
- per_file_md_parts = []
161
- for i, prompt in enumerate(prompts):
162
- fname = _extract_filename(prompt)
163
- log.info("Processing file %d/%d: %s", i + 1, len(prompts), fname)
164
- progress(
165
- (i + 1) / (len(prompts) + 1),
166
- desc=f"Summarizing [{i+1}/{len(prompts)}] {fname}...",
167
- )
168
- summary = _summarize(prompt)
169
- log.info("Summary for %s:\n%s", fname, summary)
170
- per_file_md_parts.append(f"## `{fname}`\n\n{summary}")
171
- log.info("Finished file %d/%d: %s", i + 1, len(prompts), fname)
172
-
173
- combined = "\n\n---\n\n".join(per_file_md_parts)
174
- log.info("All per-file summaries combined (%d characters).", len(combined))
175
-
176
- progress(0.95, desc="Generating final markdown report...")
177
- final_md = _final_md(combined)
178
-
179
- log.info("Pipeline finished successfully.")
180
- return combined, final_md
181
-
182
- except gr.Error:
183
- raise
184
- except Exception as e:
185
- log.error("Pipeline failed: %s", e, exc_info=True)
186
- raise gr.Error(str(e))
187
 
 
 
 
 
 
 
 
 
 
188
 
189
- # ---------------------------------------------------------------------------
190
- # Gradio app
191
- # ---------------------------------------------------------------------------
192
 
193
- with gr.Blocks(title="CommitLens", theme=gr.themes.Soft()) as demo:
194
- gr.Markdown("# CommitLens β€” AI-Powered Commit Analysis")
195
 
196
- with gr.Row():
197
- repo_url = gr.Textbox(
198
- label="GitHub Repository URL",
199
- placeholder="https://github.com/owner/repo",
200
- scale=2,
201
- )
202
- token = gr.Textbox(
203
- label="GitHub Token (for private repos)",
204
- type="password",
205
- placeholder="ghp_... or leave empty for public repos",
206
- scale=1,
207
- )
208
 
209
- run_btn = gr.Button("Run Analysis", variant="primary", size="lg")
 
 
210
 
211
- with gr.Tabs():
212
- with gr.Tab("Per-File Summaries"):
213
- per_file_out = gr.Markdown()
214
- with gr.Tab("Final Report (.md)"):
215
- final_out = gr.Markdown()
216
 
217
- run_btn.click(
218
- fn=process_repo,
219
- inputs=[repo_url, token],
220
- outputs=[per_file_out, final_out],
221
- )
222
 
223
  if __name__ == "__main__":
224
- log.info("Starting Gradio app ...")
225
- demo.launch()
 
1
  """
2
+ CommitLens β€” gradio.Server mode
3
+ ================================
4
+ - Serves custom index.html at GET /
5
+ - Exposes process_repo via @app.api() for the JS frontend to call
6
+ - Uses Mellum 2 for per-file summaries + final markdown report
 
 
7
  """
8
 
9
  from __future__ import annotations
10
 
11
  import logging
12
  import sys
13
+ from pathlib import Path
14
 
 
 
15
  import torch
16
+ from fastapi.responses import HTMLResponse
17
+ from gradio import Server
18
  from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
19
 
20
+ import spaces
21
  from commitlens import run_pipeline
22
 
23
  logging.basicConfig(
 
61
  global _model, _tokenizer
62
  if _model is None:
63
  log.info("Starting model load from %s ...", MODEL_REPO_ID)
64
+ quantization_config = BitsAndBytesConfig(load_in_8bit=True)
 
 
 
65
 
66
  log.info("Loading tokenizer ...")
67
  _tokenizer = AutoTokenizer.from_pretrained(MODEL_REPO_ID)
68
  log.info("Tokenizer loaded.")
69
 
70
+ log.info("Loading model with 8-bit quantization ...")
 
71
  _model = AutoModelForCausalLM.from_pretrained(
72
  MODEL_REPO_ID,
73
  quantization_config=quantization_config,
74
  device_map="auto",
75
+ torch_dtype=torch.bfloat16,
76
  )
77
  log.info("Model loaded successfully.")
78
  return _model, _tokenizer
 
82
  for line in prompt.splitlines():
83
  if line.startswith("Filename :"):
84
  name = line.split(":", 1)[1].strip()
 
85
  return name
 
86
  return "unknown"
87
 
88
 
89
  def _generate_response(system_prompt: str, user_prompt: str, max_tokens: int) -> str:
 
90
  model, tokenizer = _get_llm()
 
91
  messages = [
92
  {"role": "system", "content": system_prompt},
93
  {"role": "user", "content": user_prompt},
94
  ]
 
 
 
95
  formatted_prompt = tokenizer.apply_chat_template(
96
  messages, tokenize=False, add_generation_prompt=True
97
  )
 
 
98
  inputs = tokenizer(formatted_prompt, return_tensors="pt").to("cuda")
 
 
 
99
  outputs = model.generate(
100
  **inputs,
101
  max_new_tokens=max_tokens,
102
+ use_cache=True,
103
+ do_sample=False,
104
+ pad_token_id=tokenizer.eos_token_id,
105
+ )
106
+ response = tokenizer.decode(
107
+ outputs[0][inputs.input_ids.shape[-1]:], skip_special_tokens=True
108
  )
 
 
 
 
 
109
  return response.strip()
110
 
111
 
112
  def _summarize(prompt: str) -> str:
113
+ return _generate_response(SUMMARY_SYSTEM_PROMPT, prompt, max_tokens=1024)
 
 
 
114
 
115
 
116
  def _final_md(combined: str) -> str:
117
+ return _generate_response(FINAL_SYSTEM_PROMPT, combined, max_tokens=2048)
 
 
 
118
 
119
 
120
  # ---------------------------------------------------------------------------
121
+ # gradio.Server app
122
  # ---------------------------------------------------------------------------
123
 
124
+ app = Server()
125
+
126
+
127
+ @app.get("/", response_class=HTMLResponse)
128
+ async def homepage():
129
+ """Serve the custom cinematic frontend."""
130
+ html_path = Path(__file__).parent / "index.html"
131
+ return HTMLResponse(content=html_path.read_text(encoding="utf-8"))
132
+
133
+
134
+ @app.api(name="process_repo")
135
  @spaces.GPU(duration=300)
136
+ def process_repo(repo_url: str, token: str) -> dict:
137
+ """
138
+ Run the CommitLens pipeline and return structured results.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
+ Returns:
141
+ {
142
+ "files": [{"name": str, "summary": str}, ...],
143
+ "report": str # final markdown
144
+ }
145
+ """
146
+ log.info("Pipeline started for repo: %s", repo_url)
147
+ prompts = run_pipeline(repo_url, token.strip() or None)
148
+ log.info("CommitLens pipeline returned %d prompts.", len(prompts))
149
 
150
+ if not prompts:
151
+ raise ValueError("No source-code files changed in the latest commit.")
 
152
 
153
+ file_results = []
154
+ per_file_md_parts = []
155
 
156
+ for i, prompt in enumerate(prompts):
157
+ fname = _extract_filename(prompt)
158
+ log.info("Processing file %d/%d: %s", i + 1, len(prompts), fname)
159
+ summary = _summarize(prompt)
160
+ file_results.append({"name": fname, "summary": summary})
161
+ per_file_md_parts.append(f"## `{fname}`\n\n{summary}")
162
+ log.info("Finished file %d/%d: %s", i + 1, len(prompts), fname)
 
 
 
 
 
163
 
164
+ combined = "\n\n---\n\n".join(per_file_md_parts)
165
+ final_report = _final_md(combined)
166
+ log.info("Pipeline finished successfully.")
167
 
168
+ return {"files": file_results, "report": final_report}
 
 
 
 
169
 
 
 
 
 
 
170
 
171
  if __name__ == "__main__":
172
+ log.info("Starting CommitLens with gradio.Server ...")
173
+ app.launch()
index.html ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>CommitLens β€” AI Code Review</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com"/>
8
+ <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"/>
9
+ <!-- Gradio JS client for calling @app.api() endpoints -->
10
+ <script type="module">
11
+ import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
12
+ window._gradioClient = Client;
13
+ </script>
14
+ <style>
15
+ *{margin:0;padding:0;box-sizing:border-box}
16
+ html,body{background:#06060e;width:100%;min-height:100vh;font-family:'Space Mono',monospace;color:#e6edf3}
17
+
18
+ /* ── Layout ── */
19
+ #root{width:100%;min-height:100vh;display:flex;flex-direction:column;position:relative;overflow:hidden}
20
+ canvas{position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:0}
21
+
22
+ .ui{flex:1;display:grid;grid-template-columns:1fr 1fr;z-index:2;position:relative;padding-bottom:44px}
23
+ @media(max-width:780px){.ui{grid-template-columns:1fr}}
24
+
25
+ /* ── Left panel ── */
26
+ .left{padding:52px 44px;display:flex;flex-direction:column;justify-content:center;gap:28px}
27
+
28
+ .pill{display:inline-flex;align-items:center;gap:8px;border:0.5px solid #1a3a1a;border-radius:100px;padding:6px 16px;width:fit-content}
29
+ .pill-dot{width:5px;height:5px;border-radius:50%;background:#3fb950;animation:blink 2s infinite}
30
+ .pill-text{font-size:9px;letter-spacing:.2em;color:#3fb950}
31
+ @keyframes blink{0%,100%{opacity:1}50%{opacity:.15}}
32
+
33
+ .headline{font-family:'Playfair Display',serif;line-height:1}
34
+ .hl1{display:block;font-style:italic;font-weight:400;font-size:46px;color:#555;letter-spacing:-.01em}
35
+ .hl2{display:block;font-style:normal;font-weight:900;font-size:58px;color:#fff;letter-spacing:-.02em;margin-top:2px}
36
+
37
+ .desc{font-size:11px;color:#444;line-height:2;max-width:290px;letter-spacing:.02em}
38
+
39
+ /* ── Form ── */
40
+ .form-wrap{display:flex;flex-direction:column;gap:10px}
41
+ .field-lbl{font-size:9px;letter-spacing:.2em;color:#666;text-transform:uppercase;margin-bottom:4px}
42
+ .irow{display:flex;border:0.5px solid #1a1a2a;border-radius:9px;overflow:hidden;background:#0a0a14}
43
+ .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}
44
+ .irow input::placeholder{color:#2a2a3a}
45
+ .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}
46
+ .run-btn:hover:not(:disabled){background:#2ea043}
47
+ .run-btn:disabled{opacity:.45;cursor:not-allowed}
48
+ .tok-row{display:flex;align-items:center;gap:10px}
49
+ .tok-lbl{font-size:9px;color:#444;letter-spacing:.15em;flex-shrink:0}
50
+ .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}
51
+ .tok-row input::placeholder{color:#252535}
52
+
53
+ /* ── Status bar ── */
54
+ .status{font-size:9px;letter-spacing:.15em;color:#1e3e2e;height:18px;transition:color .3s}
55
+ .status.active{color:#3fb950}
56
+ .status.error{color:#f85149}
57
+
58
+ /* ── Output area ── */
59
+ .out-area{display:flex;flex-direction:column;gap:0;min-height:160px}
60
+ .tabs{display:flex;gap:0;border-bottom:0.5px solid #0e0e18;margin-bottom:14px}
61
+ .tb{font-size:9px;letter-spacing:.16em;color:#333;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}
62
+ .tb.on{color:#58a6ff;border-bottom-color:#58a6ff}
63
+
64
+ /* Per-file pane */
65
+ #pane-f{display:none}
66
+ #pane-f.on{display:block;max-height:320px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#1a1a2a transparent}
67
+ .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}
68
+ .frow:last-child{border:none}
69
+ .frow:hover{background:#0d0d18}
70
+ .dot{width:5px;height:5px;border-radius:50%;flex-shrink:0;margin-top:3px}
71
+ .file-body{display:flex;flex-direction:column;gap:3px;flex:1;min-width:0}
72
+ .fn{font-size:11px;color:#666;letter-spacing:.02em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
73
+ .fd{font-size:10px;color:#2a2a4a;font-style:italic;line-height:1.6;white-space:pre-wrap;word-break:break-word}
74
+ .fd.expanded{color:#88aacc}
75
+
76
+ /* Report pane */
77
+ #pane-r{display:none}
78
+ #pane-r.on{display:block;max-height:320px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#1a1a2a transparent}
79
+ .rp{font-size:11px;color:#444;line-height:1.9;white-space:pre-wrap;word-break:break-word}
80
+ .rp em{color:#58a6ff;font-style:normal}
81
+
82
+ /* Empty state */
83
+ .empty-state{display:flex;flex-direction:column;gap:6px;padding:20px 0}
84
+ .es-line{height:10px;border-radius:3px;background:#0d0d18;animation:shimmer 2s infinite}
85
+ .es-line:nth-child(1){width:80%}
86
+ .es-line:nth-child(2){width:60%}
87
+ .es-line:nth-child(3){width:70%}
88
+ @keyframes shimmer{0%,100%{opacity:.3}50%{opacity:.6}}
89
+
90
+ /* ── Right panel β€” hero git diagram ── */
91
+ .right{position:relative;display:flex;align-items:center;justify-content:center;overflow:hidden}
92
+ #hero-svg{position:relative;z-index:2}
93
+
94
+ /* ── Ticker ── */
95
+ .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}
96
+ .tk-track{display:flex;white-space:nowrap;animation:slide 36s linear infinite}
97
+ .ti{font-size:9px;letter-spacing:.18em;color:#222;padding:0 30px;text-transform:uppercase;flex-shrink:0}
98
+ .ti b{color:#1e2e3e}
99
+ @keyframes slide{from{transform:translateX(0)}to{transform:translateX(-50%)}}
100
+
101
+ /* ── Detail overlay ── */
102
+ #overlay{display:none;position:fixed;inset:0;background:rgba(6,6,14,.92);z-index:100;padding:40px;overflow-y:auto}
103
+ #overlay.on{display:flex;flex-direction:column;gap:20px}
104
+ .ov-head{display:flex;align-items:center;justify-content:space-between}
105
+ .ov-title{font-size:14px;color:#58a6ff;letter-spacing:.05em}
106
+ .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}
107
+ .ov-close:hover{color:#e6edf3;border-color:#3a3a5a}
108
+ .ov-body{font-size:12px;color:#8a9ab0;line-height:1.9;white-space:pre-wrap;word-break:break-word;max-width:760px}
109
+ </style>
110
+ </head>
111
+ <body>
112
+
113
+ <div id="root">
114
+ <canvas id="cv"></canvas>
115
+
116
+ <div class="ui">
117
+ <!-- LEFT -->
118
+ <div class="left">
119
+ <div class="pill"><span class="pill-dot"></span><span class="pill-text">AI Β· CODE REVIEW Β· LIVE</span></div>
120
+
121
+ <div class="headline">
122
+ <span class="hl1">the commits are</span>
123
+ <span class="hl2">already<br>judged.</span>
124
+ </div>
125
+
126
+ <p class="desc">Drop any GitHub repo. AI fetches the latest commit, reviews every changed file, returns structured markdown β€” instantly.</p>
127
+
128
+ <div class="form-wrap">
129
+ <div>
130
+ <div class="field-lbl">Repository</div>
131
+ <div class="irow">
132
+ <input type="text" placeholder="https://github.com/owner/repo" id="ri"/>
133
+ <button class="run-btn" id="runBtn" onclick="go()">β†’ RUN</button>
134
+ </div>
135
+ </div>
136
+ <div class="tok-row">
137
+ <span class="tok-lbl">TOKEN</span>
138
+ <input type="password" id="tok" placeholder="ghp_β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’ (optional)"/>
139
+ </div>
140
+ </div>
141
+
142
+ <div class="status" id="status">READY</div>
143
+
144
+ <div class="out-area">
145
+ <div class="tabs">
146
+ <button class="tb on" onclick="sw(this,'f')">Per-file</button>
147
+ <button class="tb" onclick="sw(this,'r')">Report</button>
148
+ </div>
149
+
150
+ <div id="pane-f" class="on">
151
+ <div class="empty-state" id="emptyF">
152
+ <div class="es-line"></div>
153
+ <div class="es-line"></div>
154
+ <div class="es-line"></div>
155
+ </div>
156
+ </div>
157
+
158
+ <div id="pane-r">
159
+ <div class="empty-state" id="emptyR">
160
+ <div class="es-line"></div>
161
+ <div class="es-line"></div>
162
+ <div class="es-line"></div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+
168
+ <!-- RIGHT β€” hero git diagram -->
169
+ <div class="right">
170
+ <svg id="hero-svg" viewBox="0 0 340 500" width="340" height="500" xmlns="http://www.w3.org/2000/svg">
171
+ <defs>
172
+ <radialGradient id="ng" cx="50%" cy="50%" r="50%">
173
+ <stop offset="0%" stop-color="#1a4a2e" stop-opacity="1"/>
174
+ <stop offset="100%" stop-color="#0a1a12" stop-opacity="1"/>
175
+ </radialGradient>
176
+ <radialGradient id="bg2" cx="50%" cy="50%" r="50%">
177
+ <stop offset="0%" stop-color="#1a2a4a" stop-opacity="1"/>
178
+ <stop offset="100%" stop-color="#0a1020" stop-opacity="1"/>
179
+ </radialGradient>
180
+ <radialGradient id="rg" cx="50%" cy="50%" r="50%">
181
+ <stop offset="0%" stop-color="#4a1a1a" stop-opacity="1"/>
182
+ <stop offset="100%" stop-color="#200a0a" stop-opacity="1"/>
183
+ </radialGradient>
184
+ </defs>
185
+ <!-- branch lines -->
186
+ <line x1="170" y1="40" x2="170" y2="460" stroke="#0f1f0f" stroke-width="1.5"/>
187
+ <line x1="170" y1="140" x2="260" y2="220" stroke="#0f1a0f" stroke-width="1"/>
188
+ <line x1="260" y1="220" x2="260" y2="340" stroke="#0f1a0f" stroke-width="1"/>
189
+ <line x1="260" y1="340" x2="170" y2="380" stroke="#0f1a0f" stroke-width="1"/>
190
+ <line x1="170" y1="200" x2="90" y2="260" stroke="#0f0f1a" stroke-width="1"/>
191
+ <line x1="90" y1="260" x2="90" y2="320" stroke="#0f0f1a" stroke-width="1"/>
192
+ <line x1="90" y1="320" x2="170" y2="340" stroke="#0f0f1a" stroke-width="1"/>
193
+ <!-- main branch commits -->
194
+ <circle cx="170" cy="60" r="14" fill="url(#ng)" stroke="#1a3a1a" stroke-width="0.5"/>
195
+ <text x="170" y="64" text-anchor="middle" font-size="8" fill="#3fb950" font-family="monospace">a1b2c</text>
196
+ <circle cx="170" cy="140" r="14" fill="url(#ng)" stroke="#1a3a1a" stroke-width="0.5"/>
197
+ <text x="170" y="144" text-anchor="middle" font-size="8" fill="#3fb950" font-family="monospace">d3e4f</text>
198
+ <circle cx="170" cy="200" r="14" fill="url(#ng)" stroke="#1a3a1a" stroke-width="0.5"/>
199
+ <text x="170" y="204" text-anchor="middle" font-size="8" fill="#3fb950" font-family="monospace">g5h6i</text>
200
+ <circle cx="170" cy="340" r="14" fill="url(#ng)" stroke="#1a3a1a" stroke-width="0.5"/>
201
+ <text x="170" y="344" text-anchor="middle" font-size="8" fill="#3fb950" font-family="monospace">merge</text>
202
+ <circle cx="170" cy="420" r="14" fill="url(#ng)" stroke="#1a3a1a" stroke-width="0.5"/>
203
+ <text x="170" y="424" text-anchor="middle" font-size="8" fill="#3fb950" font-family="monospace">HEAD</text>
204
+ <!-- feature branch commits -->
205
+ <circle cx="260" cy="240" r="12" fill="url(#bg2)" stroke="#1a2a4a" stroke-width="0.5"/>
206
+ <text x="260" y="244" text-anchor="middle" font-size="7" fill="#58a6ff" font-family="monospace">feat</text>
207
+ <circle cx="260" cy="300" r="12" fill="url(#bg2)" stroke="#1a2a4a" stroke-width="0.5"/>
208
+ <text x="260" y="304" text-anchor="middle" font-size="7" fill="#58a6ff" font-family="monospace">feat</text>
209
+ <!-- fix branch commits -->
210
+ <circle cx="90" cy="270" r="12" fill="url(#rg)" stroke="#4a1a1a" stroke-width="0.5"/>
211
+ <text x="90" y="274" text-anchor="middle" font-size="7" fill="#f85149" font-family="monospace">fix</text>
212
+ <circle cx="90" cy="310" r="12" fill="url(#rg)" stroke="#4a1a1a" stroke-width="0.5"/>
213
+ <text x="90" y="314" text-anchor="middle" font-size="7" fill="#f85149" font-family="monospace">fix</text>
214
+ <!-- labels -->
215
+ <text x="186" y="56" font-size="9" fill="#1a3a1a" font-family="monospace">main</text>
216
+ <text x="276" y="236" font-size="9" fill="#1a2a4a" font-family="monospace">feature/auth</text>
217
+ <text x="36" y="266" font-size="9" fill="#3a1a1a" font-family="monospace">fix/jwt</text>
218
+ <!-- HEAD label pill -->
219
+ <rect x="136" y="438" width="68" height="18" rx="9" fill="#0f2f1f" stroke="#1a4a2a" stroke-width="0.5"/>
220
+ <text x="170" y="451" text-anchor="middle" font-size="8" fill="#3fb950" font-family="monospace" letter-spacing=".08em">● HEAD</text>
221
+ <!-- AI review indicator -->
222
+ <rect x="100" y="10" width="140" height="22" rx="5" fill="#0a0a14" stroke="#1a1a2a" stroke-width="0.5"/>
223
+ <text x="170" y="25" text-anchor="middle" font-size="8" fill="#333" font-family="monospace" letter-spacing=".12em">AI REVIEWING...</text>
224
+ <rect id="prog" x="100" y="10" width="0" height="22" rx="5" fill="#238636" opacity=".15"/>
225
+ </svg>
226
+ </div>
227
+ </div>
228
+
229
+ <!-- Ticker -->
230
+ <div class="ticker">
231
+ <div class="tk-track" id="tk"></div>
232
+ </div>
233
+ </div>
234
+
235
+ <!-- Detail overlay -->
236
+ <div id="overlay">
237
+ <div class="ov-head">
238
+ <span class="ov-title" id="ovTitle"></span>
239
+ <button class="ov-close" onclick="closeOverlay()">βœ• CLOSE</button>
240
+ </div>
241
+ <pre class="ov-body" id="ovBody"></pre>
242
+ </div>
243
+
244
+ <script>
245
+ // ── Ticker ──────────────────────────────────────────────────
246
+ const tks = [
247
+ 'git commit -m "reviewed by AI"',
248
+ '<b>●</b> live code review',
249
+ 'feat: bulk import Β· fix: JWT expiry Β· chore: remove yaml',
250
+ 'diff --git a/src b/src',
251
+ 'HEAD~1 Β· origin/main Β· refs/heads/feat',
252
+ 'mellum 2 Β· per-file summaries Β· final report',
253
+ 'git push Β· <b>shipped</b>',
254
+ 'five files Β· zero context switching'
255
+ ];
256
+ const tkEl = document.getElementById('tk');
257
+ tkEl.innerHTML = [...tks, ...tks, ...tks, ...tks, ...tks, ...tks]
258
+ .map(s => `<span class="ti">${s}&nbsp;&nbsp;/</span>`).join('');
259
+
260
+ // ── Canvas particles ─────────────────────────────────────────
261
+ const cv = document.getElementById('cv'), cc = cv.getContext('2d');
262
+ let W, H;
263
+ function rsz() { W = cv.width = window.innerWidth; H = cv.height = window.innerHeight; }
264
+
265
+ 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'];
266
+ const EXTS = ['.ts','.py','.yml','.go','.rs','.tsx','.sql','.json','.sh','.md'];
267
+
268
+ class Particle {
269
+ constructor(atBottom) { this.reset(atBottom); }
270
+ reset(atBottom) {
271
+ this.x = Math.random() * W;
272
+ this.y = atBottom ? H + 30 : Math.random() * H;
273
+ this.vx = (Math.random() - .5) * .25;
274
+ this.vy = -(Math.random() * .35 + .06);
275
+ this.a = Math.random() * .4 + .05;
276
+ this.type = Math.floor(Math.random() * 4);
277
+ this.ph = Math.random() * Math.PI * 2;
278
+ this.ps = Math.random() * .015 + .004;
279
+ this.sz = Math.random() * 2 + 1;
280
+ this.hue = [210, 142, 38, 275][Math.floor(Math.random() * 4)];
281
+ this.txt = this.type === 1 ? CMDS[Math.floor(Math.random() * CMDS.length)] :
282
+ this.type === 2 ? `src/module${EXTS[Math.floor(Math.random() * EXTS.length)]}` :
283
+ this.type === 3 ? `+${Math.floor(Math.random() * 40) + 1} -${Math.floor(Math.random() * 20) + 1}` : null;
284
+ this.w = this.txt ? (this.txt.length * 6.5 + 20) : 0;
285
+ }
286
+ draw(ctx) {
287
+ this.ph += this.ps;
288
+ const a = this.a * (.5 + .5 * Math.sin(this.ph));
289
+ ctx.save(); ctx.globalAlpha = a;
290
+ if (this.type === 0) {
291
+ ctx.beginPath(); ctx.arc(this.x, this.y, this.sz, 0, Math.PI * 2);
292
+ ctx.fillStyle = `hsl(${this.hue},40%,35%)`; ctx.fill();
293
+ } else {
294
+ const col = this.type === 1 ? 'hsl(142,30%,22%)' : this.type === 2 ? 'hsl(210,30%,16%)' : 'hsl(38,30%,18%)';
295
+ const tcol = this.type === 1 ? '#1a3a1a' : this.type === 2 ? '#1a2a3a' : '#3a2a0a';
296
+ ctx.fillStyle = col;
297
+ const h = 16, r = 4;
298
+ ctx.beginPath();
299
+ ctx.moveTo(this.x + r, this.y - h / 2);
300
+ ctx.lineTo(this.x + this.w - r, this.y - h / 2);
301
+ ctx.arcTo(this.x + this.w, this.y - h / 2, this.x + this.w, this.y, r);
302
+ ctx.arcTo(this.x + this.w, this.y + h / 2, this.x + this.w - r, this.y + h / 2, r);
303
+ ctx.lineTo(this.x + r, this.y + h / 2);
304
+ ctx.arcTo(this.x, this.y + h / 2, this.x, this.y, r);
305
+ ctx.arcTo(this.x, this.y - h / 2, this.x + r, this.y - h / 2, r);
306
+ ctx.closePath(); ctx.fill();
307
+ ctx.strokeStyle = tcol; ctx.lineWidth = .5; ctx.stroke();
308
+ ctx.fillStyle = tcol; ctx.font = '9px monospace';
309
+ ctx.fillText(this.txt, this.x + 10, this.y + 3);
310
+ }
311
+ ctx.restore();
312
+ this.x += this.vx; this.y += this.vy;
313
+ }
314
+ dead() { return this.y < -30 || this.x < -220 || this.x > W + 220; }
315
+ }
316
+
317
+ let pts = [];
318
+ rsz();
319
+ for (let i = 0; i < 80; i++) pts.push(new Particle(false));
320
+ function frame() {
321
+ cc.clearRect(0, 0, W, H);
322
+ pts.forEach(p => { p.draw(cc); if (p.dead()) p.reset(true); });
323
+ requestAnimationFrame(frame);
324
+ }
325
+ frame();
326
+ window.addEventListener('resize', rsz);
327
+
328
+ // ── Progress bar ─────────────────────────────────────────────
329
+ let prog = 0;
330
+ const progEl = document.getElementById('prog');
331
+ setInterval(() => {
332
+ prog = (prog + .4) % 100;
333
+ progEl.setAttribute('width', prog * 1.4);
334
+ }, 60);
335
+
336
+ // ── Tabs ─────────────────────────────────────────────────────
337
+ function sw(el, t) {
338
+ document.querySelectorAll('.tb').forEach(x => x.classList.remove('on'));
339
+ document.querySelectorAll('#pane-f,#pane-r').forEach(x => x.classList.remove('on'));
340
+ el.classList.add('on');
341
+ document.getElementById('pane-' + t).classList.add('on');
342
+ }
343
+
344
+ // ── Status helper ─────────────────────────────────────────────
345
+ function setStatus(msg, cls = '') {
346
+ const el = document.getElementById('status');
347
+ el.textContent = msg;
348
+ el.className = 'status ' + cls;
349
+ }
350
+
351
+ // ── Dot color by extension ────────────────────────────────────
352
+ function dotColor(name) {
353
+ const ext = name.split('.').pop().toLowerCase();
354
+ if (['ts','tsx','js','jsx'].includes(ext)) return '#58a6ff'; // blue β€” JS/TS
355
+ if (['py','rb','go','rs'].includes(ext)) return '#3fb950'; // green β€” backend
356
+ if (['yml','yaml','json','toml','env'].includes(ext)) return '#f85149'; // red β€” config
357
+ if (['md','txt','rst'].includes(ext)) return '#e3b341'; // yellow β€” docs
358
+ return '#8a9ab0'; // grey β€” other
359
+ }
360
+
361
+ // ── Render results ────────────────────────────────────────────
362
+ function renderFiles(files) {
363
+ const pane = document.getElementById('pane-f');
364
+ if (!files || files.length === 0) {
365
+ pane.innerHTML = '<div class="rp" style="color:#2a3a2a">No files changed.</div>';
366
+ return;
367
+ }
368
+ pane.innerHTML = files.map((f, i) => `
369
+ <div class="frow" onclick="showDetail(${i})" data-idx="${i}">
370
+ <span class="dot" style="background:${dotColor(f.name)}"></span>
371
+ <div class="file-body">
372
+ <span class="fn">${f.name}</span>
373
+ <span class="fd">${(f.summary || '').slice(0, 120)}${(f.summary || '').length > 120 ? '…' : ''}</span>
374
+ </div>
375
+ </div>
376
+ `).join('');
377
+ }
378
+
379
+ function renderReport(md) {
380
+ const pane = document.getElementById('pane-r');
381
+ pane.innerHTML = `<pre class="rp">${escHtml(md)}</pre>`;
382
+ }
383
+
384
+ function escHtml(s) {
385
+ return (s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
386
+ }
387
+
388
+ // ── Detail overlay ────────────────────────────────────────────
389
+ let _currentFiles = [];
390
+ function showDetail(idx) {
391
+ const f = _currentFiles[idx];
392
+ if (!f) return;
393
+ document.getElementById('ovTitle').textContent = f.name;
394
+ document.getElementById('ovBody').textContent = f.summary || '';
395
+ document.getElementById('overlay').classList.add('on');
396
+ }
397
+ function closeOverlay() {
398
+ document.getElementById('overlay').classList.remove('on');
399
+ }
400
+
401
+ // ── Main: call Gradio API ─────────────────────────────────────
402
+ async function go() {
403
+ const url = document.getElementById('ri').value.trim();
404
+ if (!url) return;
405
+ const token = document.getElementById('tok').value.trim();
406
+
407
+ const btn = document.getElementById('runBtn');
408
+ btn.disabled = true;
409
+ btn.textContent = 'βŒ› RUNNING';
410
+ setStatus('CONNECTING TO API...', 'active');
411
+
412
+ // Reset panes to loading state
413
+ document.getElementById('pane-f').innerHTML = `
414
+ <div class="empty-state">
415
+ <div class="es-line"></div><div class="es-line"></div><div class="es-line"></div>
416
+ </div>`;
417
+ document.getElementById('pane-r').innerHTML = `
418
+ <div class="empty-state">
419
+ <div class="es-line"></div><div class="es-line"></div><div class="es-line"></div>
420
+ </div>`;
421
+
422
+ try {
423
+ // gradio.Server exposes @app.api() functions at /gradio/api/<name>
424
+ // We use @gradio/client for proper queuing support
425
+ const Client = window._gradioClient;
426
+
427
+ // Connect to this same origin (gradio.Server is the host)
428
+ const client = await Client.connect(window.location.origin);
429
+
430
+ setStatus('PIPELINE RUNNING β€” THIS MAY TAKE A FEW MINUTES...', 'active');
431
+
432
+ const result = await client.predict("/process_repo", {
433
+ repo_url: url,
434
+ token: token,
435
+ });
436
+
437
+ // result.data[0] is the returned dict: { files: [...], report: "..." }
438
+ const data = result.data[0];
439
+ _currentFiles = data.files || [];
440
+
441
+ renderFiles(_currentFiles);
442
+ renderReport(data.report || '');
443
+
444
+ setStatus(`βœ“ DONE β€” ${_currentFiles.length} FILE(S) REVIEWED`, 'active');
445
+ } catch (err) {
446
+ console.error(err);
447
+ const msg = err?.message || String(err);
448
+ document.getElementById('pane-f').innerHTML = `<div class="rp" style="color:#f85149">${escHtml(msg)}</div>`;
449
+ document.getElementById('pane-r').innerHTML = `<div class="rp" style="color:#f85149">${escHtml(msg)}</div>`;
450
+ setStatus('ERROR: ' + msg.slice(0, 60), 'error');
451
+ } finally {
452
+ btn.disabled = false;
453
+ btn.textContent = 'β†’ RUN';
454
+ }
455
+ }
456
+
457
+ // Allow Enter key to submit
458
+ document.getElementById('ri').addEventListener('keydown', e => {
459
+ if (e.key === 'Enter') go();
460
+ });
461
+ </script>
462
+ </body>
463
+ </html>