# 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'" ) # --- 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")