chenglu's picture
Upload 4 files
7c4bfcc verified
"""
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 '<div style="color: var(--text-muted, #71717a); text-align: center; padding: 32px 20px;"><div style="font-size: 16px; margin-bottom: 8px;">Waiting for input...</div><span style="font-size: 13px;">Enter a GitHub repo URL to begin</span></div>'
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'<div style="margin: 6px 0; padding: 4px 0;">'
f'<span style="color: var(--text-muted, #71717a);">β€Ί</span> '
f'<span style="color: {color}; font-weight: 600;">[{log["agent"]}]</span> '
f'<span style="color: var(--text-secondary, #52525b);">{log["message"]}</span>'
f"</div>"
)
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"""
<div style="margin-bottom: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<span style="color: #8b5cf6; font-weight: 600; font-size: 14px;">Pipeline Status</span>
<span style="color: {text_color}; font-size: 12px; padding: 4px 12px; background: {bg_color}; border: 1px solid {text_color}33; border-radius: 16px; font-weight: 500; text-transform: capitalize;">{status}</span>
</div>
<div style="width: 100%; height: 10px; background: var(--bg-tertiary, #f5f5f5); border-radius: 5px; overflow: hidden; border: 1px solid var(--border-color, #e4e4e7);">
<div style="width: {progress}%; height: 100%; background: linear-gradient(90deg, #8b5cf6, #22c55e); transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1); border-radius: 5px;"></div>
</div>
</div>
"""
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'<li style="font-size: 12px; display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid var(--border-color, #e4e4e7);">'
f'<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 250px; color: var(--text-secondary, #52525b);" title="{c.get("message", "")}">{c.get("message", "")[:50]}</span>'
f'<a href="{c.get("url", "#")}" target="_blank" style="color: #22c55e; text-decoration: none; font-weight: 500;">{c.get("sha", "")[:7]}</a>'
f"</li>"
for c in profile.commits[:5]
]
)
commits_html = f"""
<div style="margin-top: 16px; padding: 14px; background: var(--bg-tertiary, #f5f5f5); border: 1px solid var(--border-color, #e4e4e7); border-radius: 12px; max-height: 140px; overflow-y: auto;">
<div style="color: var(--text-muted, #71717a); font-size: 11px; font-weight: 600; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px;">Recent Commits</div>
<ul style="margin: 0; padding: 0; list-style: none;">{commit_items}</ul>
</div>
"""
# Breaking changes section
breaking_html = ""
if profile.breaking_changes and len(profile.breaking_changes) > 0:
bc_items = "".join(
[
f'<li style="font-size: 12px; color: #f59e0b; padding: 4px 0;">'
f'<span title="{bc.get("message", "")}">[{bc.get("sha", "")}] {bc.get("message", "")[:40]}...</span>'
f"</li>"
for bc in profile.breaking_changes[:3]
]
)
breaking_html = f"""
<div style="margin-top: 16px; padding: 14px; border: 1px solid rgba(245, 158, 11, 0.3); border-radius: 12px; background: rgba(245, 158, 11, 0.08);">
<div style="color: #f59e0b; font-size: 12px; font-weight: 600; margin-bottom: 8px;">⚠️ Potential Breaking Changes</div>
<ul style="margin: 0; padding-left: 18px;">{bc_items}</ul>
</div>
"""
# README changes section
readme_html = ""
if profile.has_readme_changes:
readme_html = f"""
<div style="margin-top: 16px; padding: 14px; border: 1px solid rgba(34, 197, 94, 0.3); border-radius: 12px; background: rgba(34, 197, 94, 0.08);">
<div style="color: #22c55e; font-size: 12px; font-weight: 600;">πŸ“ README Documentation Updated</div>
<div style="color: #4ade80; font-size: 12px; margin-top: 6px;">High priority - documentation changes detected</div>
</div>
"""
features_list = ", ".join(profile.main_features[:3]) if profile.main_features else "N/A"
return f"""
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; font-size: 13px; margin-top: 16px; padding: 16px; background: var(--bg-tertiary, #f5f5f5); border-radius: 12px; border: 1px solid var(--border-color, #e4e4e7);">
<div>
<div style="color: var(--text-muted, #71717a); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Repository</div>
<div style="color: #8b5cf6; font-weight: 600; overflow: hidden; text-overflow: ellipsis;">{profile.name}</div>
</div>
<div>
<div style="color: var(--text-muted, #71717a); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Language</div>
<div style="color: #06b6d4; font-weight: 600;">{profile.primary_language}</div>
</div>
<div style="grid-column: span 2;">
<div style="color: var(--text-muted, #71717a); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Key Features</div>
<div style="color: var(--text-secondary, #52525b); font-size: 12px; line-height: 1.5;">{features_list}</div>
</div>
</div>
{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(
"""
<div style="display: flex; align-items: center; justify-content: space-between; padding: 20px 0; margin-bottom: 8px;">
<div style="display: flex; align-items: center; gap: 16px;">
<div style="width: 52px; height: 52px; background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(34, 197, 94, 0.1)); border: 1px solid rgba(139, 92, 246, 0.3); border-radius: 14px; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 30px rgba(139, 92, 246, 0.2);">
<span style="font-size: 26px;">✨</span>
</div>
<div>
<h1 style="margin: 0; background: linear-gradient(135deg, #8b5cf6, #22c55e); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-size: 28px; font-weight: 800; letter-spacing: -0.5px;">DevRel Campaign Generator</h1>
<p style="margin: 4px 0 0 0; color: var(--text-muted, #71717a); font-size: 14px;">AI-Powered Content for Developer Relations</p>
</div>
</div>
<div style="display: flex; align-items: center; gap: 10px; padding: 10px 18px; background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.2); border-radius: 24px;">
<div style="width: 10px; height: 10px; border-radius: 50%; background: #22c55e; animation: pulse 2s infinite; box-shadow: 0 0 12px rgba(34, 197, 94, 0.6);"></div>
<span style="font-size: 13px; font-weight: 600; color: #22c55e;">System Online</span>
</div>
</div>
<style>
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(0.9); }
}
</style>
"""
)
# Project Introduction - Clean, Modern Design
with gr.Row():
with gr.Column():
gr.HTML(
"""
<div style="background: var(--bg-card, #ffffff); border: 1px solid var(--border-color, #e4e4e7); border-radius: 20px; padding: 32px; margin-bottom: 24px; box-shadow: var(--shadow-md, 0 4px 6px -1px rgba(0, 0, 0, 0.1)); transition: all 0.3s ease;">
<!-- Header -->
<div style="display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 24px;">
<div style="display: flex; align-items: center; gap: 16px;">
<div style="width: 56px; height: 56px; background: linear-gradient(135deg, rgba(139, 92, 246, 0.2), rgba(34, 197, 94, 0.1)); border: 1px solid rgba(139, 92, 246, 0.3); border-radius: 16px; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 30px rgba(139, 92, 246, 0.15);">
<span style="font-size: 28px;">πŸš€</span>
</div>
<div>
<h2 style="margin: 0; font-size: 24px; font-weight: 700; color: var(--text-primary, #18181b);">About This Tool</h2>
<p style="margin: 6px 0 0 0; color: var(--text-muted, #71717a); font-size: 14px;">Transform repositories into complete DevRel campaigns</p>
</div>
</div>
<div style="display: flex; gap: 10px;">
<span style="background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(139, 92, 246, 0.05)); color: #8b5cf6; padding: 8px 16px; border-radius: 20px; font-size: 13px; font-weight: 600; border: 1px solid rgba(139, 92, 246, 0.25);">Claude AI</span>
<span style="background: linear-gradient(135deg, rgba(249, 115, 22, 0.15), rgba(249, 115, 22, 0.05)); color: #f97316; padding: 8px 16px; border-radius: 20px; font-size: 13px; font-weight: 600; border: 1px solid rgba(249, 115, 22, 0.25);">Gradio 6</span>
</div>
</div>
<!-- Description -->
<p style="margin: 0 0 24px 0; color: var(--text-secondary, #52525b); font-size: 16px; line-height: 1.7;">
Analyze any GitHub repository and automatically generate <strong style="color: #22c55e;">professional DevRel content</strong> including
social threads, technical blogs, tutorials, conference talks, and hackathon challenges. Our intelligent pipeline
understands code changes and <strong style="color: #8b5cf6;">detects breaking changes</strong> to create relevant, actionable content.
</p>
<!-- Feature Grid -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; margin-bottom: 24px;">
<div style="background: rgba(139, 92, 246, 0.08); border: 1px solid rgba(139, 92, 246, 0.15); border-radius: 14px; padding: 16px; transition: all 0.2s ease; cursor: default;">
<div style="font-size: 22px; margin-bottom: 8px;">🐦</div>
<div style="color: #8b5cf6; font-size: 13px; font-weight: 600; margin-bottom: 4px;">Social Threads</div>
<div style="color: var(--text-muted, #71717a); font-size: 12px; line-height: 1.5;">Viral Twitter/X content with hooks</div>
</div>
<div style="background: rgba(34, 197, 94, 0.08); border: 1px solid rgba(34, 197, 94, 0.15); border-radius: 14px; padding: 16px; transition: all 0.2s ease;">
<div style="font-size: 22px; margin-bottom: 8px;">πŸ“</div>
<div style="color: #22c55e; font-size: 13px; font-weight: 600; margin-bottom: 4px;">Technical Blogs</div>
<div style="color: var(--text-muted, #71717a); font-size: 12px; line-height: 1.5;">SEO-optimized deep-dives</div>
</div>
<div style="background: rgba(249, 115, 22, 0.08); border: 1px solid rgba(249, 115, 22, 0.15); border-radius: 14px; padding: 16px; transition: all 0.2s ease;">
<div style="font-size: 22px; margin-bottom: 8px;">πŸ“š</div>
<div style="color: #f97316; font-size: 13px; font-weight: 600; margin-bottom: 4px;">Tutorials</div>
<div style="color: var(--text-muted, #71717a); font-size: 12px; line-height: 1.5;">Step-by-step guides</div>
</div>
<div style="background: rgba(6, 182, 212, 0.08); border: 1px solid rgba(6, 182, 212, 0.15); border-radius: 14px; padding: 16px; transition: all 0.2s ease;">
<div style="font-size: 22px; margin-bottom: 8px;">🎀</div>
<div style="color: #06b6d4; font-size: 13px; font-weight: 600; margin-bottom: 4px;">Talk Outlines</div>
<div style="color: var(--text-muted, #71717a); font-size: 12px; line-height: 1.5;">Conference-ready decks</div>
</div>
<div style="background: rgba(59, 130, 246, 0.08); border: 1px solid rgba(59, 130, 246, 0.15); border-radius: 14px; padding: 16px; transition: all 0.2s ease;">
<div style="font-size: 22px; margin-bottom: 8px;">πŸ’Ό</div>
<div style="color: #3b82f6; font-size: 13px; font-weight: 600; margin-bottom: 4px;">LinkedIn Posts</div>
<div style="color: var(--text-muted, #71717a); font-size: 12px; line-height: 1.5;">Professional updates</div>
</div>
<div style="background: rgba(236, 72, 153, 0.08); border: 1px solid rgba(236, 72, 153, 0.15); border-radius: 14px; padding: 16px; transition: all 0.2s ease;">
<div style="font-size: 22px; margin-bottom: 8px;">πŸ†</div>
<div style="color: #ec4899; font-size: 13px; font-weight: 600; margin-bottom: 4px;">Hackathon</div>
<div style="color: var(--text-muted, #71717a); font-size: 12px; line-height: 1.5;">Challenge briefs</div>
</div>
</div>
<!-- Key Innovations -->
<div style="background: var(--bg-tertiary, #f5f5f5); border: 1px solid var(--border-color, #e4e4e7); border-radius: 16px; padding: 20px; margin-bottom: 20px;">
<div style="color: #8b5cf6; font-size: 14px; font-weight: 700; margin-bottom: 16px; display: flex; align-items: center; gap: 8px;">
<span>✨</span> Key Innovations
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px;">
<div style="display: flex; align-items: flex-start; gap: 12px;">
<span style="color: #22c55e; font-size: 16px;">●</span>
<div>
<div style="color: var(--text-primary, #18181b); font-weight: 600; font-size: 14px;">Code Diff Analysis</div>
<div style="color: var(--text-muted, #71717a); font-size: 13px;">Understands actual code changes</div>
</div>
</div>
<div style="display: flex; align-items: flex-start; gap: 12px;">
<span style="color: #22c55e; font-size: 16px;">●</span>
<div>
<div style="color: var(--text-primary, #18181b); font-weight: 600; font-size: 14px;">Breaking Change Detection</div>
<div style="color: var(--text-muted, #71717a); font-size: 13px;">Auto-generates migration guides</div>
</div>
</div>
<div style="display: flex; align-items: flex-start; gap: 12px;">
<span style="color: #22c55e; font-size: 16px;">●</span>
<div>
<div style="color: var(--text-primary, #18181b); font-weight: 600; font-size: 14px;">README Priority</div>
<div style="color: var(--text-muted, #71717a); font-size: 13px;">Documentation changes highlighted</div>
</div>
</div>
<div style="display: flex; align-items: flex-start; gap: 12px;">
<span style="color: #22c55e; font-size: 16px;">●</span>
<div>
<div style="color: var(--text-primary, #18181b); font-weight: 600; font-size: 14px;">Real-time Streaming</div>
<div style="color: var(--text-muted, #71717a); font-size: 13px;">Watch content generate live</div>
</div>
</div>
</div>
</div>
<!-- Tip -->
<div style="display: flex; align-items: center; gap: 10px; padding-top: 16px; border-top: 1px solid var(--border-color, #e4e4e7);">
<span style="font-size: 18px;">πŸ’‘</span>
<p style="margin: 0; color: var(--text-muted, #71717a); font-size: 14px;">
<strong style="color: var(--text-secondary, #52525b);">Tip:</strong> Configure your own API keys in
<span style="color: #f97316; font-weight: 600;">βš™οΈ Settings</span> if the default quota is exhausted.
</p>
</div>
</div>
"""
)
# 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(
"""
<div style="display: flex; align-items: center; gap: 14px; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid var(--border-color, #e4e4e7);">
<div style="width: 48px; height: 48px; background: linear-gradient(135deg, rgba(139, 92, 246, 0.2), rgba(139, 92, 246, 0.05)); border: 1px solid rgba(139, 92, 246, 0.3); border-radius: 14px; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 20px rgba(139, 92, 246, 0.15);">
<span style="font-size: 24px;">πŸ™</span>
</div>
<div>
<h3 style="margin: 0; font-size: 20px; font-weight: 700; color: var(--text-primary, #18181b);">Command Center</h3>
<p style="margin: 4px 0 0 0; color: var(--text-muted, #71717a); font-size: 13px;">Configure and launch your campaign</p>
</div>
</div>
"""
)
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(
'<div style="padding: 12px 0; color: var(--text-muted, #71717a); font-size: 13px;">πŸ’‘ Add your own API keys if the default quota is exhausted</div>'
)
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(
'''<div style="padding: 12px 0; color: var(--text-muted, #71717a); font-size: 13px;">
<p style="margin: 0 0 8px 0;">πŸ”‘ <a href="https://console.anthropic.com" target="_blank" style="color: #8b5cf6; text-decoration: none; font-weight: 500;">Get Anthropic API Key β†’</a></p>
<p style="margin: 0;">πŸ”‘ <a href="https://github.com/settings/tokens" target="_blank" style="color: #22c55e; text-decoration: none; font-weight: 500;">Get GitHub Token β†’</a></p>
</div>'''
)
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(
"""
<div style="display: flex; align-items: center; gap: 14px; margin-bottom: 16px; padding-bottom: 14px; border-bottom: 1px solid var(--border-color, #e4e4e7);">
<div style="width: 44px; height: 44px; background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(34, 197, 94, 0.05)); border: 1px solid rgba(34, 197, 94, 0.3); border-radius: 12px; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 20px rgba(34, 197, 94, 0.15);">
<span style="font-size: 22px;">πŸ“Š</span>
</div>
<div>
<h3 style="margin: 0; font-size: 18px; font-weight: 700; color: var(--text-primary, #18181b);">Pipeline Status</h3>
<p style="margin: 4px 0 0 0; color: var(--text-muted, #71717a); font-size: 12px;">Real-time generation progress</p>
</div>
</div>
"""
)
# Progress Section
progress_display = gr.HTML(value=get_progress_html(0, "idle"))
# Event Log Section Header
gr.HTML(
"""
<div style="margin-top: 16px; border-top: 1px solid var(--border-color, #e4e4e7); padding-top: 16px;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
<div style="display: flex; align-items: center; gap: 12px;">
<div style="display: flex; gap: 6px;">
<div style="width: 12px; height: 12px; border-radius: 50%; background: linear-gradient(135deg, #ef4444, #dc2626); box-shadow: 0 0 6px rgba(239, 68, 68, 0.4);"></div>
<div style="width: 12px; height: 12px; border-radius: 50%; background: linear-gradient(135deg, #eab308, #ca8a04); box-shadow: 0 0 6px rgba(234, 179, 8, 0.4);"></div>
<div style="width: 12px; height: 12px; border-radius: 50%; background: linear-gradient(135deg, #22c55e, #16a34a); box-shadow: 0 0 6px rgba(34, 197, 94, 0.4);"></div>
</div>
<span style="font-weight: 600; color: var(--text-primary, #18181b); font-size: 14px;">Event Log</span>
</div>
<div style="display: flex; align-items: center; gap: 6px; padding: 4px 12px; background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.25); border-radius: 16px;">
<div style="width: 6px; height: 6px; border-radius: 50%; background: #22c55e; animation: pulse 2s infinite; box-shadow: 0 0 8px rgba(34, 197, 94, 0.6);"></div>
<span style="color: #22c55e; font-size: 11px; font-weight: 600;">LIVE</span>
</div>
</div>
</div>
"""
)
# Logs Display - scrollable area
logs_display = gr.HTML(
value=format_logs(),
elem_classes=["terminal-content"],
)
# Footer
gr.HTML(
"""
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border-color, #e4e4e7); font-size: 12px;">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="color: var(--text-muted, #71717a);">Powered by:</span>
<span style="background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(139, 92, 246, 0.05)); color: #8b5cf6; padding: 4px 12px; border-radius: 14px; font-weight: 600; font-size: 11px; border: 1px solid rgba(139, 92, 246, 0.2);">Claude</span>
<span style="color: var(--text-muted, #71717a);">Γ—</span>
<span style="background: linear-gradient(135deg, rgba(249, 115, 22, 0.15), rgba(249, 115, 22, 0.05)); color: #f97316; padding: 4px 12px; border-radius: 14px; font-weight: 600; font-size: 11px; border: 1px solid rgba(249, 115, 22, 0.2);">Gradio 6</span>
</div>
</div>
"""
)
# 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(
"""
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid var(--border-color, #e4e4e7);">
<div style="display: flex; align-items: center; gap: 14px;">
<div style="width: 48px; height: 48px; background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(6, 182, 212, 0.1)); border: 1px solid rgba(34, 197, 94, 0.3); border-radius: 14px; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 20px rgba(34, 197, 94, 0.15);">
<span style="font-size: 24px;">πŸ“€</span>
</div>
<div>
<h3 style="margin: 0; font-size: 20px; font-weight: 700; color: var(--text-primary, #18181b);">Campaign Output</h3>
<p style="margin: 4px 0 0 0; color: var(--text-muted, #71717a); font-size: 13px;">Generated content for all platforms</p>
</div>
</div>
<div style="display: flex; gap: 10px;">
<span style="background: rgba(34, 197, 94, 0.1); color: #22c55e; padding: 6px 14px; border-radius: 16px; font-size: 12px; font-weight: 600; border: 1px solid rgba(34, 197, 94, 0.2);">6 Formats</span>
</div>
</div>
"""
)
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
)