# app.py
import logging
import sys
import os
import json
from constants import FONT_FAMILY_SHORT
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
logger = logging.getLogger(__name__)
# Force rebuild to fetch latest data from GitHub repo
logger.info("Starting OpenHands Index application")
# Setup mock data before anything else
try:
from setup_data import setup_mock_data, start_background_refresh, CACHE_TTL_SECONDS
setup_mock_data()
logger.info("Data setup completed successfully")
# Start background refresh scheduler (checks for new data every hour)
start_background_refresh()
logger.info(f"Background refresh scheduler started (interval: {CACHE_TTL_SECONDS}s)")
except Exception as e:
logger.error(f"Error during data setup: {e}", exc_info=True)
logger.warning("Continuing with app startup despite error")
import gradio as gr
import urllib.parse
from huggingface_hub import HfApi
from config import LEADERBOARD_PATH, LOCAL_DEBUG
from content import css
from main_page import build_page as build_main_page
from bug_fixing import build_page as build_bug_fixing_page
from app_creation import build_page as build_app_creation_page
from frontend_development import build_page as build_frontend_page
from test_generation import build_page as build_test_generation_page
from information_gathering import build_page as build_information_gathering_page
from alternative_agents_page import build_page as build_alternative_agents_page
from about import build_page as build_about_page
logger.info(f"All modules imported (LOCAL_DEBUG={LOCAL_DEBUG})")
api = HfApi()
LOGO_PATH = "assets/logo.svg"
# PostHog analytics (client-side)
POSTHOG_API_KEY = os.getenv("POSTHOG_API_KEY", "phc_ERBPfEE0gwNgkOBsxbHr1wh9mBsYcsw4zSLtvdA9RFg")
posthog_script = f"""
"""
# JavaScripts
scroll_script = """
"""
redirect_script = """
"""
# JavaScript to fix navigation links to use relative paths (avoids domain mismatch when behind proxy)
fix_nav_links_script = """
"""
tooltip_script = """
"""
# JavaScript to handle dark mode for Plotly charts and OpenHands logos
dark_mode_script = """
"""
# --- Theme Definition ---
# Color scheme aligned with OpenHands brand (from openhands-ui/tokens.css)
# Primary: Yellow (#FFE165), Neutral: Grey scale, Accents: Green (#BCFF8C), Red (#FF684E)
theme = gr.themes.Base(
# Primary hue - Yellow (OpenHands brand color)
primary_hue=gr.themes.Color(
c50="#FFFCF0", c100="#FFF3C0", c200="#FFEEAA", c300="#FFEA92", c400="#FFE57B",
c500="#FFE165", c600="#DCC257", c700="#BBA54A", c800="#99873D", c900="#76682F", c950="#534921"
),
# Secondary hue - Green accent (from OpenHands palette)
secondary_hue=gr.themes.Color(
c50="#F8FFF4", c100="#E4FFD0", c200="#DAFFBF", c300="#CFFFAD", c400="#C6FF9D",
c500="#BCFF8C", c600="#A2DC79", c700="#8ABB67", c800="#719954", c900="#577641", c950="#3D532E"
),
# Neutral hue - Grey scale (OpenHands dark mode colors)
neutral_hue=gr.themes.Color(
c50="#F7F8FB", c100="#EBEDF3", c200="#D4D8E7", c300="#B1B9D3", c400="#82889B",
c500="#525662", c600="#3A3C45", c700="#2F3137", c800="#222328", c900="#18191C", c950="#0D0D0F"
),
font=[FONT_FAMILY_SHORT, 'sans-serif'],
font_mono=['monospace'],
).set(
body_text_color='*neutral_950',
body_text_color_subdued='*neutral_700',
body_text_color_subdued_dark='*neutral_300',
body_text_color_dark='*neutral_50',
background_fill_primary='*neutral_50',
background_fill_primary_dark='*neutral_900',
background_fill_secondary='*neutral_100',
background_fill_secondary_dark='*neutral_800',
border_color_accent='*primary_500',
border_color_accent_subdued='*neutral_300',
border_color_accent_subdued_dark='*neutral_600',
color_accent='*primary_500',
color_accent_soft='*neutral_200',
color_accent_soft_dark='*neutral_800',
link_text_color='*neutral_700',
link_text_color_dark='*neutral_300',
link_text_color_active_dark='*primary_500',
link_text_color_hover_dark='*primary_400',
link_text_color_visited_dark='*neutral_400',
table_even_background_fill='*neutral_100',
table_even_background_fill_dark='*neutral_800',
button_primary_background_fill='*primary_500',
button_primary_background_fill_dark='*primary_500',
button_primary_background_fill_hover='*primary_400',
button_primary_background_fill_hover_dark='*primary_400',
button_secondary_background_fill='*secondary_500',
button_secondary_background_fill_dark='*secondary_600',
button_secondary_text_color='*neutral_900',
button_secondary_text_color_dark='*neutral_900',
block_title_text_color='*neutral_900',
button_primary_text_color='*neutral_900',
block_title_text_color_dark='*neutral_50',
button_primary_text_color_dark='*neutral_900',
block_border_color='*neutral_300',
block_border_color_dark='*neutral_700',
block_background_fill_dark='*neutral_900',
block_background_fill='*neutral_50',
checkbox_label_text_color='*neutral_900',
checkbox_label_background_fill='*neutral_200',
checkbox_label_background_fill_dark='*neutral_700',
checkbox_background_color_selected='*primary_500',
checkbox_background_color_selected_dark='*primary_500',
)
try:
with open(LOGO_PATH, "r") as f:
svg_content = f.read()
encoded_svg = urllib.parse.quote(svg_content)
home_icon_data_uri = f"data:image/svg+xml,{encoded_svg}"
except FileNotFoundError:
logger.warning(f"Home icon file not found at {LOGO_PATH}")
home_icon_data_uri = "none"
# Load dark mode logo (PNG)
LOGO_DARK_PATH = "assets/logo-dark.png"
try:
import base64
with open(LOGO_DARK_PATH, "rb") as f:
dark_logo_content = f.read()
encoded_dark_logo = base64.b64encode(dark_logo_content).decode('utf-8')
home_icon_dark_data_uri = f"data:image/png;base64,{encoded_dark_logo}"
except FileNotFoundError:
logger.warning(f"Dark mode logo file not found at {LOGO_DARK_PATH}")
home_icon_dark_data_uri = home_icon_data_uri # Fallback to light logo
# --- This is the final CSS ---
final_css = css + f"""
/* --- Find the "Home" button and replace its text with an icon --- */
.nav-holder nav a[href$="/"] {{
display: none !important;
}}
.nav-holder nav a[href*="/home"] {{
grid-row: 1 !important;
grid-column: 1 !important;
justify-self: start !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
/* 2. Hide the original "Home" text */
font-size: 0 !important;
text-indent: -9999px;
/* 3. Apply the icon as the background (light mode) */
background-image: url("{home_icon_data_uri}") !important;
background-size: contain !important;
background-repeat: no-repeat !important;
background-position: center !important;
width: 240px !important;
height: 50px !important;
padding: 0 !important;
border: none !important;
outline: none !important;
}}
/* Dark mode logo override */
.dark .nav-holder nav a[href*="/home"] {{
background-image: url("{home_icon_dark_data_uri}") !important;
}}
"""
# --- Gradio App Definition ---
logger.info("Creating Gradio application")
demo = gr.Blocks(
theme=theme,
css=final_css,
head=posthog_script + scroll_script + redirect_script + fix_nav_links_script + tooltip_script + dark_mode_script,
title="OpenHands Index",
)
with demo.route("Home", "/home"):
build_main_page()
with demo.route("Issue Resolution", "/issue-resolution"):
build_bug_fixing_page()
with demo.route("Greenfield", "/greenfield"):
build_app_creation_page()
with demo.route("Frontend", "/frontend"):
build_frontend_page()
with demo.route("Testing", "/testing"):
build_test_generation_page()
with demo.route("Information Gathering", "/information-gathering"):
build_information_gathering_page()
with demo.route("Alternative Agents", "/alternative-agents"):
build_alternative_agents_page()
with demo.route("About", "/about"):
build_about_page()
logger.info("All routes configured")
# Mount the REST API on /api
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse
from starlette.middleware.base import BaseHTTPMiddleware
from api import api_app
class RootRedirectMiddleware(BaseHTTPMiddleware):
"""Middleware to redirect root path "/" to "/home".
This fixes the 307 trailing slash redirect issue (Gradio bug #11071) that
occurs when Gradio is mounted at "/" - FastAPI's default behavior redirects
"/" to "//", which breaks routing on HuggingFace Spaces.
See: https://github.com/gradio-app/gradio/issues/11071
"""
async def dispatch(self, request: Request, call_next):
if request.url.path == "/":
return RedirectResponse(url="/home", status_code=302)
return await call_next(request)
class StringifiedGradioJSONMiddleware:
"""Normalize JSON bodies double-encoded by the custom-domain proxy.
Requests sent through index.openhands.dev can arrive at Gradio as a JSON
string containing the real request object, which makes FastAPI validation
reject interactive callbacks with 422. Direct HF Space traffic already sends
proper JSON objects, so this only rewrites bodies that decode to strings.
"""
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if (
scope["type"] == "http"
and scope.get("method") == "POST"
and scope.get("path", "").startswith("/gradio_api/")
):
headers = {
key.decode("latin-1").lower(): value.decode("latin-1")
for key, value in scope.get("headers", [])
}
content_type = headers.get("content-type", "")
if "application/json" not in content_type:
return await self.app(scope, receive, send)
body_parts = []
while True:
message = await receive()
if message["type"] != "http.request":
break
body_parts.append(message.get("body", b""))
if not message.get("more_body", False):
break
body = b"".join(body_parts)
replacement_body = body
try:
decoded = json.loads(body)
except json.JSONDecodeError:
decoded = None
if isinstance(decoded, str):
stripped = decoded.strip()
if stripped.startswith(("{", "[")):
replacement_body = stripped.encode("utf-8")
sent = False
async def replay_receive():
nonlocal sent
if sent:
return {"type": "http.request", "body": b"", "more_body": False}
sent = True
return {
"type": "http.request",
"body": replacement_body,
"more_body": False,
}
return await self.app(scope, replay_receive, send)
return await self.app(scope, receive, send)
# Create a parent FastAPI app with redirect_slashes=False to prevent
# automatic trailing slash redirects that cause issues with Gradio
root_app = FastAPI(redirect_slashes=False)
# Add middleware to handle root path redirect to /home
root_app.add_middleware(RootRedirectMiddleware)
root_app.mount("/api", api_app)
# Mount Gradio app at root path
app = gr.mount_gradio_app(root_app, demo, path="/")
app = StringifiedGradioJSONMiddleware(app)
logger.info("REST API mounted at /api, Gradio app mounted at /")
# Launch the app
if __name__ == "__main__":
import uvicorn
# Respect platform port/host if provided (e.g., OpenHands runtime)
port = int(os.environ.get("PORT", os.environ.get("GRADIO_SERVER_PORT", 7860)))
host = os.environ.get("HOST", os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"))
logger.info(f"Launching app on {host}:{port}")
uvicorn.run(app, host=host, port=port)
logger.info("App launched successfully")