git-hologram / app.py
CAPT Agent
design: complete visual overhaul โ€” dark holographic theme
6d5c863
#!/usr/bin/env python3
"""
Git Hologram + CAPT Analysis โ€” Visual Overhaul
Dark holographic theme with neural-git graph visualization.
"""
import subprocess, tempfile, shutil, os, hashlib, json, re
from datetime import datetime
from typing import List, Dict
import gradio as gr
from capt_backend import CAPTBackend, health_banner_html, BRAINS
backend = CAPTBackend()
CACHE_DIR = "/tmp/git-hologram-cache"
os.makedirs(CACHE_DIR, exist_ok=True)
# โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def _repo_key(repo_url: str) -> str:
return hashlib.sha256(repo_url.encode()).hexdigest()[:16]
def _get_cached(repo_url: str):
key = _repo_key(repo_url)
meta_path = os.path.join(CACHE_DIR, f"{key}.json")
if os.path.exists(meta_path):
try:
with open(meta_path) as f:
meta = json.load(f)
repo_path = os.path.join(CACHE_DIR, key)
if os.path.isdir(repo_path):
return repo_path, meta
except Exception:
pass
return None, None
def _set_cached(repo_url: str, repo_path: str, meta: dict):
key = _repo_key(repo_url)
dest = os.path.join(CACHE_DIR, key)
if dest != repo_path and os.path.exists(repo_path):
if os.path.exists(dest):
shutil.rmtree(dest, ignore_errors=True)
shutil.move(repo_path, dest)
with open(os.path.join(CACHE_DIR, f"{key}.json"), "w") as f:
json.dump(meta, f)
return dest
def normalize_url(repo_url: str) -> str:
repo_url = repo_url.strip()
if not repo_url:
return ""
if repo_url.startswith("http"):
return repo_url.replace(".git", "") + ".git"
if repo_url.startswith("git@"):
return repo_url
if "/" not in repo_url:
repo_url = f"knowurknot/{repo_url}"
return f"https://github.com/{repo_url}.git"
def get_git_commits(repo_path: str, max_commits: int = 50) -> List[Dict]:
cmd = ["git", "-C", repo_path, "log", f"--max-count={max_commits}",
"--pretty=format:%H|%an|%ae|%ad|%s", "--date=iso"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode != 0:
return []
commits = []
for line in result.stdout.strip().split("\n"):
if "|" in line:
parts = line.split("|", 4)
if len(parts) == 5:
commits.append({"hash": parts[0][:8], "author": parts[1], "email": parts[2],
"date": parts[3], "message": parts[4]})
return commits
def analyze_repo(repo_path: str) -> Dict:
stats = {"files": 0, "languages": {}, "size_kb": 0}
try:
result = subprocess.run(["git", "-C", repo_path, "ls-files"], capture_output=True, text=True, timeout=10)
files = [f for f in result.stdout.strip().split("\n") if f]
stats["files"] = len(files)
for f in files[:500]:
ext = f.split(".")[-1].lower() if "." in f else "none"
if ext in ["py", "js", "ts", "tsx", "jsx", "rs", "go", "java", "cpp", "c", "h", "rb", "php", "swift", "kt"]:
stats["languages"][ext] = stats["languages"].get(ext, 0) + 1
result = subprocess.run(["du", "-sk", repo_path], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
stats["size_kb"] = int(result.stdout.split()[0])
except Exception:
pass
return stats
# โ”€โ”€ Visual Renderers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
LANGUAGE_COLORS = {
"py": "#3b82f6", "js": "#fbbf24", "ts": "#60a5fa", "tsx": "#38bdf8",
"jsx": "#fbbf24", "rs": "#f97316", "go": "#06b6d4", "java": "#ef4444",
"cpp": "#a855f7", "c": "#6366f1", "h": "#8b5cf6", "rb": "#ef4444",
"php": "#8b5cf6", "swift": "#f97316", "kt": "#a855f7",
}
def get_commit_color(msg: str) -> str:
m = msg.lower()
if any(w in m for w in ["feat", "feature", "add", "introduce", "implement"]):
return "#22d3ee" # cyan
if any(w in m for w in ["fix", "bugfix", "patch", "hotfix", "resolve"]):
return "#34d399" # emerald
if any(w in m for w in ["refactor", "clean", "restructure", "simplify"]):
return "#fbbf24" # amber
if any(w in m for w in ["test", "spec", "coverage"]):
return "#a78bfa" # violet
if any(w in m for w in ["doc", "readme", "comment", "guide"]):
return "#60a5fa" # blue
if any(w in m for w in ["chore", "ci", "build", "deps", "bump"]):
return "#94a3b8" # slate
if any(w in m for w in ["merge", "pull request", "pr "]):
return "#f472b6" # pink
return "#64748b" # gray
def render_stats(stats: Dict) -> str:
if not stats or not stats.get("languages"):
return "<div style='color:#64748b;text-align:center;padding:20px;'>No language data</div>"
langs = stats["languages"]
total = sum(langs.values())
bars = []
for lang, count in sorted(langs.items(), key=lambda x: -x[1]):
pct = count / total * 100
color = LANGUAGE_COLORS.get(lang, "#64748b")
bars.append(f'''
<div style="margin-bottom:10px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
<span style="color:#e2e8f0;font-size:12px;font-weight:500;text-transform:uppercase;letter-spacing:0.5px;">{lang}</span>
<span style="color:#94a3b8;font-size:11px;font-family:monospace;">{count} ยท {pct:.1f}%</span>
</div>
<div style="background:rgba(30,41,59,0.6);border-radius:4px;height:6px;overflow:hidden;">
<div style="background:{color};height:100%;border-radius:4px;width:{pct}%;box-shadow:0 0 8px {color}40;"></div>
</div>
</div>''')
size_mb = stats.get("size_kb", 0) / 1024
size_str = f"{size_mb:.1f} MB" if size_mb > 1 else f"{stats.get('size_kb', 0)} KB"
return f'''<div style="padding:4px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:20px;">
<div style="background:rgba(15,23,42,0.6);border:1px solid rgba(51,65,85,0.5);border-radius:10px;padding:14px;text-align:center;">
<div style="color:#22d3ee;font-size:22px;font-weight:700;font-family:monospace;">{stats.get('files', 0)}</div>
<div style="color:#64748b;font-size:10px;text-transform:uppercase;letter-spacing:1px;margin-top:4px;">Files</div>
</div>
<div style="background:rgba(15,23,42,0.6);border:1px solid rgba(51,65,85,0.5);border-radius:10px;padding:14px;text-align:center;">
<div style="color:#f472b6;font-size:22px;font-weight:700;font-family:monospace;">{size_str}</div>
<div style="color:#64748b;font-size:10px;text-transform:uppercase;letter-spacing:1px;margin-top:4px;">Size</div>
</div>
</div>
<div style="color:#94a3b8;font-size:10px;text-transform:uppercase;letter-spacing:1px;margin-bottom:12px;font-weight:600;">Languages</div>
{''.join(bars)}
</div>'''
def render_commits(commits: List[Dict]) -> str:
if not commits:
return "<p style='color:#64748b;text-align:center;padding:40px;'>No commits found.</p>"
authors = {}
for c in commits:
authors[c["author"]] = authors.get(c["author"], 0) + 1
top_author = max(authors, key=authors.get) if authors else "Unknown"
# Build a visual git graph
rows = []
for i, c in enumerate(commits[:40]):
msg = c["message"][:60] + ("..." if len(c["message"]) > 60 else "")
date = c["date"][:10] if len(c["date"]) > 10 else c["date"]
color = get_commit_color(c["message"])
initials = ''.join(p[0].upper() for p in c["author"].split() if p)[:2]
# Alternate branch line offset for visual variety
offset = (i % 3) * 8
rows.append(f'''
<div class="commit-row" style="display:flex;align-items:center;gap:10px;padding:8px 10px;margin-bottom:4px;border-radius:8px;transition:all 0.2s;cursor:default;position:relative;">
<div style="position:absolute;left:22px;top:0;bottom:0;width:2px;background:linear-gradient(180deg, {color}40, {color}10);border-radius:1px;z-index:0;"></div>
<div style="width:28px;height:28px;border-radius:50%;background:{color}20;border:2px solid {color};display:flex;align-items:center;justify-content:center;font-size:10px;color:{color};font-weight:700;z-index:1;flex-shrink:0;">{initials}</div>
<div style="flex:1;min-width:0;z-index:1;">
<div style="color:#e2e8f0;font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{msg}</div>
<div style="color:#64748b;font-size:10px;margin-top:2px;font-family:monospace;">{c["hash"]} ยท {date}</div>
</div>
<div style="flex-shrink:0;text-align:right;">
<div style="color:{color};font-size:10px;font-weight:600;background:{color}15;padding:2px 8px;border-radius:12px;">{c["author"].split()[0]}</div>
</div>
</div>
''')
more = f'<div style="color:#475569;text-align:center;padding:12px;font-size:12px;">+ {len(commits) - 40} more commits</div>' if len(commits) > 40 else ""
return f'''<div style="font-family:'Inter',system-ui,sans-serif;font-size:12px;">
<div style="display:flex;gap:16px;margin-bottom:16px;padding:14px;background:rgba(15,23,42,0.5);border:1px solid rgba(51,65,85,0.4);border-radius:12px;">
<div style="text-align:center;flex:1;">
<div style="color:#22d3ee;font-size:20px;font-weight:700;font-family:monospace;">{len(commits)}</div>
<div style="color:#64748b;font-size:9px;text-transform:uppercase;letter-spacing:1px;margin-top:2px;">Commits</div>
</div>
<div style="text-align:center;flex:1;">
<div style="color:#f472b6;font-size:20px;font-weight:700;font-family:monospace;">{len(authors)}</div>
<div style="color:#64748b;font-size:9px;text-transform:uppercase;letter-spacing:1px;margin-top:2px;">Authors</div>
</div>
<div style="text-align:center;flex:1;">
<div style="color:#fbbf24;font-size:14px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100px;margin:0 auto;">{top_author.split()[0]}</div>
<div style="color:#64748b;font-size:9px;text-transform:uppercase;letter-spacing:1px;margin-top:2px;">Top</div>
</div>
</div>
<div style="max-height:520px;overflow-y:auto;padding-right:4px;">
{''.join(rows)}
{more}
</div>
</div>'''
def render_analysis(result: Dict, analysis_type: str) -> str:
if not result.get("success"):
err = result.get("error", "Unknown error")
return f'''<div style="background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.3);border-radius:14px;padding:20px;">
<div style="color:#f87171;font-weight:700;margin-bottom:8px;font-size:14px;">โŒ Analysis Failed</div>
<div style="color:#fca5a5;font-family:monospace;font-size:12px;line-height:1.6;">{err}</div>
</div>'''
resp = result.get("response", {})
if isinstance(resp, dict):
text = resp.get("response") or resp.get("choices", [{}])[0].get("message", {}).get("content", "")
else:
text = str(resp)
if not text or text.strip() == "":
text = "*The backend returned an empty response. Try again.*"
# Convert markdown to styled HTML
text = text.replace("\n\n", "</p><p>").replace("\n", "<br>")
text = f"<p>{text}</p>"
text = re.sub(r'\*\*(.+?)\*\*', r'<strong style="color:#22d3ee">\1</strong>', text)
text = re.sub(r'`(.+?)`', r'<code style="background:rgba(34,211,238,0.1);color:#22d3ee;padding:2px 6px;border-radius:4px;font-size:12px;border:1px solid rgba(34,211,238,0.2);">\1</code>', text)
text = re.sub(r'#{3,6}\s*(.+?)(?=<|$)', r'<h4 style="color:#f472b6;margin:16px 0 8px;font-size:13px;text-transform:uppercase;letter-spacing:1px;">\1</h4>', text)
brain = result.get("brain", "capt").upper()
fallback = " ยท fallback" if result.get("fallback") else ""
icons = {"codebase": "๐Ÿ”", "commits": "๐Ÿ“Š", "security": "๐Ÿ›ก๏ธ"}
titles = {"codebase": "Codebase Analysis", "commits": "Commit Patterns", "security": "Security Audit"}
return f'''<div style="background:rgba(15,23,42,0.6);border:1px solid rgba(51,65,85,0.5);border-radius:16px;padding:24px;backdrop-filter:blur(10px);">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:14px;border-bottom:1px solid rgba(51,65,85,0.4);">
<span style="font-size:18px;">{icons.get(analysis_type, "๐Ÿง ")}</span>
<span style="color:#e2e8f0;font-weight:700;font-size:15px;letter-spacing:-0.3px;">{titles.get(analysis_type, "Analysis")}</span>
<span style="margin-left:auto;background:rgba(34,211,238,0.1);color:#22d3ee;padding:4px 12px;border-radius:20px;font-size:11px;font-weight:600;border:1px solid rgba(34,211,238,0.2);font-family:monospace;">โ— {brain}{fallback}</span>
</div>
<div style="color:#cbd5e1;line-height:1.75;font-size:13.5px;">{text}</div>
</div>'''
# โ”€โ”€ Core Logic โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def fetch_and_render(repo_url: str, max_commits: int):
repo_url = normalize_url(repo_url)
if not repo_url:
return "<p style='color:#f87171;padding:40px;text-align:center;'>Please enter a repository URL.</p>", "", ""
cached_path, meta = _get_cached(repo_url)
if cached_path:
commits = get_git_commits(cached_path, max_commits)
if commits:
stats = analyze_repo(cached_path)
return render_commits(commits), repo_url, render_stats(stats)
tmpdir = tempfile.mkdtemp(prefix="hologram_")
try:
depth = max(max_commits + 10, 50)
result = subprocess.run(
["git", "clone", "--depth", str(depth), "--no-tags", repo_url, tmpdir],
capture_output=True, text=True, timeout=60,
)
if result.returncode != 0:
err = result.stderr[:200] if result.stderr else "Unknown clone error"
return f"<p style='color:#f87171;padding:40px;text-align:center;'>โŒ Clone failed: {err}</p>", repo_url, ""
commits = get_git_commits(tmpdir, max_commits)
if not commits:
return "<p style='color:#64748b;padding:40px;text-align:center;'>No commits found.</p>", repo_url, ""
stats = analyze_repo(tmpdir)
_set_cached(repo_url, tmpdir, {"url": repo_url, "commits": len(commits), "cached_at": datetime.now().isoformat()})
return render_commits(commits), repo_url, render_stats(stats)
except subprocess.TimeoutExpired:
return "<p style='color:#f87171;padding:40px;text-align:center;'>โฑ๏ธ Clone timed out โ€” repo too large.</p>", repo_url, ""
except Exception as e:
return f"<p style='color:#f87171;padding:40px;text-align:center;'>๐Ÿ’ฅ Error: {str(e)[:200]}</p>", repo_url, ""
finally:
if os.path.exists(tmpdir) and tmpdir != cached_path:
shutil.rmtree(tmpdir, ignore_errors=True)
def run_analysis(repo_url: str, analysis_type: str):
if not repo_url:
return "<p style='color:#f87171;padding:40px;text-align:center;'>Generate a hologram first.</p>"
prompts = {
"codebase": f"Analyze the codebase at {repo_url}. Provide: 1) Architecture overview, 2) Tech stack, 3) Code quality assessment, 4) Key files and their roles, 5) Suggestions for improvement. Be thorough but concise.",
"commits": f"Analyze the commit history patterns for {repo_url}. What do the commit messages reveal about development velocity, team structure, and code maturity?",
"security": f"Perform a security audit of {repo_url}. Check for: 1) Common vulnerabilities, 2) Dependency risks, 3) Secret leakage patterns, 4) Supply chain concerns. Be specific and actionable.",
}
fallback_prompts = {
"codebase": f"Brief codebase overview of {repo_url}",
"commits": f"Commit patterns in {repo_url}",
"security": f"Security notes for {repo_url}",
}
prompt = prompts.get(analysis_type, prompts["codebase"])
for attempt, p in enumerate([prompt, fallback_prompts.get(analysis_type, "")], 1):
if not p:
continue
try:
result = backend.cogitate(p)
if result and result.get("success"):
return render_analysis(result, analysis_type)
except Exception:
pass
return '''<div style="background:rgba(251,191,36,0.05);border:1px solid rgba(251,191,36,0.2);border-radius:14px;padding:24px;text-align:center;">
<div style="color:#fbbf24;font-weight:700;font-size:16px;margin-bottom:10px;">โณ The CAPT brains are warming up</div>
<div style="color:#94a3b8;font-size:13px;line-height:1.6;">The cognitive backend is experiencing high load or cold-start latency.<br>Please wait a moment and try again.</div>
<div style="color:#64748b;font-size:11px;margin-top:10px;">Short queries usually work immediately.</div>
</div>'''
# โ”€โ”€ CSS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
CUSTOM_CSS = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
.gradio-container {
max-width: 1200px !important;
font-family: 'Inter', system-ui, sans-serif !important;
}
/* Dark holographic background */
.gradio-container {
background: linear-gradient(135deg, #0b0f1a 0%, #0f172a 40%, #1a103c 100%) !important;
background-attachment: fixed !important;
}
/* Custom scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #475569; }
/* Primary button โ€” cyan glow */
.gr-button-primary {
background: linear-gradient(135deg, #0891b2, #06b6d4) !important;
border: none !important;
border-radius: 10px !important;
box-shadow: 0 0 20px rgba(6,182,212,0.3), 0 4px 14px rgba(6,182,212,0.2) !important;
color: #fff !important;
font-weight: 600 !important;
letter-spacing: 0.3px !important;
transition: all 0.2s ease !important;
}
.gr-button-primary:hover {
box-shadow: 0 0 30px rgba(6,182,212,0.5), 0 6px 20px rgba(6,182,212,0.3) !important;
transform: translateY(-1px) !important;
}
/* Secondary elements */
.gr-box, .gr-form, .gr-panel {
background: rgba(15, 23, 42, 0.5) !important;
border: 1px solid rgba(51, 65, 85, 0.4) !important;
border-radius: 14px !important;
backdrop-filter: blur(12px) !important;
}
/* Inputs */
.gr-input, .gr-textbox textarea, .gr-textbox input {
background: rgba(15, 23, 42, 0.6) !important;
border: 1px solid rgba(51, 65, 85, 0.5) !important;
border-radius: 10px !important;
color: #e2e8f0 !important;
font-family: 'JetBrains Mono', monospace !important;
}
.gr-input:focus, .gr-textbox textarea:focus {
border-color: #22d3ee !important;
box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.1) !important;
}
/* Labels */
.gr-form .gr-box > label, .gr-input-label {
color: #94a3b8 !important;
font-size: 11px !important;
text-transform: uppercase !important;
letter-spacing: 1px !important;
font-weight: 600 !important;
}
/* Dropdown */
.gr-dropdown {
background: rgba(15, 23, 42, 0.6) !important;
border: 1px solid rgba(51, 65, 85, 0.5) !important;
border-radius: 10px !important;
color: #e2e8f0 !important;
}
/* Slider */
.gr-slider input[type="range"] {
accent-color: #22d3ee !important;
}
/* JSON panel */
.gr-json {
background: rgba(15, 23, 42, 0.4) !important;
border: 1px solid rgba(51, 65, 85, 0.3) !important;
border-radius: 12px !important;
color: #e2e8f0 !important;
font-family: 'JetBrains Mono', monospace !important;
font-size: 12px !important;
}
/* Commit row hover */
.commit-row:hover {
background: rgba(34, 211, 238, 0.05) !important;
}
"""
# โ”€โ”€ Gradio UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with gr.Blocks(
title="Git Hologram",
css=CUSTOM_CSS,
theme=gr.themes.Base(
primary_hue="cyan",
secondary_hue="violet",
neutral_hue="slate",
).set(
body_background_fill="transparent",
body_text_color="#e2e8f0",
block_background_fill="rgba(15,23,42,0.5)",
block_border_color="rgba(51,65,85,0.4)",
block_border_width="1px",
block_label_text_color="#94a3b8",
input_background_fill="rgba(15,23,42,0.6)",
input_border_color="rgba(51,65,85,0.5)",
input_placeholder_color="#475569",
button_primary_background_fill="linear-gradient(135deg, #0891b2, #06b6d4)",
button_primary_text_color="#ffffff",
)
) as demo:
# Hero header
gr.HTML('''
<div style="text-align:center;padding:40px 20px 30px;position:relative;overflow:hidden;">
<div style="position:absolute;top:0;left:50%;transform:translateX(-50%);width:600px;height:300px;background:radial-gradient(ellipse at center, rgba(34,211,238,0.08) 0%, transparent 70%);pointer-events:none;"></div>
<div style="font-size:3.2em;margin:0;line-height:1;">๐Ÿ”ฎ</div>
<h1 style="font-size:2em;margin:12px 0 8px;color:#e2e8f0;font-weight:700;letter-spacing:-0.5px;">
Git Hologram
</h1>
<p style="font-size:0.95em;color:#64748b;margin:0;max-width:480px;margin-left:auto;margin-right:auto;line-height:1.6;">
Visualize repository history as a living neural graph.
Clone any repo and analyze it through the CAPT cognitive lens.
</p>
<div style="display:flex;gap:8px;justify-content:center;margin-top:20px;">
<span style="background:rgba(34,211,238,0.1);color:#22d3ee;padding:4px 12px;border-radius:20px;font-size:11px;font-weight:600;border:1px solid rgba(34,211,238,0.2);">Git Clone</span>
<span style="background:rgba(244,114,182,0.1);color:#f472b6;padding:4px 12px;border-radius:20px;font-size:11px;font-weight:600;border:1px solid rgba(244,114,182,0.2);">Commit Graph</span>
<span style="background:rgba(251,191,36,0.1);color:#fbbf24;padding:4px 12px;border-radius:20px;font-size:11px;font-weight:600;border:1px solid rgba(251,191,36,0.2);">CAPT Analysis</span>
</div>
</div>
''')
# Brain status
gr.HTML(health_banner_html(backend))
# Input row
with gr.Row(equal_height=True):
with gr.Column(scale=4):
repo_input = gr.Textbox(
label="Repository",
placeholder="owner/repo or https://github.com/owner/repo",
lines=1,
)
with gr.Column(scale=1):
max_commits = gr.Slider(minimum=10, maximum=200, value=50, step=10, label="Max Commits")
with gr.Column(scale=1):
generate_btn = gr.Button("๐Ÿ”ฎ Generate Hologram", variant="primary", size="lg")
# Results
with gr.Row():
with gr.Column(scale=3):
hologram_output = gr.HTML(label="Commit Neural Graph")
with gr.Column(scale=2):
stats_output = gr.HTML(label="Repository Telemetry")
current_repo = gr.State("")
# Analysis section
with gr.Row():
with gr.Column(scale=3):
analysis_type = gr.Dropdown(
choices=[("๐Ÿ” Codebase Analysis", "codebase"), ("๐Ÿ“Š Commit Patterns", "commits"), ("๐Ÿ›ก๏ธ Security Audit", "security")],
value="codebase",
label="Analysis Type",
)
with gr.Column(scale=1):
analyze_btn = gr.Button("๐Ÿง  Analyze with CAPT", variant="primary")
analysis_output = gr.HTML(label="CAPT Analysis")
# Footer
gr.HTML('''
<div style="text-align:center;color:#334155;font-size:11px;margin-top:32px;padding:20px;border-top:1px solid rgba(51,65,85,0.3);">
<div style="margin-bottom:8px;">
<span style="color:#475569;">Git Hologram</span>
<span style="color:#334155;margin:0 8px;">ยท</span>
<span style="color:#475569;">Powered by CAPT</span>
<span style="color:#334155;margin:0 8px;">ยท</span>
<span style="color:#475569;">5 Brain Instances</span>
</div>
<div style="color:#334155;">Real-time cognitive analysis through Cloudflare Workers</div>
</div>
''')
# Event wiring
generate_btn.click(
fn=fetch_and_render,
inputs=[repo_input, max_commits],
outputs=[hologram_output, current_repo, stats_output]
)
repo_input.submit(
fn=fetch_and_render,
inputs=[repo_input, max_commits],
outputs=[hologram_output, current_repo, stats_output]
)
analyze_btn.click(
fn=run_analysis,
inputs=[current_repo, analysis_type],
outputs=analysis_output
)
if __name__ == "__main__":
demo.launch(show_error=True, server_name="0.0.0.0", server_port=7860)