Sync app.py from kosmonautical/openhands-index-paul

#23
Files changed (1) hide show
  1. app.py +486 -118
app.py CHANGED
@@ -3,7 +3,7 @@ import logging
3
  import sys
4
  import os
5
 
6
- from constants import FONT_FAMILY_SHORT
7
 
8
  logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
9
  logger = logging.getLogger(__name__)
@@ -24,8 +24,9 @@ except Exception as e:
24
  logger.error(f"Error during data setup: {e}", exc_info=True)
25
  logger.warning("Continuing with app startup despite error")
26
 
27
- import gradio as gr
28
  import urllib.parse
 
 
29
  from huggingface_hub import HfApi
30
  from config import LEADERBOARD_PATH, LOCAL_DEBUG
31
  from content import css
@@ -41,10 +42,16 @@ from about import build_page as build_about_page
41
  logger.info(f"All modules imported (LOCAL_DEBUG={LOCAL_DEBUG})")
42
 
43
  api = HfApi()
44
- LOGO_PATH = "assets/logo.svg"
45
 
46
  # PostHog analytics (client-side)
47
  POSTHOG_API_KEY = os.getenv("POSTHOG_API_KEY", "phc_ERBPfEE0gwNgkOBsxbHr1wh9mBsYcsw4zSLtvdA9RFg")
 
 
 
 
 
 
 
48
  posthog_script = f"""
49
  <script>
50
  !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||[]);
@@ -77,28 +84,196 @@ redirect_script = """
77
  </script>
78
  """
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  # JavaScript to fix navigation links to use relative paths (avoids domain mismatch when behind proxy)
81
  fix_nav_links_script = """
82
  <script>
83
  (function() {
84
  function fixNavLinks() {
85
- // Find all navigation links in the nav-holder
86
- const navLinks = document.querySelectorAll('.nav-holder nav a');
87
  navLinks.forEach(link => {
88
  const href = link.getAttribute('href');
89
  if (href) {
90
- // Extract the pathname from the href (works with both relative and absolute URLs)
91
  try {
92
  const url = new URL(href, window.location.origin);
93
- // Only update if the pathname starts with /
94
- if (url.pathname.startsWith('/')) {
 
95
  link.setAttribute('href', url.pathname);
96
  }
97
  } catch (e) {
98
- // If URL parsing fails, leave the href as-is
99
  }
100
  }
101
- // Remove target="_blank" from nav links to prevent opening in new tab
102
  link.removeAttribute('target');
103
  });
104
  }
@@ -191,8 +366,8 @@ function updateChartsForDarkMode() {
191
  'paper_bgcolor': isDark ? '#1f1f1f' : 'white',
192
  'plot_bgcolor': isDark ? '#1f1f1f' : 'white',
193
  'font.color': isDark ? '#e0e0e0' : '#333',
194
- 'xaxis.gridcolor': isDark ? '#444' : '#eee',
195
- 'yaxis.gridcolor': isDark ? '#444' : '#eee'
196
  });
197
  }
198
  });
@@ -226,133 +401,317 @@ document.addEventListener('DOMContentLoaded', () => {
226
  </script>
227
  """
228
  # --- Theme Definition ---
229
- # Color scheme aligned with OpenHands brand (from openhands-ui/tokens.css)
230
- # Primary: Yellow (#FFE165), Neutral: Grey scale, Accents: Green (#BCFF8C), Red (#FF684E)
 
 
 
 
231
  theme = gr.themes.Base(
232
- # Primary hue - Yellow (OpenHands brand color)
233
  primary_hue=gr.themes.Color(
234
- c50="#FFFCF0", c100="#FFF3C0", c200="#FFEEAA", c300="#FFEA92", c400="#FFE57B",
235
- c500="#FFE165", c600="#DCC257", c700="#BBA54A", c800="#99873D", c900="#76682F", c950="#534921"
236
  ),
237
- # Secondary hue - Green accent (from OpenHands palette)
238
- secondary_hue=gr.themes.Color(
239
- c50="#F8FFF4", c100="#E4FFD0", c200="#DAFFBF", c300="#CFFFAD", c400="#C6FF9D",
240
- c500="#BCFF8C", c600="#A2DC79", c700="#8ABB67", c800="#719954", c900="#577641", c950="#3D532E"
241
- ),
242
- # Neutral hue - Grey scale (OpenHands dark mode colors)
243
  neutral_hue=gr.themes.Color(
244
- c50="#F7F8FB", c100="#EBEDF3", c200="#D4D8E7", c300="#B1B9D3", c400="#82889B",
245
- c500="#525662", c600="#3A3C45", c700="#2F3137", c800="#222328", c900="#18191C", c950="#0D0D0F"
246
  ),
247
- font=[FONT_FAMILY_SHORT, 'sans-serif'],
248
- font_mono=['monospace'],
249
  ).set(
250
- body_text_color='*neutral_950',
251
- body_text_color_subdued='*neutral_700',
252
- body_text_color_subdued_dark='*neutral_300',
253
- body_text_color_dark='*neutral_50',
254
- background_fill_primary='*neutral_50',
255
- background_fill_primary_dark='*neutral_900',
256
- background_fill_secondary='*neutral_100',
257
- background_fill_secondary_dark='*neutral_800',
258
- border_color_accent='*primary_500',
259
- border_color_accent_subdued='*neutral_300',
260
- border_color_accent_subdued_dark='*neutral_600',
261
- color_accent='*primary_500',
262
- color_accent_soft='*neutral_200',
263
- color_accent_soft_dark='*neutral_800',
264
- link_text_color='*neutral_700',
265
- link_text_color_dark='*neutral_300',
266
- link_text_color_active_dark='*primary_500',
267
- link_text_color_hover_dark='*primary_400',
268
- link_text_color_visited_dark='*neutral_400',
269
- table_even_background_fill='*neutral_100',
270
- table_even_background_fill_dark='*neutral_800',
271
- button_primary_background_fill='*primary_500',
272
- button_primary_background_fill_dark='*primary_500',
273
- button_primary_background_fill_hover='*primary_400',
274
- button_primary_background_fill_hover_dark='*primary_400',
275
- button_secondary_background_fill='*secondary_500',
276
- button_secondary_background_fill_dark='*secondary_600',
277
- button_secondary_text_color='*neutral_900',
278
- button_secondary_text_color_dark='*neutral_900',
279
- block_title_text_color='*neutral_900',
280
- button_primary_text_color='*neutral_900',
281
- block_title_text_color_dark='*neutral_50',
282
- button_primary_text_color_dark='*neutral_900',
283
- block_border_color='*neutral_300',
284
- block_border_color_dark='*neutral_700',
285
- block_background_fill_dark='*neutral_900',
286
- block_background_fill='*neutral_50',
287
- checkbox_label_text_color='*neutral_900',
288
- checkbox_label_background_fill='*neutral_200',
289
- checkbox_label_background_fill_dark='*neutral_700',
290
- checkbox_background_color_selected='*primary_500',
291
- checkbox_background_color_selected_dark='*primary_500',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  )
 
 
 
293
  try:
294
- with open(LOGO_PATH, "r") as f:
295
- svg_content = f.read()
296
- encoded_svg = urllib.parse.quote(svg_content)
297
- home_icon_data_uri = f"data:image/svg+xml,{encoded_svg}"
298
- except FileNotFoundError:
299
- logger.warning(f"Home icon file not found at {LOGO_PATH}")
300
- home_icon_data_uri = "none"
301
-
302
- # Load dark mode logo (PNG)
303
- LOGO_DARK_PATH = "assets/logo-dark.png"
304
  try:
305
- import base64
306
- with open(LOGO_DARK_PATH, "rb") as f:
307
- dark_logo_content = f.read()
308
- encoded_dark_logo = base64.b64encode(dark_logo_content).decode('utf-8')
309
- home_icon_dark_data_uri = f"data:image/png;base64,{encoded_dark_logo}"
310
- except FileNotFoundError:
311
- logger.warning(f"Dark mode logo file not found at {LOGO_DARK_PATH}")
312
- home_icon_dark_data_uri = home_icon_data_uri # Fallback to light logo
313
-
314
- # --- This is the final CSS ---
315
- final_css = css + f"""
316
- /* --- Find the "Home" button and replace its text with an icon --- */
317
- .nav-holder nav a[href$="/"] {{
318
- display: none !important;
319
- }}
320
- .nav-holder nav a[href*="/home"] {{
321
- grid-row: 1 !important;
322
- grid-column: 1 !important;
323
- justify-self: start !important;
324
- display: flex !important;
325
- align-items: center !important;
326
- justify-content: center !important;
327
 
328
- /* 2. Hide the original "Home" text */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  font-size: 0 !important;
 
330
  text-indent: -9999px;
331
-
332
- /* 3. Apply the icon as the background (light mode) */
333
- background-image: url("{home_icon_data_uri}") !important;
 
 
 
 
 
 
 
 
 
 
 
334
  background-size: contain !important;
335
  background-repeat: no-repeat !important;
336
- background-position: center !important;
337
-
338
- width: 240px !important;
339
- height: 50px !important;
340
- padding: 0 !important;
341
  border: none !important;
342
- outline: none !important;
 
 
343
  }}
344
-
345
- /* Dark mode logo override */
346
- .dark .nav-holder nav a[href*="/home"] {{
347
- background-image: url("{home_icon_dark_data_uri}") !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  }}
349
  """
 
350
  # --- Gradio App Definition ---
351
  logger.info("Creating Gradio application")
352
  demo = gr.Blocks(
353
  theme=theme,
354
  css=final_css,
355
- head=posthog_script + scroll_script + redirect_script + fix_nav_links_script + tooltip_script + dark_mode_script,
 
 
 
 
 
 
 
 
356
  title="OpenHands Index",
357
  )
358
 
@@ -424,7 +783,16 @@ if __name__ == "__main__":
424
  # Respect platform port/host if provided (e.g., OpenHands runtime)
425
  port = int(os.environ.get("PORT", os.environ.get("GRADIO_SERVER_PORT", 7860)))
426
  host = os.environ.get("HOST", os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"))
427
- logger.info(f"Launching app on {host}:{port}")
428
- uvicorn.run(app, host=host, port=port)
 
 
 
 
 
 
 
 
 
429
  logger.info("App launched successfully")
430
 
 
3
  import sys
4
  import os
5
 
6
+ from constants import FONT_MONO_NAME, FONT_FAMILY_SHORT
7
 
8
  logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
9
  logger = logging.getLogger(__name__)
 
24
  logger.error(f"Error during data setup: {e}", exc_info=True)
25
  logger.warning("Continuing with app startup despite error")
26
 
 
27
  import urllib.parse
28
+
29
+ import gradio as gr
30
  from huggingface_hub import HfApi
31
  from config import LEADERBOARD_PATH, LOCAL_DEBUG
32
  from content import css
 
42
  logger.info(f"All modules imported (LOCAL_DEBUG={LOCAL_DEBUG})")
43
 
44
  api = HfApi()
 
45
 
46
  # PostHog analytics (client-side)
47
  POSTHOG_API_KEY = os.getenv("POSTHOG_API_KEY", "phc_ERBPfEE0gwNgkOBsxbHr1wh9mBsYcsw4zSLtvdA9RFg")
48
+ # OpenHands-Design typography (matches OpenHands-Design/index.html)
49
+ DESIGN_FONTS_LINK = """
50
+ <link rel="preconnect" href="https://fonts.googleapis.com">
51
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
52
+ <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">
53
+ """
54
+
55
  posthog_script = f"""
56
  <script>
57
  !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||[]);
 
84
  </script>
85
  """
86
 
87
+ # Gradio 5.30+ does not use .nav-holder — tag the real multipage links, then style via CSS
88
+ # (see: OpenHands-Design index.html top nav)
89
+ # IMPORTANT: do not strip all classes on every run — that unstyles the bar, changes layout, and
90
+ # makes getBoundingClientRect() unstable (layout thrash / "jumping" nav).
91
+ oh_top_nav_script = """
92
+ <script>
93
+ (function() {
94
+ const ROUTE_PATHS = new Set(['/home', '/issue-resolution', '/greenfield', '/frontend', '/testing', '/information-gathering', '/about', '/alternative-agents']);
95
+ var lastBar = null;
96
+ var lastLinks = null;
97
+ function pathKey(anchor) {
98
+ try {
99
+ var p = new URL(anchor.getAttribute('href') || '', window.location.origin).pathname.replace(/\\/$/, '') || '/';
100
+ if (p === '/' || p === '') p = '/home';
101
+ return p;
102
+ } catch (e) { return null; }
103
+ }
104
+ function routePath(anchor) {
105
+ var p = pathKey(anchor);
106
+ return p && ROUTE_PATHS.has(p) ? p : null;
107
+ }
108
+ function getTopRowRouteLinks() {
109
+ const g = document.querySelector('gradio-app');
110
+ if (!g) return [];
111
+ /* Only links inside the multipage <nav> — not intro/Markdown (avoids byPath picking a
112
+ body <a> first, and drops the #page-content-wrapper filter that could hide the bar). */
113
+ function routeLinksInNav(nav) {
114
+ if (!nav) return [];
115
+ return Array.from(nav.querySelectorAll('a[href]')).filter(function (a) { return routePath(a); });
116
+ }
117
+ /* Prefer the <nav> that contains the Home wordmark (avoids wrong <nav> + works if Svelte class hash changes). */
118
+ var bar = getRouteNav() || g.querySelector('nav.svelte-ti537g');
119
+ var candidates = routeLinksInNav(bar);
120
+ if (candidates.length < 1) {
121
+ const navs = g.querySelectorAll('nav');
122
+ for (var i = 0; i < navs.length; i++) {
123
+ const c = routeLinksInNav(navs[i]);
124
+ if (c.length > 0) { bar = navs[i]; candidates = c; break; }
125
+ }
126
+ }
127
+ if (candidates.length < 1) return [];
128
+ /* Single top-nav link: still tag nav so wordmark/oh-top-nav CSS runs (avoids “missing logo / links stuck left” until 2+ links exist) */
129
+ if (candidates.length === 1) {
130
+ const a = candidates[0];
131
+ const nav = a.closest("nav");
132
+ if (nav && g.contains(nav)) {
133
+ return [a];
134
+ }
135
+ return [];
136
+ }
137
+ const byPath = new Map();
138
+ for (const a of candidates) {
139
+ const p = pathKey(a);
140
+ if (!p || !ROUTE_PATHS.has(p)) continue;
141
+ if (!byPath.has(p)) {
142
+ byPath.set(p, a);
143
+ } else {
144
+ const cur = byPath.get(p);
145
+ const curP = new URL(cur.getAttribute('href') || '', window.location.origin).pathname;
146
+ const newP = new URL(a.getAttribute('href') || '', window.location.origin).pathname;
147
+ if (p === '/home' && (curP === '/' || curP === '') && newP === '/home') {
148
+ byPath.set(p, a);
149
+ }
150
+ }
151
+ }
152
+ const unique = Array.from(byPath.values());
153
+ const tops = unique.map((x) => x.getBoundingClientRect().top);
154
+ const minTop = Math.min.apply(null, tops);
155
+ /* ~1 row; always keep /home (wordmark) even if y differs (e.g. from position:absolute layout) */
156
+ return unique.filter((a) => {
157
+ if (pathKey(a) === '/home') return true;
158
+ return Math.abs(a.getBoundingClientRect().top - minTop) < 22;
159
+ });
160
+ }
161
+ function lca(els) {
162
+ if (els.length < 2) return els[0] && els[0].parentElement;
163
+ const chain = (node) => { const a = []; for (let n = node; n; n = n.parentElement) a.push(n); return a; };
164
+ let c = chain(els[0]);
165
+ for (let i = 1; i < els.length; i++) {
166
+ const d = new Set(chain(els[i]));
167
+ c = c.filter((n) => d.has(n));
168
+ }
169
+ return c[0] || null;
170
+ }
171
+ function sameLinkSet(a, b) {
172
+ if (!a || !b || a.length !== b.length) return false;
173
+ const sb = new Set(b);
174
+ for (var i = 0; i < a.length; i++) { if (!sb.has(a[i])) return false; }
175
+ return true;
176
+ }
177
+ function isDetached(el) { return el && !document.body.contains(el); }
178
+ function clearBar() {
179
+ if (lastBar) { lastBar.classList.remove('oh-top-nav'); }
180
+ if (lastLinks) { lastLinks.forEach(function (a) { a.classList.remove('oh-nav-link'); }); }
181
+ lastBar = null;
182
+ lastLinks = null;
183
+ }
184
+ /**
185
+ * Multipage top bar: must NOT use the first <nav> in the app (there can be others).
186
+ * The wordmark (first /home) lives in the same <nav> as the route row — that is the bar.
187
+ * Svelte class hash can change, so do not rely only on nav.svelte-ti537g.
188
+ */
189
+ function getRouteNav() {
190
+ var g = document.querySelector('gradio-app');
191
+ if (!g) return null;
192
+ var h = g.querySelector('a[href*="/home"]');
193
+ if (h) {
194
+ var n = h.closest('nav');
195
+ if (n && g.contains(n)) { return n; }
196
+ }
197
+ n = g.querySelector('nav.oh-top-nav, nav.svelte-ti537g, .nav-holder nav, nav[role="navigation"]');
198
+ if (n) return n;
199
+ n = g.querySelector('nav');
200
+ return n || null;
201
+ }
202
+ function applyOhNav() {
203
+ if (lastLinks && (lastLinks.some(isDetached) || (lastBar && isDetached(lastBar)))) {
204
+ lastBar = null;
205
+ lastLinks = null;
206
+ }
207
+ const links = getTopRowRouteLinks();
208
+ if (links.length < 1) {
209
+ if (lastBar && lastLinks && !lastLinks.some(isDetached)) {
210
+ return;
211
+ }
212
+ if (lastBar) { clearBar(); }
213
+ return;
214
+ }
215
+ var row = lca(links);
216
+ if (!row || row === document.body || (row.tagName && row.tagName.toLowerCase() === 'gradio-app')) {
217
+ row = links[0].parentElement;
218
+ }
219
+ if (lastBar === row && lastLinks && sameLinkSet(lastLinks, links)) {
220
+ return;
221
+ }
222
+ if (lastBar && lastBar !== row) { lastBar.classList.remove('oh-top-nav'); }
223
+ if (lastLinks) { lastLinks.forEach(function (a) { a.classList.remove('oh-nav-link'); }); }
224
+ if (row) { row.classList.add('oh-top-nav'); }
225
+ links.forEach(function (a) {
226
+ a.classList.add('oh-nav-link');
227
+ if (pathKey(a) === '/home') {
228
+ a.classList.add('oh-nav-wordmark');
229
+ a.setAttribute('aria-label', 'OpenHands Home'); a.setAttribute('title', 'Home');
230
+ }
231
+ });
232
+ lastBar = row;
233
+ lastLinks = links.slice();
234
+ }
235
+ var deb;
236
+ function scheduleApply() {
237
+ if (deb) { clearTimeout(deb); }
238
+ deb = setTimeout(function () { requestAnimationFrame(applyOhNav); }, 50);
239
+ }
240
+ if (document.readyState === 'loading') {
241
+ document.addEventListener('DOMContentLoaded', function () { requestAnimationFrame(applyOhNav); });
242
+ } else { requestAnimationFrame(applyOhNav); }
243
+ requestAnimationFrame(function () { requestAnimationFrame(applyOhNav); });
244
+ window.addEventListener('resize', scheduleApply, { passive: true });
245
+ var obs = new MutationObserver(scheduleApply);
246
+ var ga = document.querySelector('gradio-app');
247
+ if (ga) { obs.observe(ga, { childList: true, subtree: true }); }
248
+ else { document.addEventListener('DOMContentLoaded', function () { var g = document.querySelector('gradio-app'); if (g) obs.observe(g, { childList: true, subtree: true }); }); }
249
+ try { if (typeof queueMicrotask === 'function') { queueMicrotask(applyOhNav); } } catch (e) {}
250
+ setTimeout(applyOhNav, 0);
251
+ setTimeout(applyOhNav, 50);
252
+ setTimeout(applyOhNav, 500);
253
+ setTimeout(applyOhNav, 2000);
254
+ })();
255
+ </script>
256
+ """
257
+
258
  # JavaScript to fix navigation links to use relative paths (avoids domain mismatch when behind proxy)
259
  fix_nav_links_script = """
260
  <script>
261
  (function() {
262
  function fixNavLinks() {
263
+ const navLinks = document.querySelectorAll('gradio-app nav.oh-top-nav a[href], gradio-app nav.svelte-ti537g a[href]');
 
264
  navLinks.forEach(link => {
265
  const href = link.getAttribute('href');
266
  if (href) {
 
267
  try {
268
  const url = new URL(href, window.location.origin);
269
+ if (url.pathname === '/' || url.pathname === '') {
270
+ link.setAttribute('href', '/home');
271
+ } else if (url.pathname.startsWith('/')) {
272
  link.setAttribute('href', url.pathname);
273
  }
274
  } catch (e) {
 
275
  }
276
  }
 
277
  link.removeAttribute('target');
278
  });
279
  }
 
366
  'paper_bgcolor': isDark ? '#1f1f1f' : 'white',
367
  'plot_bgcolor': isDark ? '#1f1f1f' : 'white',
368
  'font.color': isDark ? '#e0e0e0' : '#333',
369
+ 'xaxis.gridcolor': isDark ? '#242424' : '#d4d4d4',
370
+ 'yaxis.gridcolor': isDark ? '#242424' : '#d4d4d4',
371
  });
372
  }
373
  });
 
401
  </script>
402
  """
403
  # --- Theme Definition ---
404
+ # Aligned with OpenHands-Design (see OpenHands-Design/DESIGN.md, index.html)
405
+ # Near-black canvas, white primary CTA, neutral grey scale
406
+ _MUTED_GREEN = gr.themes.Color(
407
+ c50="#f0fdf4", c100="#dcfce7", c200="#bbf7d0", c300="#86efac", c400="#4ade80",
408
+ c500="#22c55e", c600="#16a34a", c700="#15803d", c800="#166534", c900="#14532d", c950="#052e16",
409
+ )
410
  theme = gr.themes.Base(
 
411
  primary_hue=gr.themes.Color(
412
+ c50="#fafafa", c100="#f4f4f5", c200="#e4e4e7", c300="#d4d4d8", c400="#a1a1aa",
413
+ c500="#ffffff", c600="#f4f4f5", c700="#e4e4e7", c800="#d4d4d8", c900="#a1a1aa", c950="#71717a"
414
  ),
415
+ secondary_hue=_MUTED_GREEN,
 
 
 
 
 
416
  neutral_hue=gr.themes.Color(
417
+ c50="#fafafa", c100="#f4f4f4", c200="#e5e5e5", c300="#d4d4d4", c400="#a3a3a3",
418
+ c500="#737373", c600="#525252", c700="#404040", c800="#262626", c900="#171717", c950="#0d0d0d"
419
  ),
420
+ font=[FONT_FAMILY_SHORT, "system-ui", "sans-serif"],
421
+ font_mono=[FONT_MONO_NAME, "ui-monospace", "SFMono-Regular", "Menlo", "monospace"],
422
  ).set(
423
+ body_text_color="*neutral_950",
424
+ body_text_color_subdued="*neutral_600",
425
+ body_text_color_subdued_dark="*neutral_400",
426
+ body_text_color_dark="*neutral_50",
427
+ background_fill_primary="*neutral_50",
428
+ background_fill_primary_dark="*neutral_950",
429
+ background_fill_secondary="*neutral_100",
430
+ background_fill_secondary_dark="*neutral_900",
431
+ # Light: strokes match top nav (#e4e4e7); dark: DESIGN.md #242424 (--border / --input)
432
+ border_color_accent="#e4e4e7",
433
+ border_color_accent_subdued="#e4e4e7",
434
+ border_color_accent_subdued_dark="#242424",
435
+ # Primary border for inputs & dropdown chrome (maps to --border-color-primary in Gradio)
436
+ border_color_primary="#e4e4e7",
437
+ border_color_primary_dark="#242424",
438
+ color_accent="*primary_500",
439
+ color_accent_soft="*neutral_200",
440
+ color_accent_soft_dark="*neutral_800",
441
+ link_text_color="*neutral_700",
442
+ link_text_color_dark="*neutral_300",
443
+ link_text_color_active_dark="*primary_500",
444
+ link_text_color_hover_dark="*neutral_50",
445
+ link_text_color_visited_dark="*neutral_500",
446
+ table_even_background_fill="*neutral_100",
447
+ table_even_background_fill_dark="*neutral_900",
448
+ button_primary_background_fill="*primary_500",
449
+ button_primary_background_fill_dark="*primary_500",
450
+ button_primary_background_fill_hover="*primary_600",
451
+ button_primary_background_fill_hover_dark="*primary_600",
452
+ button_secondary_background_fill="*neutral_200",
453
+ button_secondary_background_fill_dark="*neutral_800",
454
+ button_secondary_text_color="*neutral_900",
455
+ button_secondary_text_color_dark="*neutral_50",
456
+ block_title_text_color="*neutral_950",
457
+ button_primary_text_color="*neutral_950",
458
+ block_title_text_color_dark="*neutral_50",
459
+ button_primary_text_color_dark="*neutral_950",
460
+ block_border_color="#e4e4e7",
461
+ block_border_color_dark="#242424",
462
+ block_background_fill_dark="*neutral_900",
463
+ block_background_fill="*neutral_50",
464
+ checkbox_label_text_color="*neutral_900",
465
+ checkbox_label_background_fill="*neutral_200",
466
+ checkbox_label_background_fill_dark="*neutral_700",
467
+ # Checkmark SVG is white; selected fill must not be white (this theme’s primary_500 = white → invisible)
468
+ checkbox_background_color_selected="*neutral_950",
469
+ checkbox_background_color_selected_dark="*neutral_600",
470
+ # OpenHands-Design §4 Inputs: rounded-md (4px), border-border, bg-muted/40, text-sm, focus ring #ccc + offset
471
+ input_radius="0.25rem",
472
+ input_border_width="1px",
473
+ input_border_color="*border_color_primary",
474
+ input_border_color_dark="#242424",
475
+ input_border_color_hover="*neutral_300",
476
+ input_border_color_hover_dark="#2e2e2e",
477
+ input_border_color_focus="#a1a1aa",
478
+ input_border_color_focus_dark="#525252",
479
+ input_background_fill="rgba(244, 244, 245, 0.75)",
480
+ input_background_fill_dark="rgba(31, 31, 31, 0.45)",
481
+ input_background_fill_hover="rgba(244, 244, 245, 0.9)",
482
+ input_background_fill_hover_dark="rgba(31, 31, 31, 0.55)",
483
+ input_background_fill_focus="rgba(229, 229, 234, 0.95)",
484
+ input_background_fill_focus_dark="rgba(31, 31, 31, 0.65)",
485
+ input_shadow="0 0 0 0 transparent",
486
+ input_shadow_dark="0 0 0 0 transparent",
487
+ input_shadow_focus="0 0 0 2px #fafafa, 0 0 0 3px #cccccc",
488
+ input_shadow_focus_dark="0 0 0 2px #0d0d0d, 0 0 0 3px #cccccc",
489
+ input_placeholder_color="*neutral_500",
490
+ input_placeholder_color_dark="#8c8c8c",
491
+ input_padding="8px 12px",
492
+ input_text_size="*text_sm",
493
+ input_text_weight="400",
494
+ # Form labels (BlockTitle): text-sm font-medium (colors set above)
495
+ block_title_text_size="*text_sm",
496
+ block_title_text_weight="500",
497
+ # Dropdown / popover elevation (§4 shadow-md)
498
+ shadow_drop="0 1px 2px 0 rgba(0, 0, 0, 0.12)",
499
+ shadow_drop_lg="0 4px 6px -1px rgba(0, 0, 0, 0.18), 0 2px 4px -2px rgba(0, 0, 0, 0.1)",
500
+ # Checkboxes: align border with inputs
501
+ checkbox_border_color="*neutral_300",
502
+ checkbox_border_color_dark="#242424",
503
+ checkbox_border_color_focus="#a1a1aa",
504
+ checkbox_border_color_focus_dark="#525252",
505
+ form_gap_width="12px",
506
  )
507
+ # Top nav wordmark: on-light = black/dark ink (light bar, far left); on-dark = light ink (dark bar)
508
+ NAV_LOGO_SVG_LIGHT = "assets/openhands-logotype-on-light.svg"
509
+ NAV_LOGO_SVG_DARK = "assets/openhands-logotype-on-dark.svg"
510
  try:
511
+ with open(NAV_LOGO_SVG_LIGHT, "r", encoding="utf-8") as _f:
512
+ _oh_nav_data_uri_light = f"data:image/svg+xml,{urllib.parse.quote(_f.read())}"
513
+ except OSError:
514
+ _oh_nav_data_uri_light = "none"
 
 
 
 
 
 
515
  try:
516
+ with open(NAV_LOGO_SVG_DARK, "r", encoding="utf-8") as _f:
517
+ _oh_nav_data_uri_dark = f"data:image/svg+xml,{urllib.parse.quote(_f.read())}"
518
+ except OSError:
519
+ _oh_nav_data_uri_dark = "none"
520
+
521
+ # Early decode to reduce first-paint logo flicker (data URI, no extra network)
522
+ NAV_LOGO_PRELOAD = (
523
+ 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)">'
524
+ f'<img src="{_oh_nav_data_uri_light}" width="160" height="40" alt="" decoding="async" fetchpriority="high"/>'
525
+ f'<img src="{_oh_nav_data_uri_dark}" width="160" height="40" alt="" decoding="async" fetchpriority="low"/>'
526
+ f"</span>"
527
+ )
 
 
 
 
 
 
 
 
 
 
528
 
529
+ # --- This is the final CSS --- (appended after content.css so it wins the cascade for Home)
530
+ final_css = (
531
+ css
532
+ + f"""
533
+ /* Multipage: trim duplicate /, hide unstyled duplicate Gradio <a> */
534
+ gradio-app nav a[href$="/"] {{ display: none !important; }}
535
+ gradio-app .nav-holder nav a[href="/home"]:not(.oh-nav-link) {{ display: none !important; }}
536
+ /* Wordmark (Gradio /home): pinned top-left; no transition (reduces paint flicker) */
537
+ gradio-app nav.svelte-ti537g a[href*="/home"],
538
+ gradio-app .nav-holder nav a[href*="/home"],
539
+ gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"] {{
540
+ position: absolute !important;
541
+ left: 20px !important;
542
+ top: 0 !important;
543
+ bottom: 0 !important;
544
+ margin: auto 0 !important;
545
+ z-index: 2 !important;
546
+ display: inline-flex !important;
547
+ align-items: center !important;
548
  font-size: 0 !important;
549
+ line-height: 0 !important;
550
  text-indent: -9999px;
551
+ color: transparent !important;
552
+ overflow: hidden !important;
553
+ width: min(133px, 42vw) !important;
554
+ min-width: 80px !important;
555
+ min-height: 22px !important;
556
+ max-width: 133px !important;
557
+ height: 22px !important;
558
+ max-height: 22px !important;
559
+ box-sizing: content-box !important;
560
+ padding: 6px 12px 6px 12px !important;
561
+ flex: 0 0 auto !important;
562
+ flex-shrink: 0 !important;
563
+ background-color: transparent !important;
564
+ background-image: url("{_oh_nav_data_uri_light}") !important;
565
  background-size: contain !important;
566
  background-repeat: no-repeat !important;
567
+ background-position: left center !important;
 
 
 
 
568
  border: none !important;
569
+ box-shadow: none !important;
570
+ border-radius: 6px !important;
571
+ transition: none !important;
572
  }}
573
+ /* Dark top bar: light-colored wordmark */
574
+ html.dark gradio-app nav.svelte-ti537g a[href*="/home"],
575
+ html.dark gradio-app .nav-holder nav a[href*="/home"],
576
+ html.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"],
577
+ body.dark gradio-app nav.svelte-ti537g a[href*="/home"],
578
+ body.dark gradio-app .nav-holder nav a[href*="/home"],
579
+ body.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"],
580
+ html:has([class*="gradio-container-"].dark) gradio-app nav.svelte-ti537g a[href*="/home"],
581
+ html:has([class*="gradio-container-"].dark) gradio-app .nav-holder nav a[href*="/home"],
582
+ html:has([class*="gradio-container-"].dark) gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"],
583
+ .gradio-container.dark gradio-app nav.svelte-ti537g a[href*="/home"],
584
+ .gradio-container.dark gradio-app .nav-holder nav a[href*="/home"],
585
+ .gradio-container.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"],
586
+ [class*="gradio-container-"].dark gradio-app nav.svelte-ti537g a[href*="/home"],
587
+ [class*="gradio-container-"].dark gradio-app .nav-holder nav a[href*="/home"],
588
+ [class*="gradio-container-"].dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"] {{
589
+ background-image: url("{_oh_nav_data_uri_dark}") !important;
590
+ }}
591
+ /* Home wordmark: no grey hover/focus chip */
592
+ gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:hover,
593
+ gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:focus-visible,
594
+ gradio-app nav.svelte-ti537g a[href*="/home"]:hover,
595
+ html.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:hover,
596
+ html.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:focus-visible,
597
+ html.dark gradio-app nav.svelte-ti537g a[href*="/home"]:hover,
598
+ body.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:hover,
599
+ body.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:focus-visible,
600
+ body.dark gradio-app nav.svelte-ti537g a[href*="/home"]:hover,
601
+ html:has([class*="gradio-container-"].dark) gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:hover,
602
+ html:has([class*="gradio-container-"].dark) gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:focus-visible,
603
+ html:has([class*="gradio-container-"].dark) gradio-app nav.svelte-ti537g a[href*="/home"]:hover,
604
+ .gradio-container.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:hover,
605
+ .gradio-container.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:focus-visible,
606
+ .gradio-container.dark gradio-app nav.svelte-ti537g a[href*="/home"]:hover,
607
+ [class*="gradio-container-"].dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:hover,
608
+ [class*="gradio-container-"].dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:focus-visible,
609
+ [class*="gradio-container-"].dark gradio-app nav.svelte-ti537g a[href*="/home"]:hover {{
610
+ background-color: transparent !important;
611
+ }}
612
+ @media (max-width: 768px) {{
613
+ gradio-app nav.svelte-ti537g a[href*="/home"],
614
+ gradio-app .nav-holder nav a[href*="/home"],
615
+ gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"] {{
616
+ left: 20px !important;
617
+ }}
618
+ gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:hover,
619
+ gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"]:focus-visible,
620
+ gradio-app nav.svelte-ti537g a[href*="/home"]:hover {{
621
+ left: 20px !important;
622
+ }}
623
+ }}
624
+ /* Active Home (wordmark — route .active) */
625
+ gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].active,
626
+ gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].svelte-ti537g.active {{
627
+ color: transparent !important;
628
+ left: 20px !important;
629
+ top: 0 !important;
630
+ bottom: 0 !important;
631
+ margin: auto 0 !important;
632
+ background-color: #e4e4e7 !important;
633
+ background-image: url("{_oh_nav_data_uri_light}") !important;
634
+ background-size: contain !important;
635
+ background-repeat: no-repeat !important;
636
+ background-position: left center !important;
637
+ border-color: transparent !important;
638
+ box-shadow: none !important;
639
+ }}
640
+ html.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].active,
641
+ html.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].svelte-ti537g.active,
642
+ body.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].active,
643
+ body.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].svelte-ti537g.active,
644
+ html:has([class*="gradio-container-"].dark) gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].active,
645
+ html:has([class*="gradio-container-"].dark) gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].svelte-ti537g.active,
646
+ .gradio-container.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].active,
647
+ .gradio-container.dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].svelte-ti537g.active,
648
+ [class*="gradio-container-"].dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].active,
649
+ [class*="gradio-container-"].dark gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].svelte-ti537g.active {{
650
+ background-color: hsl(0 0% 12% / 0.95) !important;
651
+ background-image: url("{_oh_nav_data_uri_dark}") !important;
652
+ }}
653
+ @media (max-width: 768px) {{
654
+ gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].active,
655
+ gradio-app nav.oh-top-nav a.oh-nav-link[href*="/home"].svelte-ti537g.active {{
656
+ left: 20px !important;
657
+ }}
658
+ }}
659
+ /* Markdown `---` → <hr>. var(--oh-border) = light #e4e4e7 / dark #242424 (content.py). */
660
+ gradio-app hr, .gradio-container hr, [class*="gradio-container-"] hr {{
661
+ box-sizing: border-box !important;
662
+ border: 0 !important;
663
+ border-top: 1px solid var(--oh-border) !important;
664
+ color: var(--oh-border) !important;
665
+ background: transparent !important;
666
+ background-color: transparent !important;
667
+ height: 0 !important;
668
+ opacity: 1 !important;
669
+ }}
670
+ /* Multipage route chips: Gradio 5.30 index bundle sets
671
+ a.active.svelte-ti537g {{ color: var(--body-text-color); background: var(--block-background-fill) }}.
672
+ This block is last in final_css; typography matches route chip rules in content.py (13px) for every tab including active. */
673
+ gradio-app nav a.svelte-ti537g.active:not([href*="/home"]) {{
674
+ color: #fafafa !important;
675
+ background-color: #18181b !important;
676
+ border-color: transparent !important;
677
+ box-shadow: none !important;
678
+ transition: none !important;
679
+ font-size: 13px !important;
680
+ line-height: 1.4 !important;
681
+ font-weight: 400 !important;
682
+ font-family: var(--oh-font-sans) !important;
683
+ }}
684
+ html.dark gradio-app nav a.svelte-ti537g.active:not([href*="/home"]),
685
+ body.dark gradio-app nav a.svelte-ti537g.active:not([href*="/home"]),
686
+ html:has([class*="gradio-container-"].dark) gradio-app nav a.svelte-ti537g.active:not([href*="/home"]),
687
+ .gradio-container.dark gradio-app nav a.svelte-ti537g.active:not([href*="/home"]),
688
+ [class*="gradio-container-"].dark gradio-app nav a.svelte-ti537g.active:not([href*="/home"]) {{
689
+ color: #ffffff !important;
690
+ background-color: hsl(0 0% 12% / 0.95) !important;
691
+ border-color: transparent !important;
692
+ box-shadow: none !important;
693
+ transition: none !important;
694
+ font-size: 13px !important;
695
+ line-height: 1.4 !important;
696
+ font-weight: 400 !important;
697
+ font-family: var(--oh-font-sans) !important;
698
  }}
699
  """
700
+ )
701
  # --- Gradio App Definition ---
702
  logger.info("Creating Gradio application")
703
  demo = gr.Blocks(
704
  theme=theme,
705
  css=final_css,
706
+ head=DESIGN_FONTS_LINK
707
+ + NAV_LOGO_PRELOAD
708
+ + posthog_script
709
+ + scroll_script
710
+ + redirect_script
711
+ + oh_top_nav_script
712
+ + fix_nav_links_script
713
+ + tooltip_script
714
+ + dark_mode_script,
715
  title="OpenHands Index",
716
  )
717
 
 
783
  # Respect platform port/host if provided (e.g., OpenHands runtime)
784
  port = int(os.environ.get("PORT", os.environ.get("GRADIO_SERVER_PORT", 7860)))
785
  host = os.environ.get("HOST", os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"))
786
+ # Auto-reload: set RELOAD=1 or UVICORN_RELOAD=1 to restart on .py changes (CSS in content.py, etc.)
787
+ _reload = os.environ.get("UVICORN_RELOAD", os.environ.get("RELOAD", "")).lower() in (
788
+ "1",
789
+ "true",
790
+ "yes",
791
+ )
792
+ logger.info(f"Launching app on {host}:{port}" + (" (auto-reload on .py changes)" if _reload else ""))
793
+ if _reload:
794
+ uvicorn.run("app:app", host=host, port=port, reload=True)
795
+ else:
796
+ uvicorn.run(app, host=host, port=port)
797
  logger.info("App launched successfully")
798