| |
| 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__) |
|
|
| |
| logger.info("Starting OpenHands Index application") |
|
|
| |
| 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() |
| 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_API_KEY = os.getenv("POSTHOG_API_KEY", "phc_ERBPfEE0gwNgkOBsxbHr1wh9mBsYcsw4zSLtvdA9RFg") |
| |
| 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> |
| """ |
|
|
| |
| 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> |
| """ |
|
|
| |
| |
| |
| |
| 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> |
| """ |
|
|
| |
| 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> |
| """ |
|
|
| |
| 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> |
| """ |
| |
| |
| |
| _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", |
| |
| border_color_accent="#e4e4e7", |
| border_color_accent_subdued="#e4e4e7", |
| border_color_accent_subdued_dark="#242424", |
| |
| 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", |
| |
| checkbox_background_color_selected="*neutral_950", |
| checkbox_background_color_selected_dark="*neutral_600", |
| |
| 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", |
| |
| block_title_text_size="*text_sm", |
| block_title_text_weight="500", |
| |
| 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)", |
| |
| 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", |
| ) |
| |
| 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" |
|
|
| |
| 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>" |
| ) |
|
|
| |
| 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; |
| }} |
| """ |
| ) |
| |
| 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") |
|
|
| |
| 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) |
|
|
|
|
| |
| |
| root_app = FastAPI(redirect_slashes=False) |
|
|
| |
| root_app.add_middleware(RootRedirectMiddleware) |
|
|
| root_app.mount("/api", api_app) |
|
|
| |
| app = gr.mount_gradio_app(root_app, demo, path="/") |
| logger.info("REST API mounted at /api, Gradio app mounted at /") |
|
|
|
|
| |
| if __name__ == "__main__": |
| import uvicorn |
| |
| 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")) |
| |
| _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") |
|
|
|
|