# app.py - Improved version
import os
import gradio as gr
from github_ai_agent import GitHubAIAgent
import markdown
from typing import Tuple, Dict, Any, List
import google.generativeai as genai
def create_web_interface(agent: GitHubAIAgent) -> gr.Blocks:
"""Create enhanced Gradio web interface for the GitHub AI Agent with improved UI"""
# Custom CSS for better styling
custom_css = """
.gradio-container {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* Repository Card */
.repository-card {
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
padding: 20px;
margin-bottom: 20px;
background-color: #f9f9f9;
transition: transform 0.2s, box-shadow 0.2s;
}
.repository-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 10px rgba(0,0,0,0.1);
}
/* Authentication Badge */
.auth-badge {
display: inline-block;
padding: 5px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
margin-bottom: 10px;
}
.auth-authenticated {
background-color: #d4edda;
color: #155724;
}
.auth-unauthenticated {
background-color: #f8d7da;
color: #721c24;
}
/* Metric Cards */
.metric-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 15px;
}
.metric-card {
flex: 1 1 100px;
padding: 15px;
border-radius: 8px;
background-color: #ffffff;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
text-align: center;
}
.metric-value {
font-size: 24px;
font-weight: bold;
margin: 10px 0;
color: #2980b9;
}
.metric-label {
font-size: 12px;
color: #7f8c8d;
}
/* Progress Bar */
.progress-container {
width: 100%;
background-color: #e0e0e0;
border-radius: 5px;
margin: 10px 0;
}
.progress-bar {
height: 5px;
border-radius: 5px;
background-color: #3498db;
}
/* Theme Toggle Switch */
.theme-switch-container {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.theme-switch {
position: relative;
display: inline-block;
width: 60px;
height: 30px;
margin-left: 10px;
}
.theme-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 22px;
width: 22px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #2196F3;
}
input:checked + .slider:before {
transform: translateX(30px);
}
/* Dark Theme Styles */
.dark-theme {
background-color: #2c3e50;
color: #ecf0f1;
}
.dark-theme .metric-card {
background-color: #34495e;
color: #ecf0f1;
}
.dark-theme .metric-value {
color: #3498db;
}
.dark-theme .repository-card {
background-color: #34495e;
color: #ecf0f1;
}
/* Loading Animation */
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
.loading {
animation: pulse 1.5s infinite;
border-radius: 8px;
background-color: #f0f0f0;
padding: 20px;
text-align: center;
}
/* Tooltip */
.tooltip {
position: relative;
display: inline-block;
cursor: help;
}
.tooltip .tooltiptext {
visibility: hidden;
width: 200px;
background-color: #555;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -100px;
opacity: 0;
transition: opacity 0.3s;
}
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
/* Improved tabs */
.tab-nav {
border-bottom: 2px solid #e0e0e0;
margin-bottom: 20px;
}
.tab-button {
padding: 10px 20px;
background: transparent;
border: none;
cursor: pointer;
font-weight: bold;
color: #7f8c8d;
position: relative;
}
.tab-button.active {
color: #3498db;
}
.tab-button.active:after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background-color: #3498db;
}
"""
def load_repository(repo_url: str, gemini_key: str, use_token: bool = False, github_token: str = "") -> Tuple[str, str, float]:
"""Load a repository (Gradio wrapper) with progress tracking"""
try:
if use_token and not github_token:
return "Error: GitHub token is required for private repositories.", "", 0.0
# Set API keys
actual_github_token = github_token if use_token else ""
agent.set_api_keys(gemini_key, actual_github_token)
if not agent.config.gemini_api_key:
return "Error: Gemini API key is required.", "", 0.0
# Progress indicators (for UI feedback)
progress_value = 0.1
progress_message = "Initializing repository..."
yield progress_message, "", progress_value
# Load repository
progress_value = 0.2
progress_message = f"Loading repository: {repo_url}"
yield progress_message, "", progress_value
result = agent.load_repository(repo_url)
if not result['success']:
return f"Error: {result['message']}", "", 0.0
# Indicate progress
progress_value = 0.6
progress_message = "Repository loaded. Building knowledge base..."
yield progress_message, "", progress_value
# Get repository insights
insights = agent.get_repository_insights()
# Create HTML report
analysis_content = insights['analysis']['analysis'].replace('\n', '
') if insights['success'] else "No analysis available."
# Calculate badge text and class
badge_text = "Private Repository" if use_token else "Public Repository"
badge_class = "auth-authenticated" if use_token else "auth-unauthenticated"
# Format date properly
created_at = result['repo_data'].get('created_at', 'Unknown')
if hasattr(created_at, 'strftime'):
created_at = created_at.strftime('%Y-%m-%d')
updated_at = result['repo_data'].get('updated_at', 'Unknown')
if hasattr(updated_at, 'strftime'):
updated_at = updated_at.strftime('%Y-%m-%d')
# Build HTML with improved styling
html_report = f"""
{badge_text}
Repository: {result['repo_data']['full_name']}
Description: {result['repo_data']['description'] or "No description available"}
Stars
{result['repo_data']['stars']}
Forks
{result['repo_data']['forks']}
Language
{result['repo_data']['language'] or "N/A"}
Files
{result['file_count']}
Contributors
{result['contributor_count']}
Created: {created_at} | Last Updated: {updated_at}
License: {result['repo_data'].get('license', 'None')}
Topics
{', '.join(result['repo_data'].get('topics', ['None'])) if result['repo_data'].get('topics') else 'None'}
Repository Analysis
{analysis_content}
"""
# Complete progress
progress_value = 1.0
progress_message = "Analysis complete!"
yield progress_message, markdown.markdown(html_report), progress_value
return progress_message, markdown.markdown(html_report), progress_value
except Exception as e:
return f"Error: {str(e)}", "", 0.0
def ask_question(question: str) -> str:
"""Ask a question about the repository (Gradio wrapper)"""
if not question:
return "Please enter a question."
if not agent.repository_loaded:
return "Please load a repository first."
try:
result = agent.answer_query(question)
if result['success']:
# Format the answer with improved styling
answer_content = result['answer'].replace('\n', '
')
html_response = f"""
Answer
{answer_content}
"""
if result['relevant_files']:
html_response += f"""
Relevant Files:
"""
for file in result['relevant_files']:
html_response += f"{file}
"
html_response += "
"
html_response += "
"
return markdown.markdown(html_response)
else:
return f"Error: {result['message']}"
except Exception as e:
return f"Error: {str(e)}"
def analyze_code(file_path: str, code_snippet: str) -> str:
"""Analyze code (Gradio wrapper)"""
if not file_path and not code_snippet:
return "Please provide either a file path or a code snippet."
try:
result = agent.analyze_code(file_path, code_snippet)
if result['success']:
# Format the analysis with improved styling
analysis_content = result['analysis'].replace('\n', '
')
html_response = f"""
Code Analysis
{"File" if file_path else "Code Snippet"}: {file_path if file_path else "Provided snippet"}
{analysis_content}
"""
return markdown.markdown(html_response)
else:
return f"Error: {result['message']}"
except Exception as e:
return f"Error: {str(e)}"
def find_collaborators(requirements: str) -> str:
"""Find potential collaborators (Gradio wrapper)"""
if not requirements:
return "Please provide requirements for collaborators."
if not agent.repository_loaded:
return "Please load a repository first."
try:
result = agent.find_collaborators(requirements)
if result['success']:
html_response = "Potential Collaborators
"
for collaborator in result['collaborators']:
confidence = collaborator.get('confidence', 0)
confidence_percent = f"{confidence:.1%}" if isinstance(confidence, float) else "N/A"
# Apply color based on confidence
if isinstance(confidence, float):
if confidence >= 0.8:
confidence_color = "#27ae60" # Green for high confidence
elif confidence >= 0.5:
confidence_color = "#f39c12" # Yellow for medium confidence
else:
confidence_color = "#e74c3c" # Red for low confidence
else:
confidence_color = "#7f8c8d" # Gray for N/A
html_response += f"""
{collaborator['login']}
Confidence: {confidence_percent}
Reasons:
"""
for reason in collaborator.get('reasons', []):
html_response += f"- {reason}
"
html_response += """
"""
return markdown.markdown(html_response)
else:
return f"Error: {result['message']}"
except Exception as e:
return f"Error: {str(e)}"
def get_insights() -> str:
"""Get repository insights (Gradio wrapper)"""
if not agent.repository_loaded:
return "Please load a repository first."
try:
result = agent.get_repository_insights()
if result['success']:
insights = result['insights']
html_response = "Repository Insights
"
if 'basic_stats' in insights:
html_response += """
Basic Statistics
"""
html_response += f"- Repository: {insights['basic_stats'].get('name', 'N/A')}
"
html_response += f"- Description: {insights['basic_stats'].get('description', 'N/A')}
"
html_response += f"- Stars: {insights['basic_stats'].get('stars', 0)}
"
html_response += f"- Forks: {insights['basic_stats'].get('forks', 0)}
"
html_response += f"- Age: {insights['basic_stats'].get('age_days', 0)} days
"
html_response += f"- Primary Language: {insights['basic_stats'].get('primary_language', 'N/A')}
"
if 'topics' in insights['basic_stats']:
html_response += f"- Topics: {', '.join(insights['basic_stats']['topics'])}
"
html_response += """
"""
if 'activity' in insights:
html_response += """
Activity
"""
# Add metrics
metrics = [
("Total Commits", insights['activity'].get('total_commits', 0)),
("Days Span", insights['activity'].get('days_span', 0)),
("Commits Per Day", insights['activity'].get('commits_per_day', 0))
]
for label, value in metrics:
html_response += f"""
"""
html_response += """
"""
if 'first_commit' in insights['activity']:
first_commit = insights['activity']['first_commit']
if hasattr(first_commit, 'strftime'):
first_commit = first_commit.strftime('%Y-%m-%d')
html_response += f"- First Commit: {first_commit}
"
if 'last_commit' in insights['activity']:
last_commit = insights['activity']['last_commit']
if hasattr(last_commit, 'strftime'):
last_commit = last_commit.strftime('%Y-%m-%d')
html_response += f"- Last Commit: {last_commit}
"
if 'monthly_activity' in insights['activity']:
html_response += "- Monthly Activity:
"
for month_data in insights['activity']['monthly_activity'][:5]:
html_response += f"- {month_data['month']}: {month_data['commits']} commits
"
html_response += "
"
html_response += """
"""
if 'contributors' in insights:
html_response += """
Contributors
"""
# Add metrics
metrics = [
("Total Contributors", insights['contributors'].get('total_contributors', 0)),
("Bus Factor", insights['contributors'].get('bus_factor', 0))
]
for label, value in metrics:
html_response += f"""
"""
html_response += """
"""
if 'top_contributors' in insights['contributors']:
html_response += "
Top Contributors
"
for contributor in insights['contributors']['top_contributors'][:5]:
html_response += f"- {contributor['login']}: {contributor['contributions']} contributions"
if contributor['top_files']:
html_response += f" (Top files: {', '.join(contributor['top_files'][:3])})"
html_response += "
"
html_response += "
"
html_response += """
"""
if 'code' in insights:
html_response += """
Code
"""
if 'central_files' in insights['code']:
html_response += "
Central Files
"
for file in insights['code']['central_files'][:5]:
html_response += f"- {file['filename']}: {file['connections']} connections
"
html_response += "
"
if 'frequently_modified_files' in insights['code']:
html_response += "
Frequently Modified Files
"
for file in insights['code']['frequently_modified_files'][:5]:
html_response += f"- {file['filename']}: {file['modifications']} modifications
"
html_response += "
"
if 'file_types' in insights['code']:
html_response += "
File Types
"
for file_type in insights['code']['file_types'][:5]:
ext = file_type['extension'] or 'No extension'
html_response += f"- {ext}: {file_type['count']} files
"
html_response += "
"
html_response += """
"""
if 'issues' in insights:
html_response += """
Issues
"""
# Add metrics
metrics = [
("Total Issues", insights['issues'].get('total_issues', 0)),
("Open Issues", insights['issues'].get('open_issues', 0)),
("Closed Issues", insights['issues'].get('closed_issues', 0)),
("Resolution Rate", f"{insights['issues'].get('resolution_rate', 0) * 100:.1f}%")
]
for label, value in metrics:
html_response += f"""
"""
html_response += """
"""
if 'avg_days_to_close' in insights['issues']:
html_response += f"
Average Days to Close: {insights['issues']['avg_days_to_close']}
"
if 'top_labels' in insights['issues']:
html_response += "
Top Labels
"
for label in insights['issues']['top_labels']:
html_response += f"- {label['label']}: {label['count']} issues
"
html_response += "
"
html_response += """
"""
return markdown.markdown(html_response)
else:
return f"Error: {result['message']}"
except Exception as e:
return f"Error: {str(e)}"
def show_visualizations() -> Tuple[str, str, str, str]:
"""Show repository visualizations (Gradio wrapper)"""
if not agent.repository_loaded:
return "No repository loaded.", "No repository loaded.","No repository loaded.","No repository loaded."
try:
result = agent.get_visualizations()
if result['success']:
visualizations = result['visualizations']
repo_graph_html = "No repository graph available.
"
activity_chart_html = "No activity chart available.
"
contributor_network_html = "No contributor network available.
"
dependency_graph_html = "No dependency graph available.
"
for viz_type, path in visualizations.items():
if os.path.exists(path):
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
if viz_type == "repository_graph":
repo_graph_html = content
elif viz_type == "activity_chart":
activity_chart_html = content
elif viz_type == "contributor_network":
contributor_network_html = content
elif viz_type == "dependency_graph":
dependency_graph_html = content
return repo_graph_html, activity_chart_html, contributor_network_html, dependency_graph_html
else:
error_msg = f"Error: {result['message']}
"
return error_msg, error_msg, error_msg, error_msg
except Exception as e:
error_msg = f""
return error_msg, error_msg, error_msg, error_msg
# Helper function to display API key tooltip
def api_key_tooltip():
return """
ⓘ
Your API key is used securely and not stored permanently.
"""
# Create the Gradio interface
with gr.Blocks(css=custom_css, title="GitHub Repository AI Agent") as interface:
gr.HTML("""
GitHub Repository AI Agent
Analyze repositories, understand code, identify collaborators, and extract insights with Gemini AI
""")
# Theme toggle
with gr.Row():
with gr.Column(scale=1):
pass # Empty column for spacing
with gr.Column(scale=10):
theme_switch = gr.Checkbox(label="Dark Mode", value=False)
with gr.Column(scale=1):
pass # Empty column for spacing
with gr.Tab("Repository"):
with gr.Row():
with gr.Column():
gr.HTML("Repository Configuration
")
repo_url = gr.Textbox(label="Repository URL", placeholder="https://github.com/username/repository")
gemini_api_key = gr.Textbox(label="Gemini API Key", type="password")
# Private repository toggle
repo_type = gr.Radio(
["Public Repository", "Private Repository"],
label="Repository Type",
value="Public Repository"
)
# GitHub token input (initially hidden)
with gr.Group(visible=False) as private_repo_group:
github_token = gr.Textbox(
label="GitHub Personal Access Token",
type="password",
placeholder="Enter your GitHub token for private repository access"
)
gr.HTML("⚠️ Your token is used securely and not stored permanently.
")
load_btn = gr.Button("Load Repository", variant="primary")
# Loading status
load_status = gr.Textbox(label="Status", visible=True)
progress = gr.Slider(
minimum=0,
maximum=1,
value=0,
label="Loading Progress",
visible=True
)
repo_info = gr.HTML(label="Repository Information")
# Show/hide GitHub token based on repository type selection
repo_type.change(
fn=lambda x: gr.Group(visible=(x == "Private Repository")),
inputs=repo_type,
outputs=private_repo_group
)
# Load repository with progress updates
load_btn.click(
load_repository,
inputs=[
repo_url,
gemini_api_key,
# Pass whether to use token based on repo type
repo_type.change(lambda x: x == "Private Repository", repo_type),
github_token
],
outputs=[load_status, repo_info, progress]
)
with gr.Tab("Ask Questions"):
with gr.Row():
question = gr.Textbox(
label="Question",
placeholder="Ask about the repository (e.g., What's the main purpose of this repository?)",
lines=3
)
ask_btn = gr.Button("Ask", variant="primary")
answer = gr.HTML(label="Answer")
ask_btn.click(
ask_question,
inputs=question,
outputs=answer
)
with gr.Tab("Code Analysis"):
with gr.Row():
with gr.Column():
file_path = gr.Textbox(label="File Path", placeholder="path/to/file.py")
code_snippet = gr.Code(label="Or paste code snippet", language="python", lines=10)
analyze_btn = gr.Button("Analyze", variant="primary")
code_analysis = gr.HTML(label="Analysis")
analyze_btn.click(
analyze_code,
inputs=[file_path, code_snippet],
outputs=code_analysis
)
with gr.Tab("Find Collaborators"):
with gr.Row():
requirements = gr.Textbox(
label="Requirements",
placeholder="Describe what you're looking for in a collaborator (e.g., experience with machine learning, React development, etc.)",
lines=5
)
find_btn = gr.Button("Find Collaborators", variant="primary")
collaborators = gr.HTML(label="Potential Collaborators")
find_btn.click(
find_collaborators,
inputs=requirements,
outputs=collaborators
)
with gr.Tab("Insights"):
insights_btn = gr.Button("Get Repository Insights", variant="primary")
insights_html = gr.HTML(label="Repository Insights")
insights_btn.click(
get_insights,
inputs=[],
outputs=insights_html
)
with gr.Tab("Visualizations"):
viz_btn = gr.Button("Show Visualizations", variant="primary")
# Create tabs for visualizations
viz_tabs = gr.Tabs([
gr.TabItem("Repository Graph", gr.HTML(label="Repository Graph")),
gr.TabItem("Activity Chart", gr.HTML(label="Activity Chart")),
gr.TabItem("Contributor Network", gr.HTML(label="Contributor Network")),
gr.TabItem("Dependency Graph", gr.HTML(label="Dependency Graph"))
])
viz_btn.click(
show_visualizations,
inputs=[],
outputs=[
viz_tabs.select(0),
viz_tabs.select(1),
viz_tabs.select(2),
viz_tabs.select(3)
]
)
# Add theme toggle functionality
theme_switch.change(
fn=lambda x: gr.Blocks(css=custom_css + (".gradio-container {background-color: #2c3e50; color: #ecf0f1;}" if x else "")),
inputs=theme_switch,
outputs=interface
)
return interface
def main():
"""Main function for running the GitHub AI Agent"""
agent = GitHubAIAgent()
interface = create_web_interface(agent)
interface.launch(show_api=False, server_name="0.0.0.0", server_port=int(os.environ.get('PORT', 7860)))
if __name__ == "__main__":
main()