Spaces:
Running
Running
Sync UI from kosmonautical/openhands-index-paul with show_all_labels feature preserved
734891b verified | # 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 = """ | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> | |
| """ | |
| posthog_script = f""" | |
| <script> | |
| !function(t,e){{var o,n,p,r;e.__SV||(window.posthog && window.posthog.__loaded)||(window.posthog=e,e._i=[],e.init=function(i,s,a){{function g(t,e){{var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){{t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){{var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e}},u.people.toString=function(){{return u.toString(1)+".people (stub)"}},o="init ss us bi os hs es ns capture Bi calculateEventProperties cs register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey displaySurvey cancelPendingSurvey canRenderSurvey canRenderSurveyAsync identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException startExceptionAutocapture stopExceptionAutocapture loadToolbar get_property getSessionProperty ps vs createPersonProfile gs Zr ys opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing get_explicit_consent_status is_capturing clear_opt_in_out_capturing ds debug O fs getPageViewId captureTraceFeedback captureTraceMetric Yr".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])}},e.__SV=1)}}(document,window.posthog||[]); | |
| posthog.init('{POSTHOG_API_KEY}', {{ | |
| api_host: 'https://us.i.posthog.com', | |
| defaults: '2025-11-30', | |
| person_profiles: 'identified_only', | |
| }}) | |
| </script> | |
| """ | |
| # JavaScripts | |
| scroll_script = """ | |
| <script> | |
| function scroll_to_element(id) { | |
| console.log("Global scroll_to_element called for ID:", id); | |
| const element = document.querySelector('#' + id); | |
| if (element) { | |
| console.log("Element found:", element); | |
| element.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| } else { | |
| console.error("Error: Element with ID '" + id + "' not found in the document."); | |
| } | |
| } | |
| </script> | |
| """ | |
| redirect_script = """ | |
| <script> | |
| if (window.location.pathname === '/') { window.location.replace('/home'); } | |
| </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 = """ | |
| <script> | |
| (function() { | |
| const ROUTE_PATHS = new Set(['/home', '/issue-resolution', '/greenfield', '/frontend', '/testing', '/information-gathering', '/about', '/alternative-agents']); | |
| var lastBar = null; | |
| var lastLinks = null; | |
| function pathKey(anchor) { | |
| try { | |
| var p = new URL(anchor.getAttribute('href') || '', window.location.origin).pathname.replace(/\\/$/, '') || '/'; | |
| if (p === '/' || p === '') p = '/home'; | |
| return p; | |
| } catch (e) { return null; } | |
| } | |
| function routePath(anchor) { | |
| var p = pathKey(anchor); | |
| return p && ROUTE_PATHS.has(p) ? p : null; | |
| } | |
| function getTopRowRouteLinks() { | |
| const g = document.querySelector('gradio-app'); | |
| if (!g) return []; | |
| /* Only links inside the multipage <nav> — not intro/Markdown (avoids byPath picking a | |
| body <a> first, and drops the #page-content-wrapper filter that could hide the bar). */ | |
| function routeLinksInNav(nav) { | |
| if (!nav) return []; | |
| return Array.from(nav.querySelectorAll('a[href]')).filter(function (a) { return routePath(a); }); | |
| } | |
| /* Prefer the <nav> that contains the Home wordmark (avoids wrong <nav> + works if Svelte class hash changes). */ | |
| var bar = getRouteNav() || g.querySelector('nav.svelte-ti537g'); | |
| var candidates = routeLinksInNav(bar); | |
| if (candidates.length < 1) { | |
| const navs = g.querySelectorAll('nav'); | |
| for (var i = 0; i < navs.length; i++) { | |
| const c = routeLinksInNav(navs[i]); | |
| if (c.length > 0) { bar = navs[i]; candidates = c; break; } | |
| } | |
| } | |
| if (candidates.length < 1) return []; | |
| /* Single top-nav link: still tag nav so wordmark/oh-top-nav CSS runs (avoids “missing logo / links stuck left” until 2+ links exist) */ | |
| if (candidates.length === 1) { | |
| const a = candidates[0]; | |
| const nav = a.closest("nav"); | |
| if (nav && g.contains(nav)) { | |
| return [a]; | |
| } | |
| return []; | |
| } | |
| const byPath = new Map(); | |
| for (const a of candidates) { | |
| const p = pathKey(a); | |
| if (!p || !ROUTE_PATHS.has(p)) continue; | |
| if (!byPath.has(p)) { | |
| byPath.set(p, a); | |
| } else { | |
| const cur = byPath.get(p); | |
| const curP = new URL(cur.getAttribute('href') || '', window.location.origin).pathname; | |
| const newP = new URL(a.getAttribute('href') || '', window.location.origin).pathname; | |
| if (p === '/home' && (curP === '/' || curP === '') && newP === '/home') { | |
| byPath.set(p, a); | |
| } | |
| } | |
| } | |
| const unique = Array.from(byPath.values()); | |
| const tops = unique.map((x) => x.getBoundingClientRect().top); | |
| const minTop = Math.min.apply(null, tops); | |
| /* ~1 row; always keep /home (wordmark) even if y differs (e.g. from position:absolute layout) */ | |
| return unique.filter((a) => { | |
| if (pathKey(a) === '/home') return true; | |
| return Math.abs(a.getBoundingClientRect().top - minTop) < 22; | |
| }); | |
| } | |
| function lca(els) { | |
| if (els.length < 2) return els[0] && els[0].parentElement; | |
| const chain = (node) => { const a = []; for (let n = node; n; n = n.parentElement) a.push(n); return a; }; | |
| let c = chain(els[0]); | |
| for (let i = 1; i < els.length; i++) { | |
| const d = new Set(chain(els[i])); | |
| c = c.filter((n) => d.has(n)); | |
| } | |
| return c[0] || null; | |
| } | |
| function sameLinkSet(a, b) { | |
| if (!a || !b || a.length !== b.length) return false; | |
| const sb = new Set(b); | |
| for (var i = 0; i < a.length; i++) { if (!sb.has(a[i])) return false; } | |
| return true; | |
| } | |
| function isDetached(el) { return el && !document.body.contains(el); } | |
| function clearBar() { | |
| if (lastBar) { lastBar.classList.remove('oh-top-nav'); } | |
| if (lastLinks) { lastLinks.forEach(function (a) { a.classList.remove('oh-nav-link'); }); } | |
| lastBar = null; | |
| lastLinks = null; | |
| } | |
| /** | |
| * Multipage top bar: must NOT use the first <nav> in the app (there can be others). | |
| * The wordmark (first /home) lives in the same <nav> as the route row — that is the bar. | |
| * Svelte class hash can change, so do not rely only on nav.svelte-ti537g. | |
| */ | |
| function getRouteNav() { | |
| var g = document.querySelector('gradio-app'); | |
| if (!g) return null; | |
| var h = g.querySelector('a[href*="/home"]'); | |
| if (h) { | |
| var n = h.closest('nav'); | |
| if (n && g.contains(n)) { return n; } | |
| } | |
| n = g.querySelector('nav.oh-top-nav, nav.svelte-ti537g, .nav-holder nav, nav[role="navigation"]'); | |
| if (n) return n; | |
| n = g.querySelector('nav'); | |
| return n || null; | |
| } | |
| function applyOhNav() { | |
| if (lastLinks && (lastLinks.some(isDetached) || (lastBar && isDetached(lastBar)))) { | |
| lastBar = null; | |
| lastLinks = null; | |
| } | |
| const links = getTopRowRouteLinks(); | |
| if (links.length < 1) { | |
| if (lastBar && lastLinks && !lastLinks.some(isDetached)) { | |
| return; | |
| } | |
| if (lastBar) { clearBar(); } | |
| return; | |
| } | |
| var row = lca(links); | |
| if (!row || row === document.body || (row.tagName && row.tagName.toLowerCase() === 'gradio-app')) { | |
| row = links[0].parentElement; | |
| } | |
| if (lastBar === row && lastLinks && sameLinkSet(lastLinks, links)) { | |
| return; | |
| } | |
| if (lastBar && lastBar !== row) { lastBar.classList.remove('oh-top-nav'); } | |
| if (lastLinks) { lastLinks.forEach(function (a) { a.classList.remove('oh-nav-link'); }); } | |
| if (row) { row.classList.add('oh-top-nav'); } | |
| links.forEach(function (a) { | |
| a.classList.add('oh-nav-link'); | |
| if (pathKey(a) === '/home') { | |
| a.classList.add('oh-nav-wordmark'); | |
| a.setAttribute('aria-label', 'OpenHands Home'); a.setAttribute('title', 'Home'); | |
| } | |
| }); | |
| lastBar = row; | |
| lastLinks = links.slice(); | |
| } | |
| var deb; | |
| function scheduleApply() { | |
| if (deb) { clearTimeout(deb); } | |
| deb = setTimeout(function () { requestAnimationFrame(applyOhNav); }, 50); | |
| } | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', function () { requestAnimationFrame(applyOhNav); }); | |
| } else { requestAnimationFrame(applyOhNav); } | |
| requestAnimationFrame(function () { requestAnimationFrame(applyOhNav); }); | |
| window.addEventListener('resize', scheduleApply, { passive: true }); | |
| var obs = new MutationObserver(scheduleApply); | |
| var ga = document.querySelector('gradio-app'); | |
| if (ga) { obs.observe(ga, { childList: true, subtree: true }); } | |
| else { document.addEventListener('DOMContentLoaded', function () { var g = document.querySelector('gradio-app'); if (g) obs.observe(g, { childList: true, subtree: true }); }); } | |
| try { if (typeof queueMicrotask === 'function') { queueMicrotask(applyOhNav); } } catch (e) {} | |
| setTimeout(applyOhNav, 0); | |
| setTimeout(applyOhNav, 50); | |
| setTimeout(applyOhNav, 500); | |
| setTimeout(applyOhNav, 2000); | |
| })(); | |
| </script> | |
| """ | |
| # JavaScript to fix navigation links to use relative paths (avoids domain mismatch when behind proxy) | |
| fix_nav_links_script = """ | |
| <script> | |
| (function() { | |
| function fixNavLinks() { | |
| const navLinks = document.querySelectorAll('gradio-app nav.oh-top-nav a[href], gradio-app nav.svelte-ti537g a[href]'); | |
| navLinks.forEach(link => { | |
| const href = link.getAttribute('href'); | |
| if (href) { | |
| try { | |
| const url = new URL(href, window.location.origin); | |
| if (url.pathname === '/' || url.pathname === '') { | |
| link.setAttribute('href', '/home'); | |
| } else if (url.pathname.startsWith('/')) { | |
| link.setAttribute('href', url.pathname); | |
| } | |
| } catch (e) { | |
| } | |
| } | |
| link.removeAttribute('target'); | |
| }); | |
| } | |
| // Run when DOM is ready | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', fixNavLinks); | |
| } else { | |
| fixNavLinks(); | |
| } | |
| // Also run periodically to catch dynamically added links | |
| setInterval(fixNavLinks, 1000); | |
| })(); | |
| </script> | |
| """ | |
| tooltip_script = """ | |
| <script> | |
| function initializeSmartTooltips() { | |
| // Find all tooltip trigger icons | |
| const tooltipIcons = document.querySelectorAll('.tooltip-icon-legend'); | |
| tooltipIcons.forEach(icon => { | |
| // Find the tooltip card associated with this icon | |
| const tooltipCard = icon.querySelector('.tooltip-card'); | |
| if (!tooltipCard) return; | |
| // Move the card to the end of the <body>. This is the KEY to escaping | |
| // any parent containers that might clip it. | |
| document.body.appendChild(tooltipCard); | |
| // --- MOUSE HOVER EVENT --- | |
| icon.addEventListener('mouseenter', () => { | |
| // Get the exact position of the icon on the screen | |
| const iconRect = icon.getBoundingClientRect(); | |
| // Get the dimensions of the tooltip card | |
| const cardRect = tooltipCard.getBoundingClientRect(); | |
| // Calculate the ideal top position (above the icon with a 10px gap) | |
| const top = iconRect.top - cardRect.height - 10; | |
| // --- Smart Centering Logic --- | |
| // Start by calculating the perfect center | |
| let left = iconRect.left + (iconRect.width / 2) - (cardRect.width / 2); | |
| // Check if it's going off the left edge of the screen | |
| if (left < 10) { | |
| left = 10; // Pin it to the left with a 10px margin | |
| } | |
| // Check if it's going off the right edge of the screen | |
| if (left + cardRect.width > window.innerWidth) { | |
| left = window.innerWidth - cardRect.width - 10; // Pin it to the right | |
| } | |
| // Apply the calculated position and show the card | |
| tooltipCard.style.top = `${top}px`; | |
| tooltipCard.style.left = `${left}px`; | |
| tooltipCard.classList.add('visible'); | |
| }); | |
| // --- MOUSE LEAVE EVENT --- | |
| icon.addEventListener('mouseleave', () => { | |
| // Hide the card | |
| tooltipCard.classList.remove('visible'); | |
| }); | |
| }); | |
| } | |
| // Poll the page until the tooltips exist, then run the initialization. | |
| const tooltipInterval = setInterval(() => { | |
| if (document.querySelector('.tooltip-icon-legend')) { | |
| clearInterval(tooltipInterval); | |
| initializeSmartTooltips(); | |
| } | |
| }, 200); | |
| </script> | |
| """ | |
| # JavaScript to handle dark mode for Plotly charts and OpenHands logos | |
| dark_mode_script = """ | |
| <script> | |
| function updateChartsForDarkMode() { | |
| const isDark = document.body.classList.contains('dark'); | |
| // Update Plotly chart backgrounds | |
| const plots = document.querySelectorAll('.js-plotly-plot'); | |
| plots.forEach(plot => { | |
| if (plot._fullLayout) { | |
| Plotly.relayout(plot, { | |
| 'paper_bgcolor': isDark ? '#1f1f1f' : 'white', | |
| 'plot_bgcolor': isDark ? '#1f1f1f' : 'white', | |
| 'font.color': isDark ? '#e0e0e0' : '#333', | |
| 'xaxis.gridcolor': isDark ? '#242424' : '#d4d4d4', | |
| 'yaxis.gridcolor': isDark ? '#242424' : '#d4d4d4', | |
| }); | |
| } | |
| }); | |
| // Swap OpenHands logos based on theme | |
| const images = document.querySelectorAll('.js-plotly-plot image'); | |
| images.forEach(img => { | |
| const href = img.getAttribute('href') || img.getAttribute('xlink:href') || ''; | |
| if (href.includes('openhands=lightlogo')) { | |
| img.style.display = isDark ? 'none' : ''; | |
| } else if (href.includes('openhands=darklogo')) { | |
| img.style.display = isDark ? '' : 'none'; | |
| } | |
| }); | |
| } | |
| document.addEventListener('DOMContentLoaded', () => { | |
| setTimeout(updateChartsForDarkMode, 500); | |
| const observer = new MutationObserver((mutations) => { | |
| mutations.forEach((mutation) => { | |
| if (mutation.attributeName === 'class') { | |
| updateChartsForDarkMode(); | |
| } | |
| }); | |
| }); | |
| observer.observe(document.body, { attributes: true }); | |
| setInterval(updateChartsForDarkMode, 1000); | |
| }); | |
| </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'<span class="oh-nav-logotype-preload" aria-hidden="true" style="position:absolute;width:0;height:0;overflow:hidden;clip:rect(0,0,0,0)">' | |
| f'<img src="{_oh_nav_data_uri_light}" width="160" height="40" alt="" decoding="async" fetchpriority="high"/>' | |
| f'<img src="{_oh_nav_data_uri_dark}" width="160" height="40" alt="" decoding="async" fetchpriority="low"/>' | |
| f"</span>" | |
| ) | |
| # --- 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 <a> */ | |
| 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 `---` → <hr>. 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") | |