"""Streamlit runtime helpers for long-running jobs on Hugging Face Spaces.""" from __future__ import annotations import os import time from typing import Any, Callable, Optional def is_huggingface_space() -> bool: """Detect Hugging Face Spaces runtime.""" return bool( os.getenv("SPACE_ID") or os.getenv("HF_HOME") or os.getenv("SYSTEM") ) def embed_html(content: str, height: int = 0) -> Any: """Render raw HTML/JS in an iframe. Uses ``st.iframe`` (Streamlit >= 1.5x) and falls back to the deprecated ``st.components.v1.html`` on older versions. ``st.iframe`` rejects a height of 0, so script-only snippets get a 1px frame instead. """ import streamlit as st if hasattr(st, "iframe"): size = height if isinstance(height, int) and height > 0 else 1 return st.iframe(content, height=size) from streamlit.components.v1 import html as _components_html return _components_html(content, height=height) class AnalysisProgressReporter: """Keeps the Streamlit websocket alive during long Gemini calls.""" def __init__( self, progress_bar: Any = None, status_text: Any = None, time_estimate: Any = None, step_indicator: Any = None, working_indicator: Any = None, stream_indicator: Any = None, base_progress: float = 70.0, progress_span: float = 25.0, ): self.progress_bar = progress_bar self.status_text = status_text self.time_estimate = time_estimate self.step_indicator = step_indicator self.working_indicator = working_indicator self.stream_indicator = stream_indicator self.base_progress = base_progress self.progress_span = progress_span self._last_flush = 0.0 self._total_steps = 1 def on_phase(self, message: str, fraction: float = 0.0) -> None: self._update( progress=self.base_progress + (self.progress_span * fraction), status=message, working=message, ) def on_step_start(self, step_number: int, step_title: str, total_steps: int) -> None: self._total_steps = max(total_steps, 1) fraction = max(0.0, (step_number - 1) / self._total_steps) self.on_phase( f"🤖 **AI Step {step_number}/{total_steps}:** {step_title}...", fraction=fraction, ) def on_stream_chunk(self, step_number: int, chars_received: int) -> None: now = time.time() if now - self._last_flush < 0.75: return self._last_flush = now fraction = min(0.99, ((step_number - 1) + min(chars_received / 4000.0, 0.95)) / self._total_steps) progress = int(self.base_progress + (self.progress_span * fraction)) stream_msg = f"Receiving AI output... {chars_received:,} characters" self._update( progress=progress, status=f"🤖 **AI Step {step_number}/{self._total_steps}:** {stream_msg}", working=stream_msg, stream=stream_msg, ) def on_step_complete(self, step_number: int) -> None: fraction = min(1.0, step_number / self._total_steps) self.on_phase( f"✅ **AI Step {step_number}/{self._total_steps}** complete", fraction=fraction, ) def _update( self, progress: float, status: str, working: Optional[str] = None, stream: Optional[str] = None, ) -> None: try: if self.progress_bar is not None: self.progress_bar.progress(min(max(int(progress), 0), 100)) if self.status_text is not None: self.status_text.markdown(status) if self.working_indicator is not None and working: self.working_indicator.markdown(f"🔄 **Processing:** {working}") if self.stream_indicator is not None and stream: self.stream_indicator.caption(stream) if self.time_estimate is not None: self.time_estimate.markdown( "⏱️ Analysis is still running — keep this tab open." ) if self.step_indicator is not None: self.step_indicator.markdown("📋 AI analysis in progress") except Exception: pass def make_progress_callback( progress_bar: Any = None, status_text: Any = None, time_estimate: Any = None, step_indicator: Any = None, working_indicator: Any = None, stream_indicator: Any = None, ) -> AnalysisProgressReporter: return AnalysisProgressReporter( progress_bar=progress_bar, status_text=status_text, time_estimate=time_estimate, step_indicator=step_indicator, working_indicator=working_indicator, stream_indicator=stream_indicator, ) def is_connection_abort_error(error: Exception | str) -> bool: message = str(error).lower() return any( token in message for token in ( "bodystreambuffer", "aborted", "connection reset", "connection interrupted", "client disconnected", "stream closed", ) )