import base64 from functools import lru_cache from pathlib import Path CSS_PATH = Path(__file__).resolve().parent.parent / "ui"/"assets" / "style.css" # Decorative app-wide background. A downscaled WebP keeps the inlined payload # small (~300 KB) while the original PNG stays as the master asset. FONDO_PATH = ( Path(__file__).resolve().parent.parent / "ui" / "assets" / "datathon_fondo_bg.webp" ) FONDO_OPACITY = 0.3 # Palette tokens — the colours themselves live in style.css (:root). Keeping # the token names here lets the inline status banners follow the theme. COLOR_ERROR = "var(--color-danger-bg)" COLOR_SUCCESS = "var(--color-success-bg)" COLOR_ON_ACCENT = "var(--color-white)" def is_error_message(message: str) -> bool: """True when a UI message is an error, whatever its display language. Participant lifecycle messages are produced through the i18n dictionary, whose ES errors start with "Error" and PT errors with "Erro". """ return message.startswith(("Error", "Erro")) @lru_cache(maxsize=1) def get_fondo_css() -> str: """CSS del fondo decorativo global (``datathon_fondo``). La imagen se embebe como data URI base64 (mismo criterio que el logo del sidebar) para no depender de una ruta servida estáticamente. Un pseudo-elemento ``::before`` porta la opacidad, ya que CSS no permite fijar la opacidad de un ``background-image`` directamente. El contenedor raíz de Gradio establece su propio contexto de apilamiento (``isolation``) para que el fondo quede por detrás del contenido. """ try: data = base64.b64encode(FONDO_PATH.read_bytes()).decode("ascii") except OSError: return "" return ( ".gradio-container{position:relative;isolation:isolate;}" # position:fixed (not background-attachment:fixed) so the background is # anchored to the viewport and cover is computed against it; works on # mobile browsers, where background-attachment:fixed is unreliable. ".gradio-container::before{" "content:'';position:fixed;inset:0;z-index:-1;" "background:url('data:image/webp;base64," f"{data}" "') center/cover no-repeat;" f"opacity:{FONDO_OPACITY};pointer-events:none;" "}" ) def get_css() -> str: """Devuelve el CSS global para inyectarlo en el de la app. Pasar esta cadena a ``gr.mount_gradio_app(css=...)`` hace que Gradio la aplique antes del primer render, evitando el flash de contenido sin estilo. """ base = CSS_PATH.read_text(encoding="utf-8") if CSS_PATH.exists() else "" return base + get_fondo_css() # Inria Sans is the brand typeface for the UI (see style.css, which maps the # theme's `--font` variable to it). The stylesheet is injected in the document # head through `gr.mount_gradio_app(head=...)`; the preconnect hints shave the # TLS handshake off the font request. # # `display=optional` (not `swap`): the web font is fetched asynchronously, so a # late arrival under `swap` re-wraps text after first paint. In the HF Space # iframe that made the header row (banner + login/language controls) reflow and # the login button jump; a manual resize recomputed the stale layout. With # `optional` the layout never waits for the font and never swaps after paint # (fallback on the first uncached load, Inria Sans on later cached loads), which # removes the layout shift. INRIA_SANS_URL = ( "https://fonts.googleapis.com/css2" "?family=Inria+Sans:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700" "&display=optional" ) def get_head() -> str: """Devuelve el HTML extra para el (Google Fonts de Inria Sans).""" return ( '' '' f'' ) def status_banner_html(message: str, is_error: bool) -> str: """Envuelve un mensaje de estado. Errores con fondo rojo, éxito con verde. La caja es cuadrada (sin radio) con borde negro de 1px. """ bg = COLOR_ERROR if is_error else COLOR_SUCCESS return ( f"
" f"{message}
" )