diff --git a/src/aignostics/application/__init__.py b/src/aignostics/application/__init__.py index 3a043970..94ce029c 100644 --- a/src/aignostics/application/__init__.py +++ b/src/aignostics/application/__init__.py @@ -12,8 +12,13 @@ # advertise PageBuilder to enable auto-discovery if find_spec("nicegui"): + from ._gui._frame import _frame from ._gui._page_builder import PageBuilder + # Create public alias for the frame function + application_frame = _frame + __all__ += [ "PageBuilder", + "application_frame", ] diff --git a/src/aignostics/application/_gui/_page_index.py b/src/aignostics/application/_gui/_page_index.py index 1e0e5b92..7b0c74c1 100644 --- a/src/aignostics/application/_gui/_page_index.py +++ b/src/aignostics/application/_gui/_page_index.py @@ -20,11 +20,11 @@ async def _page_index(client: Client) -> None: with ui.row().classes("p-4 pt-2 pr-0"), ui.column(): await ui.context.client.connected() - ui.label("Welcome to the Aignostics Launchpad").bind_text_from( + ui.label("Hala walah to the Aignostics Launchpad").bind_text_from( app.storage.tab, "user_info", lambda user_info: ( - f"Welcome " + f"Hala walah " f"{user_info.profile.given_name if hasattr(user_info, 'profile') and user_info.profile else ''} " f"to the Aignostics Launchpad" ), @@ -65,6 +65,12 @@ async def _page_index(client: Client) -> None: ui.label("Analyze results in Python Notebooks?").classes("text-xl") ui.label('On completed runs click "Marimo" to directly open the notebook.').classes("text") + ui.label("Want to see how modules work?").classes("text-xl") + with ui.row().classes("text"): + ui.label("Check out our") + ui.link("Example Module", "/example").classes("text-blue-600 underline") + ui.label("to understand the SDK architecture.") + with ( ui.carousel(animated=True, arrows=True, navigation=True) .classes("flex-1 h-full m-0 p-0 self-end bg-[#423D6B] ") diff --git a/src/aignostics/example/__init__.py b/src/aignostics/example/__init__.py new file mode 100644 index 00000000..798d0e74 --- /dev/null +++ b/src/aignostics/example/__init__.py @@ -0,0 +1,19 @@ +"""Example module for learning purposes.""" + +from ._cli import cli +from ._service import Service + +__all__ = [ + "Service", + "cli", +] + +from importlib.util import find_spec + +# advertise PageBuilder to enable auto-discovery +if find_spec("nicegui"): + from ._gui._page_builder import PageBuilder + + __all__ += [ + "PageBuilder", + ] diff --git a/src/aignostics/example/_cli.py b/src/aignostics/example/_cli.py new file mode 100644 index 00000000..ffa088c9 --- /dev/null +++ b/src/aignostics/example/_cli.py @@ -0,0 +1,96 @@ +"""CLI of example module. + +This module demonstrates how to create CLI commands using Typer that integrate +with the main Aignostics CLI system. The CLI follows a modular pattern where +each module can register its own commands. + +Usage examples: + uv run aignostics example hello + uv run aignostics example hello "Your Name" + uv run aignostics example data + uv run aignostics example process "some text to process" +""" + +from typing import Annotated + +import typer + +from aignostics.utils import console, get_logger + +from ._service import Service + +logger = get_logger(__name__) + +# Create a Typer instance for this module's CLI commands +# - name="example": This becomes the subcommand name (aignostics example ...) +# - help: Shown when user runs "aignostics example --help" +# This CLI object is automatically discovered and registered with the main CLI +# through the module's __init__.py file which exports it in __all__ +cli = typer.Typer(name="example", help="Example module commands") + + +@cli.command() +def hello(name: Annotated[str, typer.Argument(help="Name to greet")] = "World") -> None: + """Say hello to someone. + + This is a simple command that demonstrates: + - How to use Typer's @cli.command() decorator to register a function as a CLI command + - How to use Annotated types for command arguments with help text + - How to provide default values for optional arguments + - How to use the console utility for colored output + + Usage: + uv run aignostics example hello # Uses default "World" + uv run aignostics example hello "Alice" # Custom name + + Args: + name (str): Name to greet. This is a positional argument with a default value. + """ + # Use the console utility from aignostics.utils for rich text output + # The [green] syntax is Rich markup for colored text + console.print(f"[green]Hello {name} from Example module![/green]") + + +@cli.command() +def data() -> None: + """Get example data. + + This command demonstrates: + - How to create a command with no arguments + - How to call service layer methods from CLI commands + - How to format and display structured data in the terminal + + Usage: + uv run aignostics example data + """ + # Call the service layer to get data - this follows the separation of concerns pattern + # where CLI commands are thin wrappers around business logic in the service layer + example_data = Service.get_example_data() + + # Display the data with formatting + console.print("[blue]Example Data:[/blue]") + for key, value in example_data.items(): + console.print(f" {key}: {value}") + + +@cli.command() +def process(text: Annotated[str, typer.Argument(help="Text to process")]) -> None: + """Process some text. + + This command demonstrates: + - How to use required positional arguments + - How to pass user input to service layer methods + - How to display processed results + + Usage: + uv run aignostics example process "Hello World" + uv run aignostics example process "Any text you want to process" + + Args: + text (str): Text to process. This is a required positional argument. + """ + # Process the text using the service layer + result = Service.process_example(text) + + # Display the result with yellow coloring + console.print(f"[yellow]{result}[/yellow]") diff --git a/src/aignostics/example/_gui/_page_builder.py b/src/aignostics/example/_gui/_page_builder.py new file mode 100644 index 00000000..960cff90 --- /dev/null +++ b/src/aignostics/example/_gui/_page_builder.py @@ -0,0 +1,19 @@ +"""GUI page builder for example module.""" + +from aignostics.utils import BasePageBuilder + + +class PageBuilder(BasePageBuilder): + """Page builder for example module.""" + + @staticmethod + def register_pages() -> None: + """Register example module pages.""" + from nicegui import ui # noqa: PLC0415 + + @ui.page("/example") + async def page_example() -> None: + """Example page.""" + from ._page_example import _page_example # noqa: PLC0415 + + await _page_example() diff --git a/src/aignostics/example/_gui/_page_example.py b/src/aignostics/example/_gui/_page_example.py new file mode 100644 index 00000000..442400d9 --- /dev/null +++ b/src/aignostics/example/_gui/_page_example.py @@ -0,0 +1,77 @@ +"""Example page for the example module.""" + +from nicegui import ui + +from aignostics.application import application_frame +from aignostics.example import Service +from aignostics.utils import get_logger + +logger = get_logger(__name__) + +# Constants +SECTION_HEADER_CLASSES = "text-2xl font-semibold mb-4" + + +async def _page_example() -> None: + """Example page displaying module functionality.""" + ui.page_title("Example Module") + + # Set client content styling to work with frame layout + ui.context.client.content.classes(remove="nicegui-content") + ui.context.client.content.classes(add="pl-5 pt-5") + + await application_frame( + navigation_title="🔬 Example Module", + navigation_icon="science", + navigation_icon_color="primary", + navigation_icon_tooltip="Example Module for learning SDK architecture", + left_sidebar=True, + ) + + with ui.row().classes("p-4 pt-2 pr-0"), ui.column().classes("w-full max-w-4xl"): + # Header + ui.label("🔬 Example Module").classes("text-4xl font-bold mb-4") + ui.label("This is a template module for learning the SDK architecture.").classes("text-xl text-gray-600 mb-8") + + # Example data section + with ui.card().classes("w-full mb-6"): + ui.label("📊 Example Data").classes(SECTION_HEADER_CLASSES) + example_data = Service.get_example_data() + + with ui.grid(columns=2).classes("w-full gap-4"): + for key, value in example_data.items(): + ui.label(f"{key.title()}:").classes("font-medium") + ui.label(value).classes("text-blue-600") + + # Interactive section + with ui.card().classes("w-full mb-6"): + ui.label("🛠️ Text Processing").classes(SECTION_HEADER_CLASSES) + + input_field = ui.input(label="Enter text to process", placeholder="Type something here...").classes( + "w-full mb-4" + ) + + result_area = ui.label("").classes("text-green-600 font-medium") + + def process_text() -> None: + """Process the input text and display result.""" + if input_field.value: + processed = Service.process_example(input_field.value) + result_area.text = processed + else: + result_area.text = "Please enter some text first!" + + ui.button("Process Text", on_click=process_text).classes("bg-blue-500 text-white") + + # Navigation section + with ui.card().classes("w-full"): + ui.label("🧭 Navigation").classes(SECTION_HEADER_CLASSES) + ui.label("This example module demonstrates:").classes("mb-2") + + with ui.column().classes("ml-4"): + ui.label("• Service layer with static methods") + ui.label("• CLI commands (try: uv run aignostics example --help)") + ui.label("• GUI page registration and routing") + ui.label("• Module auto-discovery via dependency injection") + + ui.button("← Back to Home", on_click=lambda: ui.navigate.to("/")).classes("mt-4") diff --git a/src/aignostics/example/_service.py b/src/aignostics/example/_service.py new file mode 100644 index 00000000..40069df8 --- /dev/null +++ b/src/aignostics/example/_service.py @@ -0,0 +1,35 @@ +"""Service of the example module.""" + +from aignostics.utils import BaseService, get_logger + +logger = get_logger(__name__) + + +class Service(BaseService): + """Example service for demonstration purposes.""" + + def __init__(self) -> None: + """Initialize the example service.""" + super().__init__() + logger.info("Example service initialized") + + @staticmethod + def get_example_data() -> dict[str, str]: + """Get some example data. + + Returns: + dict[str, str]: Example data dictionary. + """ + return {"message": "Hello from Example module!", "status": "active", "module": "example"} + + @staticmethod + def process_example(input_text: str) -> str: + """Process example input. + + Args: + input_text (str): Text to process. + + Returns: + str: Processed text. + """ + return f"Processed: {input_text}"