# 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 = """ """ hf_space_fetch_credentials_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=( hf_space_fetch_credentials_script + 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 custom-domain Gradio requests before they reach Gradio. 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. The custom domain also loads the Gradio frontend from Vercel while window.gradio_config.root points to the hf.space runtime. Chrome therefore requires successful credentialed CORS preflights for queue and heartbeat endpoints. """ CUSTOM_DOMAIN_ORIGIN = "https://index.openhands.dev" def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): origin = None if scope["type"] == "http": headers = { key.decode("latin-1").lower(): value.decode("latin-1") for key, value in scope.get("headers", []) } origin = headers.get("origin") should_apply_cors = ( scope["type"] == "http" and scope.get("path", "").startswith("/gradio_api/") and origin == self.CUSTOM_DOMAIN_ORIGIN ) if should_apply_cors: cors_headers = [ (b"access-control-allow-origin", self.CUSTOM_DOMAIN_ORIGIN.encode("latin-1")), (b"access-control-allow-credentials", b"true"), (b"access-control-allow-methods", b"DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT"), (b"access-control-allow-headers", b"*"), (b"access-control-expose-headers", b"*"), (b"vary", b"Origin"), ] if scope.get("method") == "OPTIONS": await send({ "type": "http.response.start", "status": 200, "headers": cors_headers, }) await send({"type": "http.response.body", "body": b""}) return async def cors_send(message): if message["type"] == "http.response.start": message["headers"] = [ (key, value) for key, value in message.get("headers", []) if not key.lower().startswith(b"access-control-") and key.lower() != b"vary" ] + cors_headers await send(message) else: cors_send = send if ( scope["type"] == "http" and scope.get("method") == "POST" and scope.get("path", "").startswith("/gradio_api/") ): content_type = headers.get("content-type", "") if "application/json" not in content_type: return await self.app(scope, receive, cors_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, cors_send) return await self.app(scope, receive, cors_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")