Spaces:
Running
Running
| #!/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) | |