# app.py
import logging
import sys
import os
from constants import FONT_MONO_NAME, 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 urllib.parse
import gradio as gr
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()
# PostHog analytics (client-side)
POSTHOG_API_KEY = os.getenv("POSTHOG_API_KEY", "phc_ERBPfEE0gwNgkOBsxbHr1wh9mBsYcsw4zSLtvdA9RFg")
# OpenHands-Design typography (matches OpenHands-Design/index.html)
DESIGN_FONTS_LINK = """
"""
posthog_script = f"""
"""
# JavaScripts
scroll_script = """
"""
redirect_script = """
"""
# Gradio 5.30+ does not use .nav-holder — tag the real multipage links, then style via CSS
# (see: OpenHands-Design index.html top nav)
# IMPORTANT: do not strip all classes on every run — that unstyles the bar, changes layout, and
# makes getBoundingClientRect() unstable (layout thrash / "jumping" nav).
oh_top_nav_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 ---
# Aligned with OpenHands-Design (see OpenHands-Design/DESIGN.md, index.html)
# Near-black canvas, white primary CTA, neutral grey scale
_MUTED_GREEN = gr.themes.Color(
c50="#f0fdf4", c100="#dcfce7", c200="#bbf7d0", c300="#86efac", c400="#4ade80",
c500="#22c55e", c600="#16a34a", c700="#15803d", c800="#166534", c900="#14532d", c950="#052e16",
)
theme = gr.themes.Base(
primary_hue=gr.themes.Color(
c50="#fafafa", c100="#f4f4f5", c200="#e4e4e7", c300="#d4d4d8", c400="#a1a1aa",
c500="#ffffff", c600="#f4f4f5", c700="#e4e4e7", c800="#d4d4d8", c900="#a1a1aa", c950="#71717a"
),
secondary_hue=_MUTED_GREEN,
neutral_hue=gr.themes.Color(
c50="#fafafa", c100="#f4f4f4", c200="#e5e5e5", c300="#d4d4d4", c400="#a3a3a3",
c500="#737373", c600="#525252", c700="#404040", c800="#262626", c900="#171717", c950="#0d0d0d"
),
font=[FONT_FAMILY_SHORT, "system-ui", "sans-serif"],
font_mono=[FONT_MONO_NAME, "ui-monospace", "SFMono-Regular", "Menlo", "monospace"],
).set(
body_text_color="*neutral_950",
body_text_color_subdued="*neutral_600",
body_text_color_subdued_dark="*neutral_400",
body_text_color_dark="*neutral_50",
background_fill_primary="*neutral_50",
background_fill_primary_dark="*neutral_950",
background_fill_secondary="*neutral_100",
background_fill_secondary_dark="*neutral_900",
# Light: strokes match top nav (#e4e4e7); dark: DESIGN.md #242424 (--border / --input)
border_color_accent="#e4e4e7",
border_color_accent_subdued="#e4e4e7",
border_color_accent_subdued_dark="#242424",
# Primary border for inputs & dropdown chrome (maps to --border-color-primary in Gradio)
border_color_primary="#e4e4e7",
border_color_primary_dark="#242424",
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="*neutral_50",
link_text_color_visited_dark="*neutral_500",
table_even_background_fill="*neutral_100",
table_even_background_fill_dark="*neutral_900",
button_primary_background_fill="*primary_500",
button_primary_background_fill_dark="*primary_500",
button_primary_background_fill_hover="*primary_600",
button_primary_background_fill_hover_dark="*primary_600",
button_secondary_background_fill="*neutral_200",
button_secondary_background_fill_dark="*neutral_800",
button_secondary_text_color="*neutral_900",
button_secondary_text_color_dark="*neutral_50",
block_title_text_color="*neutral_950",
button_primary_text_color="*neutral_950",
block_title_text_color_dark="*neutral_50",
button_primary_text_color_dark="*neutral_950",
block_border_color="#e4e4e7",
block_border_color_dark="#242424",
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",
# Checkmark SVG is white; selected fill must not be white (this theme’s primary_500 = white → invisible)
checkbox_background_color_selected="*neutral_950",
checkbox_background_color_selected_dark="*neutral_600",
# OpenHands-Design §4 Inputs: rounded-md (4px), border-border, bg-muted/40, text-sm, focus ring #ccc + offset
input_radius="0.25rem",
input_border_width="1px",
input_border_color="*border_color_primary",
input_border_color_dark="#242424",
input_border_color_hover="*neutral_300",
input_border_color_hover_dark="#2e2e2e",
input_border_color_focus="#a1a1aa",
input_border_color_focus_dark="#525252",
input_background_fill="rgba(244, 244, 245, 0.75)",
input_background_fill_dark="rgba(31, 31, 31, 0.45)",
input_background_fill_hover="rgba(244, 244, 245, 0.9)",
input_background_fill_hover_dark="rgba(31, 31, 31, 0.55)",
input_background_fill_focus="rgba(229, 229, 234, 0.95)",
input_background_fill_focus_dark="rgba(31, 31, 31, 0.65)",
input_shadow="0 0 0 0 transparent",
input_shadow_dark="0 0 0 0 transparent",
input_shadow_focus="0 0 0 2px #fafafa, 0 0 0 3px #cccccc",
input_shadow_focus_dark="0 0 0 2px #0d0d0d, 0 0 0 3px #cccccc",
input_placeholder_color="*neutral_500",
input_placeholder_color_dark="#8c8c8c",
input_padding="8px 12px",
input_text_size="*text_sm",
input_text_weight="400",
# Form labels (BlockTitle): text-sm font-medium (colors set above)
block_title_text_size="*text_sm",
block_title_text_weight="500",
# Dropdown / popover elevation (§4 shadow-md)
shadow_drop="0 1px 2px 0 rgba(0, 0, 0, 0.12)",
shadow_drop_lg="0 4px 6px -1px rgba(0, 0, 0, 0.18), 0 2px 4px -2px rgba(0, 0, 0, 0.1)",
# Checkboxes: align border with inputs
checkbox_border_color="*neutral_300",
checkbox_border_color_dark="#242424",
checkbox_border_color_focus="#a1a1aa",
checkbox_border_color_focus_dark="#525252",
form_gap_width="12px",
)
# Top nav wordmark: on-light = black/dark ink (light bar, far left); on-dark = light ink (dark bar)
NAV_LOGO_SVG_LIGHT = "assets/openhands-logotype-on-light.svg"
NAV_LOGO_SVG_DARK = "assets/openhands-logotype-on-dark.svg"
try:
with open(NAV_LOGO_SVG_LIGHT, "r", encoding="utf-8") as _f:
_oh_nav_data_uri_light = f"data:image/svg+xml,{urllib.parse.quote(_f.read())}"
except OSError:
_oh_nav_data_uri_light = "none"
try:
with open(NAV_LOGO_SVG_DARK, "r", encoding="utf-8") as _f:
_oh_nav_data_uri_dark = f"data:image/svg+xml,{urllib.parse.quote(_f.read())}"
except OSError:
_oh_nav_data_uri_dark = "none"
# Early decode to reduce first-paint logo flicker (data URI, no extra network)
NAV_LOGO_PRELOAD = (
f''
f'
'
f'
'
f""
)
# --- This is the final CSS --- (appended after content.css so it wins the cascade for Home)
final_css = (
css
+ f"""
/* Multipage: trim duplicate /, hide unstyled duplicate Gradio */
gradio-app nav a[href$="/"] {{ display: none !important; }}
gradio-app .nav-holder nav a[href="/home"]:not(.oh-nav-link) {{ display: none !important; }}
/* Wordmark (Gradio /home): pinned top-left; no transition (reduces paint flicker) */
gradio-app nav.svelte-ti537g a[href*="/home"],
gradio-app .nav-holder nav a[href*="/home"],
gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"] {{
position: absolute !important;
left: 20px !important;
top: 0 !important;
bottom: 0 !important;
margin: auto 0 !important;
z-index: 2 !important;
display: inline-flex !important;
align-items: center !important;
font-size: 0 !important;
line-height: 0 !important;
text-indent: -9999px;
color: transparent !important;
overflow: hidden !important;
width: min(133px, 42vw) !important;
min-width: 80px !important;
min-height: 22px !important;
max-width: 133px !important;
height: 22px !important;
max-height: 22px !important;
box-sizing: content-box !important;
padding: 6px 12px 6px 12px !important;
flex: 0 0 auto !important;
flex-shrink: 0 !important;
background-color: transparent !important;
background-image: url("{_oh_nav_data_uri_light}") !important;
background-size: contain !important;
background-repeat: no-repeat !important;
background-position: left center !important;
border: none !important;
box-shadow: none !important;
border-radius: 6px !important;
transition: none !important;
}}
/* Dark top bar: light-colored wordmark */
html.dark gradio-app nav.svelte-ti537g a[href*="/home"],
html.dark gradio-app .nav-holder nav a[href*="/home"],
html.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"],
body.dark gradio-app nav.svelte-ti537g a[href*="/home"],
body.dark gradio-app .nav-holder nav a[href*="/home"],
body.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"],
html:has([class*="gradio-container-"].dark) gradio-app nav.svelte-ti537g a[href*="/home"],
html:has([class*="gradio-container-"].dark) gradio-app .nav-holder nav a[href*="/home"],
html:has([class*="gradio-container-"].dark) gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"],
.gradio-container.dark gradio-app nav.svelte-ti537g a[href*="/home"],
.gradio-container.dark gradio-app .nav-holder nav a[href*="/home"],
.gradio-container.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"],
[class*="gradio-container-"].dark gradio-app nav.svelte-ti537g a[href*="/home"],
[class*="gradio-container-"].dark gradio-app .nav-holder nav a[href*="/home"],
[class*="gradio-container-"].dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"] {{
background-image: url("{_oh_nav_data_uri_dark}") !important;
}}
/* Home wordmark: no grey hover/focus chip */
gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:hover,
gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:focus-visible,
gradio-app nav.svelte-ti537g a[href*="/home"]:hover,
html.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:hover,
html.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:focus-visible,
html.dark gradio-app nav.svelte-ti537g a[href*="/home"]:hover,
body.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:hover,
body.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:focus-visible,
body.dark gradio-app nav.svelte-ti537g a[href*="/home"]:hover,
html:has([class*="gradio-container-"].dark) gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:hover,
html:has([class*="gradio-container-"].dark) gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:focus-visible,
html:has([class*="gradio-container-"].dark) gradio-app nav.svelte-ti537g a[href*="/home"]:hover,
.gradio-container.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:hover,
.gradio-container.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:focus-visible,
.gradio-container.dark gradio-app nav.svelte-ti537g a[href*="/home"]:hover,
[class*="gradio-container-"].dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:hover,
[class*="gradio-container-"].dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:focus-visible,
[class*="gradio-container-"].dark gradio-app nav.svelte-ti537g a[href*="/home"]:hover {{
background-color: transparent !important;
}}
@media (max-width: 768px) {{
gradio-app nav.svelte-ti537g a[href*="/home"],
gradio-app .nav-holder nav a[href*="/home"],
gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"] {{
left: 20px !important;
}}
gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:hover,
gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:focus-visible,
gradio-app nav.svelte-ti537g a[href*="/home"]:hover {{
left: 20px !important;
}}
}}
/* Active Home (wordmark — route .active) */
gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].active,
gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].svelte-ti537g.active {{
color: transparent !important;
left: 20px !important;
top: 0 !important;
bottom: 0 !important;
margin: auto 0 !important;
background-color: #e4e4e7 !important;
background-image: url("{_oh_nav_data_uri_light}") !important;
background-size: contain !important;
background-repeat: no-repeat !important;
background-position: left center !important;
border-color: transparent !important;
box-shadow: none !important;
}}
html.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].active,
html.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].svelte-ti537g.active,
body.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].active,
body.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].svelte-ti537g.active,
html:has([class*="gradio-container-"].dark) gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].active,
html:has([class*="gradio-container-"].dark) gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].svelte-ti537g.active,
.gradio-container.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].active,
.gradio-container.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].svelte-ti537g.active,
[class*="gradio-container-"].dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].active,
[class*="gradio-container-"].dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].svelte-ti537g.active {{
background-color: hsl(0 0% 12% / 0.95) !important;
background-image: url("{_oh_nav_data_uri_dark}") !important;
}}
@media (max-width: 768px) {{
gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].active,
gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].svelte-ti537g.active {{
left: 20px !important;
}}
}}
/* Markdown `---` →
. var(--oh-border) = light #e4e4e7 / dark #242424 (content.py). */
gradio-app hr, .gradio-container hr, [class*="gradio-container-"] hr {{
box-sizing: border-box !important;
border: 0 !important;
border-top: 1px solid var(--oh-border) !important;
color: var(--oh-border) !important;
background: transparent !important;
background-color: transparent !important;
height: 0 !important;
opacity: 1 !important;
}}
/* Multipage route chips: Gradio 5.30 index bundle sets
a.active.svelte-ti537g {{ color: var(--body-text-color); background: var(--block-background-fill) }}.
This block is last in final_css; typography matches route chip rules in content.py (13px) for every tab including active. */
gradio-app nav a.svelte-ti537g.active:not([href*="/home"]) {{
color: #fafafa !important;
background-color: #18181b !important;
border-color: transparent !important;
box-shadow: none !important;
transition: none !important;
font-size: 13px !important;
line-height: 1.4 !important;
font-weight: 400 !important;
font-family: var(--oh-font-sans) !important;
}}
html.dark gradio-app nav a.svelte-ti537g.active:not([href*="/home"]),
body.dark gradio-app nav a.svelte-ti537g.active:not([href*="/home"]),
html:has([class*="gradio-container-"].dark) gradio-app nav a.svelte-ti537g.active:not([href*="/home"]),
.gradio-container.dark gradio-app nav a.svelte-ti537g.active:not([href*="/home"]),
[class*="gradio-container-"].dark gradio-app nav a.svelte-ti537g.active:not([href*="/home"]) {{
color: #ffffff !important;
background-color: hsl(0 0% 12% / 0.95) !important;
border-color: transparent !important;
box-shadow: none !important;
transition: none !important;
font-size: 13px !important;
line-height: 1.4 !important;
font-weight: 400 !important;
font-family: var(--oh-font-sans) !important;
}}
"""
)
# --- Gradio App Definition ---
logger.info("Creating Gradio application")
demo = gr.Blocks(
theme=theme,
css=final_css,
head=DESIGN_FONTS_LINK
+ NAV_LOGO_PRELOAD
+ posthog_script
+ scroll_script
+ redirect_script
+ oh_top_nav_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)
# 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="/")
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"))
# Auto-reload: set RELOAD=1 or UVICORN_RELOAD=1 to restart on .py changes (CSS in content.py, etc.)
_reload = os.environ.get("UVICORN_RELOAD", os.environ.get("RELOAD", "")).lower() in (
"1",
"true",
"yes",
)
logger.info(f"Launching app on {host}:{port}" + (" (auto-reload on .py changes)" if _reload else ""))
if _reload:
uvicorn.run("app:app", host=host, port=port, reload=True)
else:
uvicorn.run(app, host=host, port=port)
logger.info("App launched successfully")