""" DevRel Campaign Generator - Gradio 6 Version A cyberpunk-themed DevRel content generator powered by Gradio 6 and Claude. Transform any GitHub repository into a complete DevRel campaign in minutes. Features: - šŸ“š Step-by-step tutorials - šŸ“ Technical blog posts - šŸŽ¤ Conference talk outlines - 🐦 Social media threads (Twitter/X) - šŸ’¼ LinkedIn posts - šŸ† Hackathon challenges """ import asyncio import os import gradio as gr from typing import Optional from datetime import datetime from services import ( ClaudeService, RepoProfile, ContentVariant, ) from github_utils import ( fetch_github_repo, fetch_recent_commits, get_since_iso, fetch_commits_with_diffs, fetch_readme_changes, analyze_breaking_changes, ) # Custom CSS - Modern, adaptive design with smooth transitions CUSTOM_CSS = """ /* ======================================== CSS Variables for Light/Dark Mode ======================================== */ :root { /* Light mode colors */ --bg-primary: #fafafa; --bg-secondary: #ffffff; --bg-tertiary: #f5f5f5; --bg-card: #ffffff; --text-primary: #18181b; --text-secondary: #52525b; --text-muted: #a1a1aa; --border-color: #e4e4e7; --border-light: #f4f4f5; --accent-purple: #8b5cf6; --accent-purple-light: rgba(139, 92, 246, 0.1); --accent-green: #22c55e; --accent-green-light: rgba(34, 197, 94, 0.1); --accent-orange: #f97316; --accent-orange-light: rgba(249, 115, 22, 0.1); --accent-blue: #3b82f6; --accent-cyan: #06b6d4; --accent-pink: #ec4899; --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); --shadow-glow: 0 0 40px rgba(139, 92, 246, 0.15); --radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; --radius-xl: 20px; --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } /* Dark mode colors */ .dark, html.dark, [data-theme="dark"] { --bg-primary: #09090b; --bg-secondary: #18181b; --bg-tertiary: #27272a; --bg-card: #1c1c1f; --text-primary: #fafafa; --text-secondary: #a1a1aa; --text-muted: #71717a; --border-color: #27272a; --border-light: #3f3f46; --accent-purple: #a78bfa; --accent-purple-light: rgba(167, 139, 250, 0.15); --accent-green: #4ade80; --accent-green-light: rgba(74, 222, 128, 0.15); --accent-orange: #fb923c; --accent-orange-light: rgba(251, 146, 60, 0.15); --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4); --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5); --shadow-glow: 0 0 60px rgba(139, 92, 246, 0.2); } /* ======================================== Base Styles ======================================== */ html, body { background: var(--bg-primary) !important; color: var(--text-primary) !important; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; transition: var(--transition); } .gradio-container { background: var(--bg-primary) !important; max-width: 1600px !important; margin: 0 auto !important; padding: 32px !important; } /* ======================================== Cards & Groups - Glass Morphism Style ======================================== */ .group { background: var(--bg-card) !important; border: 1px solid var(--border-color) !important; border-radius: var(--radius-xl) !important; padding: 28px !important; margin-bottom: 24px !important; box-shadow: var(--shadow-md) !important; backdrop-filter: blur(10px) !important; transition: var(--transition); } .group:hover { box-shadow: var(--shadow-lg), var(--shadow-glow) !important; transform: translateY(-2px); } .dark .group { background: linear-gradient(145deg, var(--bg-card) 0%, rgba(28, 28, 31, 0.8) 100%) !important; border: 1px solid rgba(139, 92, 246, 0.15) !important; } /* ======================================== Form Elements ======================================== */ input, textarea, select { background: var(--bg-tertiary) !important; border: 1px solid var(--border-color) !important; border-radius: var(--radius-md) !important; color: var(--text-primary) !important; padding: 14px 18px !important; font-size: 15px !important; transition: var(--transition); } input:focus, textarea:focus, select:focus { border-color: var(--accent-purple) !important; box-shadow: 0 0 0 3px var(--accent-purple-light) !important; outline: none !important; } label, .label-wrap { color: var(--text-secondary) !important; font-weight: 500 !important; font-size: 14px !important; margin-bottom: 8px !important; } /* ======================================== Buttons - Modern with Smooth Hover ======================================== */ button.primary { background: linear-gradient(135deg, var(--accent-purple), #7c3aed) !important; border: none !important; border-radius: var(--radius-md) !important; color: white !important; font-weight: 600 !important; font-size: 15px !important; padding: 14px 28px !important; box-shadow: var(--shadow-md), 0 0 20px rgba(139, 92, 246, 0.3) !important; transition: var(--transition); } button.primary:hover { transform: translateY(-2px) !important; box-shadow: var(--shadow-lg), 0 0 40px rgba(139, 92, 246, 0.5) !important; } button.secondary { background: var(--bg-tertiary) !important; border: 1px solid var(--border-color) !important; border-radius: var(--radius-md) !important; color: var(--text-secondary) !important; font-weight: 500 !important; padding: 14px 28px !important; transition: var(--transition); } button.secondary:hover { background: var(--bg-secondary) !important; border-color: var(--accent-purple) !important; color: var(--accent-purple) !important; } /* ======================================== Tabs - Premium Look ======================================== */ .tabs { background: var(--bg-card) !important; border: 1px solid var(--border-color) !important; border-radius: var(--radius-xl) !important; overflow: hidden !important; box-shadow: var(--shadow-md) !important; } .dark .tabs { background: linear-gradient(145deg, var(--bg-card) 0%, rgba(28, 28, 31, 0.9) 100%) !important; border: 1px solid rgba(74, 222, 128, 0.15) !important; } .tab-nav { background: var(--bg-tertiary) !important; border-bottom: 1px solid var(--border-color) !important; padding: 12px 16px !important; display: flex !important; gap: 8px !important; } .tab-nav button { background: transparent !important; border: none !important; border-radius: var(--radius-md) !important; color: var(--text-muted) !important; font-weight: 500 !important; font-size: 14px !important; padding: 12px 20px !important; transition: var(--transition); } .tab-nav button:hover { background: var(--accent-purple-light) !important; color: var(--accent-purple) !important; } .tab-nav button.selected { background: linear-gradient(135deg, var(--accent-purple-light), var(--accent-green-light)) !important; color: var(--accent-purple) !important; font-weight: 600 !important; } .tabitem { background: var(--bg-secondary) !important; padding: 28px !important; } /* ======================================== Accordion - Expandable Sections ======================================== */ .accordion { background: var(--bg-card) !important; border: 1px solid var(--border-color) !important; border-radius: var(--radius-lg) !important; margin-top: 20px !important; overflow: hidden !important; transition: var(--transition); } .dark .accordion { background: linear-gradient(145deg, rgba(28, 28, 31, 0.8), rgba(39, 39, 42, 0.5)) !important; border: 1px solid rgba(249, 115, 22, 0.15) !important; } .accordion:hover { border-color: var(--accent-orange) !important; } /* ======================================== Markdown Content - Clean Typography ======================================== */ .markdown, .prose { color: var(--text-primary) !important; font-size: 15px !important; line-height: 1.8 !important; padding: 20px !important; } .markdown h1, .markdown h2, .markdown h3 { color: var(--text-primary) !important; font-weight: 700 !important; margin-top: 28px !important; margin-bottom: 16px !important; } .markdown p { margin-bottom: 16px !important; color: var(--text-secondary) !important; } .markdown code { background: var(--accent-purple-light) !important; color: var(--accent-purple) !important; padding: 3px 8px !important; border-radius: 6px !important; font-family: 'JetBrains Mono', 'Fira Code', monospace !important; font-size: 14px !important; } .markdown pre { background: var(--bg-tertiary) !important; border: 1px solid var(--border-color) !important; border-radius: var(--radius-md) !important; padding: 20px !important; overflow-x: auto !important; } .markdown ul, .markdown ol { padding-left: 28px !important; margin-bottom: 16px !important; } .markdown li { margin-bottom: 8px !important; color: var(--text-secondary) !important; } /* ======================================== Scrollbar - Subtle & Modern ======================================== */ ::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar-track { background: var(--bg-tertiary); border-radius: 5px; } ::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 5px; border: 2px solid var(--bg-tertiary); } ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } /* ======================================== Animations ======================================== */ @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(0.95); } } @keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } } @keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } @keyframes glow-pulse { 0%, 100% { box-shadow: 0 0 20px rgba(139, 92, 246, 0.3); } 50% { box-shadow: 0 0 40px rgba(139, 92, 246, 0.5); } } /* ======================================== Utility Classes ======================================== */ .glow-purple { box-shadow: 0 0 30px rgba(139, 92, 246, 0.4); } .glow-green { box-shadow: 0 0 30px rgba(34, 197, 94, 0.4); } /* ======================================== Layout Adjustments ======================================== */ .row { gap: 28px !important; } .col { padding: 0 !important; } /* Container max width */ .gradio-container > .main { padding: 0 !important; } /* Responsive adjustments */ @media (max-width: 768px) { .gradio-container { padding: 16px !important; } .group { padding: 20px !important; } .tab-nav { flex-wrap: wrap !important; } .tab-nav button { font-size: 12px !important; padding: 10px 14px !important; } } /* ======================================== Special Elements ======================================== */ /* Hero section styling */ .hero-card { background: linear-gradient(145deg, var(--bg-card), var(--bg-secondary)) !important; border: 1px solid var(--border-color) !important; } .dark .hero-card { background: linear-gradient(145deg, rgba(28, 28, 31, 0.9), rgba(18, 18, 20, 0.9)) !important; border: 1px solid rgba(139, 92, 246, 0.2) !important; } /* Terminal styling */ .terminal-header { background: linear-gradient(90deg, var(--bg-tertiary), var(--bg-secondary)) !important; } .dark .terminal-header { background: linear-gradient(90deg, rgba(28, 28, 31, 0.9), rgba(24, 24, 27, 0.9)) !important; } /* Status indicator */ .status-online { width: 10px; height: 10px; border-radius: 50%; background: var(--accent-green); animation: pulse 2s infinite; box-shadow: 0 0 10px var(--accent-green); } /* Badge styling */ .badge { display: inline-flex; align-items: center; padding: 6px 14px; border-radius: 20px; font-size: 12px; font-weight: 600; transition: var(--transition); } .badge-purple { background: var(--accent-purple-light); color: var(--accent-purple); border: 1px solid rgba(139, 92, 246, 0.3); } .badge-green { background: var(--accent-green-light); color: var(--accent-green); border: 1px solid rgba(34, 197, 94, 0.3); } .badge-orange { background: var(--accent-orange-light); color: var(--accent-orange); border: 1px solid rgba(249, 115, 22, 0.3); } /* Feature card */ .feature-card { background: var(--bg-secondary) !important; border: 1px solid var(--border-color) !important; border-radius: var(--radius-lg) !important; padding: 16px !important; transition: var(--transition); } .feature-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); border-color: var(--accent-purple) !important; } /* Icon box */ .icon-box { width: 44px; height: 44px; border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; transition: var(--transition); } .icon-box:hover { transform: scale(1.1); } /* ======================================== Custom Card Classes - Matching About Section Style ======================================== */ .command-center-card, .pipeline-status-card, .campaign-output-card { background: var(--bg-card, #ffffff) !important; border: 1px solid var(--border-color, #e4e4e7) !important; border-radius: 20px !important; padding: 32px !important; margin-bottom: 24px !important; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; } .command-center-card:hover, .pipeline-status-card:hover, .campaign-output-card:hover { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05), 0 0 40px rgba(139, 92, 246, 0.1) !important; } /* Dark mode for all business cards */ .dark .command-center-card, .dark .pipeline-status-card, .dark .campaign-output-card { background: var(--bg-card, #1c1c1f) !important; border: 1px solid rgba(139, 92, 246, 0.15) !important; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3) !important; } .dark .command-center-card:hover, .dark .pipeline-status-card:hover, .dark .campaign-output-card:hover { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.4), 0 0 60px rgba(139, 92, 246, 0.2) !important; border-color: rgba(139, 92, 246, 0.25) !important; } /* Command Center - Purple accent border on hover */ .command-center-card:hover { border-color: rgba(139, 92, 246, 0.3) !important; } /* Pipeline Status - Green accent border on hover */ .pipeline-status-card:hover { border-color: rgba(34, 197, 94, 0.3) !important; } /* Campaign Output - Cyan accent border on hover */ .campaign-output-card:hover { border-color: rgba(6, 182, 212, 0.3) !important; } /* Terminal content styling */ .terminal-content { max-height: 200px !important; overflow-y: auto !important; padding: 16px !important; background: var(--bg-tertiary, #f5f5f5) !important; border-radius: 12px !important; border: 1px solid var(--border-color, #e4e4e7) !important; font-family: 'JetBrains Mono', 'Fira Code', monospace !important; font-size: 13px !important; line-height: 1.6 !important; } .dark .terminal-content { background: rgba(39, 39, 42, 0.5) !important; border-color: rgba(63, 63, 70, 0.5) !important; } /* Tabs inside Campaign Output - matching card style */ .campaign-output-card .tabs { background: transparent !important; border: none !important; box-shadow: none !important; margin-top: 0 !important; } .campaign-output-card .tab-nav { background: var(--bg-tertiary, #f5f5f5) !important; border: 1px solid var(--border-color, #e4e4e7) !important; border-radius: 14px !important; padding: 8px !important; gap: 6px !important; margin-bottom: 16px !important; } .dark .campaign-output-card .tab-nav { background: rgba(39, 39, 42, 0.4) !important; border-color: rgba(63, 63, 70, 0.4) !important; } .campaign-output-card .tab-nav button { background: transparent !important; border: none !important; border-radius: 10px !important; color: var(--text-muted, #71717a) !important; font-weight: 500 !important; font-size: 13px !important; padding: 10px 16px !important; transition: all 0.2s ease !important; } .campaign-output-card .tab-nav button:hover { background: rgba(139, 92, 246, 0.1) !important; color: var(--accent-purple, #8b5cf6) !important; } .campaign-output-card .tab-nav button.selected { background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(34, 197, 94, 0.1)) !important; color: var(--accent-purple, #8b5cf6) !important; font-weight: 600 !important; box-shadow: 0 2px 4px rgba(139, 92, 246, 0.15) !important; } .campaign-output-card .tabitem { background: var(--bg-tertiary, #f5f5f5) !important; border: 1px solid var(--border-color, #e4e4e7) !important; border-radius: 14px !important; padding: 24px !important; min-height: 300px !important; } .dark .campaign-output-card .tabitem { background: rgba(39, 39, 42, 0.3) !important; border-color: rgba(63, 63, 70, 0.4) !important; } /* Markdown content inside tabs */ .campaign-output-card .markdown { color: var(--text-primary, #18181b) !important; font-size: 14px !important; line-height: 1.7 !important; } .dark .campaign-output-card .markdown { color: var(--text-primary, #fafafa) !important; } /* Form elements inside cards */ .command-center-card input, .command-center-card textarea, .command-center-card select { background: var(--bg-tertiary, #f5f5f5) !important; border: 1px solid var(--border-color, #e4e4e7) !important; border-radius: 12px !important; } .dark .command-center-card input, .dark .command-center-card textarea, .dark .command-center-card select { background: rgba(39, 39, 42, 0.5) !important; border-color: rgba(63, 63, 70, 0.5) !important; } /* Accordion styling inside cards */ .command-center-card .accordion { background: var(--bg-tertiary, #f5f5f5) !important; border: 1px solid var(--border-color, #e4e4e7) !important; border-radius: 14px !important; margin-top: 16px !important; } .dark .command-center-card .accordion { background: rgba(39, 39, 42, 0.4) !important; border-color: rgba(249, 115, 22, 0.15) !important; } .command-center-card .accordion:hover { border-color: rgba(249, 115, 22, 0.3) !important; } /* Buttons inside cards */ .command-center-card button.primary { background: linear-gradient(135deg, #8b5cf6, #7c3aed) !important; border: none !important; border-radius: 12px !important; color: white !important; font-weight: 600 !important; padding: 14px 24px !important; box-shadow: 0 4px 6px rgba(139, 92, 246, 0.25), 0 0 20px rgba(139, 92, 246, 0.2) !important; transition: all 0.3s ease !important; } .command-center-card button.primary:hover { transform: translateY(-2px) !important; box-shadow: 0 6px 12px rgba(139, 92, 246, 0.35), 0 0 30px rgba(139, 92, 246, 0.3) !important; } .command-center-card button.secondary { background: var(--bg-tertiary, #f5f5f5) !important; border: 1px solid var(--border-color, #e4e4e7) !important; border-radius: 12px !important; color: var(--text-secondary, #52525b) !important; font-weight: 500 !important; padding: 14px 24px !important; transition: all 0.3s ease !important; } .dark .command-center-card button.secondary { background: rgba(39, 39, 42, 0.5) !important; border-color: rgba(63, 63, 70, 0.5) !important; color: var(--text-secondary, #a1a1aa) !important; } .command-center-card button.secondary:hover { background: var(--bg-secondary, #ffffff) !important; border-color: var(--accent-purple, #8b5cf6) !important; color: var(--accent-purple, #8b5cf6) !important; } """ # Global state current_job = { "id": None, "status": "idle", "progress": 0, "logs": [], "profile": None, "variants": {}, "selected_variants": {}, } def add_log(agent: str, message: str, color: str = "text-foreground") -> str: """Add a log entry and return formatted log.""" timestamp = datetime.now().strftime("%H:%M:%S") entry = f"[{timestamp}] [{agent}] {message}" current_job["logs"].append({"agent": agent, "message": message, "color": color}) return format_logs() def format_logs() -> str: """Format all logs as HTML.""" if not current_job["logs"]: return '
Waiting for input...
Enter a GitHub repo URL to begin
' color_map = { "text-primary": "#8b5cf6", "text-accent": "#22c55e", "text-chart-3": "#06b6d4", "text-chart-4": "#ec4899", "text-chart-5": "#f59e0b", "text-destructive": "#ef4444", "text-chart-1": "#22c55e", } lines = [] for log in current_job["logs"]: color = color_map.get(log["color"], "var(--text-primary, #18181b)") lines.append( f'
' f'› ' f'[{log["agent"]}] ' f'{log["message"]}' f"
" ) return "".join(lines) def get_progress_html(progress: int, status: str) -> str: """Generate progress bar HTML.""" status_colors = { "idle": ("var(--text-muted, #71717a)", "var(--border-color, #e4e4e7)"), "analyzing": ("#8b5cf6", "rgba(139, 92, 246, 0.2)"), "generating": ("#22c55e", "rgba(34, 197, 94, 0.2)"), "completed": ("#22c55e", "rgba(34, 197, 94, 0.2)"), "failed": ("#ef4444", "rgba(239, 68, 68, 0.2)"), } text_color, bg_color = status_colors.get(status, status_colors["idle"]) return f"""
Pipeline Status {status}
""" def get_profile_html(profile: Optional[RepoProfile]) -> str: """Generate profile info HTML.""" if not profile: return "" commits_html = "" if profile.commits and len(profile.commits) > 0: commit_items = "".join( [ f'
  • ' f'{c.get("message", "")[:50]}' f'{c.get("sha", "")[:7]}' f"
  • " for c in profile.commits[:5] ] ) commits_html = f"""
    Recent Commits
    """ # Breaking changes section breaking_html = "" if profile.breaking_changes and len(profile.breaking_changes) > 0: bc_items = "".join( [ f'
  • ' f'[{bc.get("sha", "")}] {bc.get("message", "")[:40]}...' f"
  • " for bc in profile.breaking_changes[:3] ] ) breaking_html = f"""
    āš ļø Potential Breaking Changes
    """ # README changes section readme_html = "" if profile.has_readme_changes: readme_html = f"""
    šŸ“ README Documentation Updated
    High priority - documentation changes detected
    """ features_list = ", ".join(profile.main_features[:3]) if profile.main_features else "N/A" return f"""
    Repository
    {profile.name}
    Language
    {profile.primary_language}
    Key Features
    {features_list}
    {breaking_html} {readme_html} {commits_html} """ async def generate_campaign(repo_url: str, time_range: str, anthropic_key: str, github_token: str, progress=gr.Progress()): """Main generation function with streaming progress updates.""" global current_job if not repo_url: yield ( format_logs(), get_progress_html(0, "idle"), "", "", "", "", "", "", "", ) return # API key priority: Environment variables (HF Spaces secrets) > User input # This allows HF Spaces to use pre-configured secrets while still allowing user override custom_anthropic_key = anthropic_key.strip() if anthropic_key else None custom_github_token = github_token.strip() if github_token else None # Reset state current_job = { "id": f"job-{datetime.now().timestamp()}", "status": "pending", "progress": 0, "logs": [], "profile": None, "variants": {}, "selected_variants": {}, } formats = ["twitter", "blog", "tutorial", "talk", "linkedin", "hackathon"] try: # Phase 1: Analyzing repository current_job["status"] = "analyzing" current_job["progress"] = 10 add_log("System", "Initializing DevRel Campaign Generator...", "text-primary") progress(0.1, desc="Initializing...") yield ( format_logs(), get_progress_html(10, "analyzing"), "", "", "", "", "", "", "", ) add_log("Understanding Agent", "Fetching repository metadata...", "text-chart-3") yield ( format_logs(), get_progress_html(15, "analyzing"), "", "", "", "", "", "", "", ) repo_data = await fetch_github_repo(repo_url, custom_github_token) add_log( "Understanding Agent", f"Found {len(repo_data.get('languages', {}))} languages", "text-chart-3", ) # Fetch commits add_log( "Understanding Agent", f"Fetching recent commits for {time_range} window...", "text-chart-3", ) since_iso = get_since_iso(time_range) recent_commits = await fetch_recent_commits(repo_url, 50, since_iso, github_token=custom_github_token) if recent_commits: add_log( "Understanding Agent", f"Loaded {len(recent_commits)} recent commits since {since_iso[:10]}", "text-chart-3", ) else: add_log( "Understanding Agent", "No recent commits available or fetch failed", "text-chart-5", ) # Fetch code diffs for recent commits add_log( "Understanding Agent", "Fetching code diffs for recent commits...", "text-chart-3", ) commits_with_diffs = await fetch_commits_with_diffs( repo_url, recent_commits, max_commits=5, github_token=custom_github_token ) if commits_with_diffs: add_log( "Understanding Agent", f"Analyzed diffs from {len(commits_with_diffs)} commits", "text-chart-3", ) # Analyze for breaking changes add_log( "Understanding Agent", "Scanning for potential breaking changes...", "text-chart-3", ) breaking_changes = await analyze_breaking_changes(commits_with_diffs) if breaking_changes: add_log( "Understanding Agent", f"āš ļø Found {len(breaking_changes)} commits with potential breaking changes", "text-chart-5", ) else: add_log( "Understanding Agent", "No breaking changes detected", "text-chart-3", ) # Fetch README changes (highest priority for DevRel content) add_log( "Understanding Agent", "Checking for README documentation changes...", "text-chart-3", ) readme_changes = await fetch_readme_changes(repo_url, since_iso, custom_github_token) if readme_changes.get("has_readme_changes"): add_log( "Understanding Agent", f"šŸ“ README updated {len(readme_changes.get('readme_commits', []))} times - HIGH PRIORITY", "text-chart-1", ) else: add_log( "Understanding Agent", "No README changes in the analysis period", "text-chart-3", ) yield ( format_logs(), get_progress_html(25, "analyzing"), "", "", "", "", "", "", "", ) # Phase 2: Extract profile with Claude current_job["progress"] = 25 add_log("Claude", "Analyzing repository structure...", "text-primary") try: claude = ClaudeService(api_key=custom_anthropic_key) add_log("Claude", "Extracting repository profile...", "text-primary") extracted_profile = claude.extract_repo_profile(repo_data) except Exception as e: error_msg = str(e) if "ANTHROPIC_API_KEY" in error_msg or "API key" in error_msg: add_log( "System", "āš ļø Claude API key not configured or quota exhausted", "text-chart-5" ) add_log( "System", "šŸ’” Please add your own API key in Settings below", "text-chart-5" ) add_log( "System", "Using fallback content generation...", "text-chart-5" ) elif "credit" in error_msg.lower() or "quota" in error_msg.lower() or "rate" in error_msg.lower(): add_log( "System", "āš ļø API quota exhausted or rate limited", "text-chart-5" ) add_log( "System", "šŸ’” Please add your own API key in Settings below", "text-chart-5" ) add_log( "System", "Using fallback content generation...", "text-chart-5" ) else: add_log("System", f"āš ļø Claude error: {error_msg}", "text-chart-5") add_log( "System", "Using fallback content generation...", "text-chart-5" ) extracted_profile = { "mainFeatures": ["Feature analysis unavailable - add API key"], "technicalStack": list(repo_data.get("languages", {}).keys()), "quickstartPath": None, } claude = None # Will use fallback profile = RepoProfile( url=repo_data.get("url", repo_url), owner=repo_data.get("owner", ""), name=repo_data.get("name", ""), description=repo_data.get("description", ""), languages=repo_data.get("languages", {}), primary_language=repo_data.get("primaryLanguage", "Unknown"), stars=repo_data.get("stars", 0), forks=repo_data.get("forks", 0), structure=repo_data.get("structure", {}), readme=repo_data.get("readme", "")[:5000], main_features=extracted_profile.get("mainFeatures", []), technical_stack=extracted_profile.get("technicalStack", []), quickstart_path=extracted_profile.get("quickstartPath"), commits=recent_commits, commits_with_diffs=commits_with_diffs, breaking_changes=breaking_changes, initial_readme=readme_changes.get("initial_readme", ""), readme_diff=readme_changes.get("readme_diff", ""), has_readme_changes=readme_changes.get("has_readme_changes", False), ) current_job["profile"] = profile current_job["progress"] = 35 add_log( "Claude", f"Identified {len(profile.main_features)} key features", "text-primary", ) progress(0.35, desc="Profile extracted...") profile_html = get_profile_html(profile) yield ( format_logs(), get_progress_html(35, "analyzing") + profile_html, "", "", "", "", "", "", "", ) # Phase 3: Generate content variants with Claude current_job["status"] = "generating" current_job["progress"] = 40 has_api_key = bool(custom_anthropic_key or os.environ.get("ANTHROPIC_API_KEY")) if not has_api_key or claude is None: add_log( "Claude", "Generating fallback content (no API key)...", "text-chart-5", ) add_log( "System", "šŸ’” Add your API key in Settings for AI-powered generation", "text-chart-5", ) # Create a new claude service for fallback try: claude = ClaudeService(api_key=custom_anthropic_key) except Exception: pass else: add_log( "Claude", "Generating AI-powered content variants...", "text-primary", ) all_variants = [] progress_per_format = 50 / len(formats) for i, fmt in enumerate(formats): add_log("Claude", f"Creating {fmt} content...", "text-primary") if claude: variants = claude.generate_content_variants(profile, fmt, 1) else: # Fallback variants variants = [ ContentVariant( id=f"{fmt}-variant-1", format=fmt, content=f"# {fmt.title()} Content for {profile.name}\n\nāš ļø **AI content generation unavailable**\n\nTo generate AI-powered content, please add your Anthropic API key in the **āš™ļø Settings** section below the Command Center.\n\n**Get your API key:**\n1. Go to [console.anthropic.com](https://console.anthropic.com)\n2. Create an account or sign in\n3. Generate an API key\n4. Paste it in the Settings section", ) ] all_variants.extend(variants) current_job["progress"] = 40 + int((i + 1) * progress_per_format) progress(current_job["progress"] / 100, desc=f"Generated {fmt}...") # Update variants display in real-time for v in variants: current_job["variants"][v.format] = v yield ( format_logs(), get_progress_html(current_job["progress"], "generating") + profile_html, current_job["variants"].get("twitter", ContentVariant("", "twitter", "")).content, current_job["variants"].get("blog", ContentVariant("", "blog", "")).content, current_job["variants"].get("tutorial", ContentVariant("", "tutorial", "")).content, current_job["variants"].get("talk", ContentVariant("", "talk", "")).content, current_job["variants"].get("linkedin", ContentVariant("", "linkedin", "")).content, current_job["variants"].get("hackathon", ContentVariant("", "hackathon", "")).content, "", ) # Complete current_job["status"] = "completed" current_job["progress"] = 100 add_log("System", "Campaign generation complete! ✨", "text-primary") progress(1.0, desc="Complete!") yield ( format_logs(), get_progress_html(100, "completed") + profile_html, current_job["variants"].get("twitter", ContentVariant("", "twitter", "")).content, current_job["variants"].get("blog", ContentVariant("", "blog", "")).content, current_job["variants"].get("tutorial", ContentVariant("", "tutorial", "")).content, current_job["variants"].get("talk", ContentVariant("", "talk", "")).content, current_job["variants"].get("linkedin", ContentVariant("", "linkedin", "")).content, current_job["variants"].get("hackathon", ContentVariant("", "hackathon", "")).content, "", ) except Exception as e: current_job["status"] = "failed" add_log("System", f"āŒ Error: {str(e)}", "text-destructive") yield ( format_logs(), get_progress_html(current_job["progress"], "failed"), "", "", "", "", "", "", "", ) def reset_state(): """Reset all state.""" global current_job current_job = { "id": None, "status": "idle", "progress": 0, "logs": [], "profile": None, "variants": {}, "selected_variants": {}, } return ( "", # repo_url "3months", # time_range format_logs(), # logs get_progress_html(0, "idle"), # progress "", # twitter "", # blog "", # tutorial "", # talk "", # linkedin "", # hackathon "", # metrics ) # Build Gradio interface def create_app(): """Create and configure the Gradio app.""" # Create custom theme - adaptive to light/dark modes custom_theme = gr.themes.Soft( primary_hue="purple", secondary_hue="green", neutral_hue="zinc", font=gr.themes.GoogleFont("Inter"), font_mono=gr.themes.GoogleFont("JetBrains Mono"), ).set( # Light mode backgrounds body_background_fill="#fafafa", block_background_fill="#ffffff", panel_background_fill="#ffffff", # Dark mode backgrounds body_background_fill_dark="#09090b", block_background_fill_dark="#1c1c1f", panel_background_fill_dark="#1c1c1f", # Borders block_border_color="#e4e4e7", block_border_color_dark="rgba(139, 92, 246, 0.2)", border_color_primary="#8b5cf6", border_color_primary_dark="#a78bfa", # Inputs input_background_fill="#f5f5f5", input_background_fill_dark="#27272a", input_border_color="#e4e4e7", input_border_color_dark="#3f3f46", # Text colors body_text_color="#18181b", body_text_color_dark="#fafafa", block_title_text_color="#18181b", block_title_text_color_dark="#fafafa", block_label_text_color="#52525b", block_label_text_color_dark="#a1a1aa", # Buttons button_primary_background_fill="linear-gradient(135deg, #8b5cf6, #7c3aed)", button_primary_background_fill_dark="linear-gradient(135deg, #a78bfa, #8b5cf6)", button_primary_background_fill_hover="linear-gradient(135deg, #7c3aed, #6d28d9)", button_primary_background_fill_hover_dark="linear-gradient(135deg, #8b5cf6, #7c3aed)", button_primary_text_color="white", button_primary_text_color_dark="white", button_secondary_background_fill="#f5f5f5", button_secondary_background_fill_dark="#27272a", button_secondary_text_color="#52525b", button_secondary_text_color_dark="#a1a1aa", # Shadows block_shadow="0 4px 6px -1px rgba(0, 0, 0, 0.1)", block_shadow_dark="0 4px 6px -1px rgba(0, 0, 0, 0.4)", # Radius block_radius="16px", button_large_radius="12px", button_small_radius="8px", ) # No forced dark mode - let system preference handle it DARK_MODE_JS = """ function() { // Respect system preference if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { document.documentElement.classList.add('dark'); } } """ with gr.Blocks(title="DevRel Campaign Generator") as app: # Set theme and CSS after Blocks creation (Gradio 6 API) app.theme = custom_theme app.css = CUSTOM_CSS app.js = DARK_MODE_JS # Header with gr.Row(): with gr.Column(): gr.HTML( """
    ✨

    DevRel Campaign Generator

    AI-Powered Content for Developer Relations

    System Online
    """ ) # Project Introduction - Clean, Modern Design with gr.Row(): with gr.Column(): gr.HTML( """
    šŸš€

    About This Tool

    Transform repositories into complete DevRel campaigns

    Claude AI Gradio 6

    Analyze any GitHub repository and automatically generate professional DevRel content including social threads, technical blogs, tutorials, conference talks, and hackathon challenges. Our intelligent pipeline understands code changes and detects breaking changes to create relevant, actionable content.

    🐦
    Social Threads
    Viral Twitter/X content with hooks
    šŸ“
    Technical Blogs
    SEO-optimized deep-dives
    šŸ“š
    Tutorials
    Step-by-step guides
    šŸŽ¤
    Talk Outlines
    Conference-ready decks
    šŸ’¼
    LinkedIn Posts
    Professional updates
    šŸ†
    Hackathon
    Challenge briefs
    ✨ Key Innovations
    ā—
    Code Diff Analysis
    Understands actual code changes
    ā—
    Breaking Change Detection
    Auto-generates migration guides
    ā—
    README Priority
    Documentation changes highlighted
    ā—
    Real-time Streaming
    Watch content generate live
    šŸ’”

    Tip: Configure your own API keys in āš™ļø Settings if the default quota is exhausted.

    """ ) # Main content - two columns with gr.Row(): # Left Column: Command Center + Pipeline Status (40%) with gr.Column(scale=2): # Command Center Card - Using Gradio Group for proper containment with gr.Group(elem_classes=["command-center-card"]): gr.HTML( """
    šŸ™

    Command Center

    Configure and launch your campaign

    """ ) repo_url = gr.Textbox( label="Repository URL", placeholder="https://github.com/username/repo", lines=1, ) time_range = gr.Dropdown( label="Analysis Period", choices=[ ("Last 1 week", "1week"), ("Last 1 month", "1month"), ("Last 3 months", "3months"), ("Last 6 months", "6months"), ("Last Year", "1year"), ], value="3months", ) # Settings Accordion for API Keys with gr.Accordion("āš™ļø Settings (API Keys)", open=False): gr.HTML( '
    šŸ’” Add your own API keys if the default quota is exhausted
    ' ) anthropic_key = gr.Textbox( label="Anthropic API Key (Claude)", placeholder="sk-ant-api03-...", type="password", lines=1, ) github_token = gr.Textbox( label="GitHub Token (Optional)", placeholder="ghp_...", type="password", lines=1, ) gr.HTML( '''

    šŸ”‘ Get Anthropic API Key →

    šŸ”‘ Get GitHub Token →

    ''' ) with gr.Row(): generate_btn = gr.Button( "✨ Generate Campaign", variant="primary", size="lg", ) reset_btn = gr.Button("šŸ”„ Reset", variant="secondary", size="lg") # Pipeline Status Card - Using Gradio Group for proper containment with gr.Group(elem_classes=["pipeline-status-card"]): gr.HTML( """
    šŸ“Š

    Pipeline Status

    Real-time generation progress

    """ ) # Progress Section progress_display = gr.HTML(value=get_progress_html(0, "idle")) # Event Log Section Header gr.HTML( """
    Event Log
    LIVE
    """ ) # Logs Display - scrollable area logs_display = gr.HTML( value=format_logs(), elem_classes=["terminal-content"], ) # Footer gr.HTML( """
    Powered by: Claude Ɨ Gradio 6
    """ ) # Right Column: Campaign Output (60%) with gr.Column(scale=3): # Campaign Output Card - Using Gradio Group for proper containment with gr.Group(elem_classes=["campaign-output-card"]): gr.HTML( """
    šŸ“¤

    Campaign Output

    Generated content for all platforms

    6 Formats
    """ ) metrics_display = gr.HTML(value="") with gr.Tabs() as tabs: with gr.Tab("🐦 Social", id="social"): twitter_output = gr.Markdown( label="Twitter Thread", value="Content not yet generated...", ) with gr.Tab("šŸ“ Blog", id="blog"): blog_output = gr.Markdown( label="Technical Blog Post", value="Content not yet generated...", ) with gr.Tab("šŸ“š Tutorial", id="tutorial"): tutorial_output = gr.Markdown( label="Getting Started Tutorial", value="Content not yet generated...", ) with gr.Tab("šŸŽ¤ Talk", id="talk"): talk_output = gr.Markdown( label="Conference Talk Outline", value="Content not yet generated...", ) with gr.Tab("šŸ’¼ LinkedIn", id="linkedin"): linkedin_output = gr.Markdown( label="LinkedIn Post", value="Content not yet generated...", ) with gr.Tab("šŸ† Hackathon", id="hackathon"): hackathon_output = gr.Markdown( label="Hackathon Challenge", value="Content not yet generated...", ) # Event handlers generate_btn.click( fn=generate_campaign, inputs=[repo_url, time_range, anthropic_key, github_token], outputs=[ logs_display, progress_display, twitter_output, blog_output, tutorial_output, talk_output, linkedin_output, hackathon_output, metrics_display, ], ) reset_btn.click( fn=reset_state, inputs=[], outputs=[ repo_url, time_range, logs_display, progress_display, twitter_output, blog_output, tutorial_output, talk_output, linkedin_output, hackathon_output, metrics_display, ], ) return app # Run the app if __name__ == "__main__": app = create_app() app.launch( server_name="0.0.0.0", server_port=7860, share=False, show_error=True, ssr_mode=False, # Disable experimental SSR to avoid potential issues )