| """ |
| Base App Class for consistent app structure across the ecosystem. |
| |
| Provides a base class that handles: |
| - Multi-page routing with gr.Blocks.route() |
| - Consistent layout with navbar and footer |
| - Cross-app navigation |
| """ |
|
|
| from abc import ABC, abstractmethod |
| from typing import Callable |
| import gradio as gr |
|
|
| |
| try: |
| from config.app_registry import get_app_info |
| from apps.shared.ui_components import get_navbar_links, create_footer |
| except ImportError: |
| from config.app_registry import get_app_info |
| from shared.ui_components import get_navbar_links, create_footer |
|
|
|
|
| class AppBase(ABC): |
| """ |
| Base class for all apps in the ecosystem. |
| |
| Provides multi-page routing with consistent layout including |
| navbar (cross-app navigation) and footer. |
| """ |
|
|
| def __init__(self, app_id: str, title: str, description: str | None = None): |
| """ |
| Initialize the app. |
| |
| Args: |
| app_id: Unique identifier matching registry key |
| title: Display title for the app |
| description: Optional description shown in header |
| """ |
| self.app_id = app_id |
| self.title = title |
| self.description = description |
| self._pages: list[tuple[str, str, Callable]] = [] |
|
|
| |
| app_info = get_app_info(app_id) |
| if not app_info: |
| raise ValueError(f"App '{app_id}' not found in registry") |
|
|
| self.app_info = app_info |
|
|
| def add_page(self, name: str, path: str, build_fn: Callable[[], None]) -> None: |
| """ |
| Register a page for this app. |
| |
| Args: |
| name: Display name for the page (shown in navbar) |
| path: URL path for the page (e.g., "/", "/settings") |
| build_fn: Function that builds the page content |
| """ |
| self._pages.append((name, path, build_fn)) |
|
|
| def get_page_list(self) -> list[tuple[str, str]]: |
| """Get list of (name, path) for all pages.""" |
| return [(name, path) for name, path, _ in self._pages] |
|
|
| def _build_page_with_layout(self, build_fn: Callable, navbar_links: list) -> None: |
| """ |
| Build a page wrapped in the standard layout. |
| |
| Args: |
| build_fn: Function that builds the page content |
| navbar_links: Links for the navbar |
| """ |
| |
| gr.Navbar( |
| value=navbar_links, |
| main_page_name=self.title, |
| ) |
|
|
| |
| if self.description: |
| gr.Markdown(f"*{self.description}*") |
|
|
| |
| build_fn() |
|
|
| |
| create_footer() |
|
|
| @abstractmethod |
| def register_pages(self) -> None: |
| """ |
| Register all pages for this app. |
| |
| Subclasses must implement this method to call add_page() |
| for each page they want to include. |
| |
| Example: |
| def register_pages(self): |
| self.add_page("Home", "/", self.build_home) |
| self.add_page("Settings", "/settings", self.build_settings) |
| """ |
| pass |
|
|
| def build(self) -> gr.Blocks: |
| """ |
| Build the complete Gradio interface with all pages. |
| |
| Returns: |
| gr.Blocks instance with multi-page routing |
| """ |
| |
| self.register_pages() |
|
|
| if not self._pages: |
| raise ValueError("No pages registered. Call add_page() in register_pages().") |
|
|
| |
| navbar_links = get_navbar_links(self.app_id) |
|
|
| |
| main_name, main_path, main_build_fn = self._pages[0] |
|
|
| with gr.Blocks(title=self.title) as demo: |
| self._build_page_with_layout(main_build_fn, navbar_links) |
|
|
| |
| for name, path, build_fn in self._pages[1:]: |
| with demo.route(name, path): |
| self._build_page_with_layout(build_fn, navbar_links) |
|
|
| return demo |
|
|
| def launch(self, **kwargs) -> None: |
| """ |
| Build and launch the app. |
| |
| Args: |
| **kwargs: Arguments passed to gr.Blocks.launch() |
| """ |
| demo = self.build() |
| demo.launch(**kwargs) |
|
|