openhands-index / app.py
juan-all-hands's picture
Sync UI from kosmonautical/openhands-index-paul with show_all_labels feature preserved
734891b verified
raw
history blame
36.2 kB
# 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")