File size: 4,348 Bytes
c939d66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
"""
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

# Support both local and HF Spaces imports
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]] = []

        # Validate app exists in registry
        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
        """
        # Top navbar with cross-app navigation
        gr.Navbar(
            value=navbar_links,
            main_page_name=self.title,
        )

        # Header with description
        if self.description:
            gr.Markdown(f"*{self.description}*")

        # Main content area
        build_fn()

        # Footer
        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
        """
        # Register pages first
        self.register_pages()

        if not self._pages:
            raise ValueError("No pages registered. Call add_page() in register_pages().")

        # Get navbar links to other apps
        navbar_links = get_navbar_links(self.app_id)

        # Build main page (first registered page, typically "/")
        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)

        # Build additional pages with routes (outside the Blocks context)
        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)