Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
04aabfa
feat: add new routes to GuiRoutes
KristijanArmeni Feb 8, 2026
5972394
Merge remote-tracking branch 'upstream/gui-prototype' into gui-prototype
KristijanArmeni Feb 11, 2026
7c5e2a3
fix: make self.back_text optional in GuiPage
KristijanArmeni Feb 11, 2026
f9505c4
Merge remote-tracking branch 'upstream/gui-prototype' into gui-prototype
KristijanArmeni Feb 17, 2026
0296044
add RunAnalysisPage and register it in main_workflow.py
KristijanArmeni Feb 17, 2026
2401a81
add navigation in analysis_params.py
KristijanArmeni Feb 17, 2026
a81ec76
chore: rename ConfigureAnalsys to ConfigureAnalsysiDatasetPage
KristijanArmeni Feb 17, 2026
3c8dd1f
rename analysis_configure.py -> analysis_configure_dataset.py
KristijanArmeni Feb 17, 2026
13abc07
rename analysis_configure_dataset.py -> analysis_dataset.py
KristijanArmeni Feb 17, 2026
cf8e083
update import statements
KristijanArmeni Feb 17, 2026
6abc8a4
fix: imports in main_workflow.py and analyzer_new.py
KristijanArmeni Feb 17, 2026
384e941
fix routing variable names
KristijanArmeni Feb 17, 2026
f1ade8c
feat: update RunAnalysisPage with progress bars and buttons
KristijanArmeni Feb 17, 2026
355394b
feat: disable cancel button after error make return button visible
KristijanArmeni Feb 17, 2026
a8c2747
use nonlocal var for cancel_request tracking
KristijanArmeni Feb 17, 2026
339e6eb
make reuturn and success buttons visible after analysis is run
KristijanArmeni Feb 17, 2026
17af952
dev: use `USE_RENDER_DELAY` constant var
KristijanArmeni Feb 17, 2026
e8cddd1
ux: make return_btn visible after successful analysis
KristijanArmeni Feb 17, 2026
31f3915
feat: wrap analysis run into ui.dialog, initial attempt
KristijanArmeni Feb 25, 2026
17d5bd9
remove retrun button for now
KristijanArmeni Feb 25, 2026
1848cd5
fix routes, button positioning
KristijanArmeni Feb 25, 2026
b549c37
add PostAnalysisPage and add to workflow
KristijanArmeni Feb 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions gui/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ class GuiRoutes(BaseModel):
select_analyzer_fork: str = "/select_analyzer_fork"
select_analyzer: str = "/select_analyzer"
select_previous_analyzer: str = "/select_previous_analyzer"
configure_analysis: str = "/configure_analysis"
configure_analysis_dataset: str = "/configure_analysis_dataset"
configure_analysis_parameters: str = "/configure_analysis_parameters"
preview_dataset: str = "/preview_dataset"
run_analysis: str = "/run_analysis"
analysis_options: str = "/analysis_options"


class GuiColors(BaseModel):
Expand Down Expand Up @@ -297,13 +299,16 @@ def _render_header(self) -> None:
with ui.row().classes("w-full items-center justify-between"):
# Left: Back button or spacer
with ui.element("div").classes("flex items-center"):
if self.show_back_button and self.back_route and self.back_text:
ui.button(
text=self.back_text,
icon=self.back_icon,
color="accent",
on_click=self._handle_back_click,
).props("flat")
if self.show_back_button and self.back_route:
# Build button parameters conditionally
btn_kwargs = {
"icon": self.back_icon,
"color": "accent",
"on_click": self._handle_back_click,
}
if self.back_text:
btn_kwargs["text"] = self.back_text
ui.button(**btn_kwargs).props("flat")

# Center: Title
ui.label(self.title).classes("text-h6")
Expand Down
21 changes: 15 additions & 6 deletions gui/main_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
from gui.context import GUIContext
from gui.pages import (
ConfigureAnalaysisParams,
ConfigureAnalysis,
ConfigureAnalysisDatasetPage,
ImportDatasetPage,
NewProjectPage,
PreviewDatasetPage,
RunAnalysisPage,
SelectAnalyzerForkPage,
SelectNewAnalyzerPage,
SelectPreviousAnalyzerPage,
Expand Down Expand Up @@ -81,20 +82,28 @@ def select_previous_analyzer():
page = SelectPreviousAnalyzerPage(session=gui_session)
page.render()

@ui.page(gui_routes.configure_analysis)
def configure_analysis():
page = ConfigureAnalysis(session=gui_session)
@ui.page(gui_routes.configure_analysis_dataset)
def configure_analysis_dataset():
"""Renders page where user selects dataset columns and previews."""
page = ConfigureAnalysisDatasetPage(session=gui_session)
page.render()

@ui.page(gui_routes.configure_analysis_parameters)
def configure_analysis_parameters():
"""Render page to allow user to configure analysis parameters."""
page = ConfigureAnalaysisParams(session=gui_session)
page.render()

@ui.page(gui_routes.run_analysis)
def run_analysis():
"""Render page that runs the analysis with selected parameters."""
page = RunAnalysisPage(session=gui_session)
page.render()

# Launch in native mode
ui.run(
native=False,
window_size=(800, 600),
native=True,
fullscreen=True,
title="CIB Mango Tree",
favicon="🥭",
reload=False,
Expand Down
6 changes: 4 additions & 2 deletions gui/pages/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .analysis_configure import ConfigureAnalysis
from .analysis_dataset import ConfigureAnalysisDatasetPage
from .analysis_params import ConfigureAnalaysisParams
from .analysis_run import RunAnalysisPage
from .analyzer_new import SelectNewAnalyzerPage
from .analyzer_previous import SelectPreviousAnalyzerPage
from .analyzer_select import SelectAnalyzerForkPage
Expand All @@ -17,7 +18,8 @@
"SelectAnalyzerForkPage",
"SelectNewAnalyzerPage",
"SelectPreviousAnalyzerPage",
"ConfigureAnalysis",
"ConfigureAnalysisDatasetPage",
"ConfigureAnalaysisParams",
"RunAnalysisPage",
"PreviewDatasetPage",
]
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
from gui.base import GuiPage, GuiSession, gui_routes


class ConfigureAnalysis(GuiPage):
class ConfigureAnalysisDatasetPage(GuiPage):
def __init__(self, session: GuiSession):
config_analysis_title: str = "Configure Analysis"
super().__init__(
session=session,
route=gui_routes.configure_analysis,
route=gui_routes.configure_analysis_dataset,
title=(
f"{session.current_project.display_name}: {config_analysis_title}"
if session.current_project is not None
Expand Down
6 changes: 3 additions & 3 deletions gui/pages/analysis_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def __init__(self, session: GuiSession):
route=gui_routes.configure_analysis_parameters,
title=f"{session.current_project.display_name}: Configure Parameters",
show_back_button=True,
back_route=gui_routes.configure_analysis,
back_route=gui_routes.configure_analysis_dataset,
show_footer=True,
)

Expand All @@ -27,7 +27,7 @@ def render_content(self):

if not self.session.column_mapping:
self.notify_warning("Column mapping not configured. Redirecting...")
self.navigate_to(gui_routes.configure_analysis)
self.navigate_to(gui_routes.configure_analysis_dataset)
return

analyzer = self.session.selected_analyzer
Expand Down Expand Up @@ -106,7 +106,7 @@ def _on_proceed():
self.session.analysis_params = final_params

# TODO: Navigate to next step (run analysis or review page)
self.notify_success("Coming soon!")
self.navigate_to(gui_routes.run_analysis)

ui.button(
"Proceed to Run Analysis",
Expand Down
208 changes: 208 additions & 0 deletions gui/pages/analysis_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import asyncio
from traceback import format_exc

from nicegui import ui

from gui.base import GuiPage, GuiSession, gui_routes

# Small delay to ensure UI elements are fully rendered before starting analysis
UI_RENDER_DELAY = 0.1


class RunAnalysisPage(GuiPage):
"""
Page that executes the analysis and shows progress.

After parameter configuration, this page:
1. Creates an AnalysisContext using project.create_analysis()
2. Runs the analysis via analysis.run() generator
3. Shows progress and status updates
4. Handles errors and cleanup of draft analyses
5. On success, navigates to AnalysisOptionsPage
"""

def __init__(self, session: GuiSession):
super().__init__(
session=session,
route=gui_routes.run_analysis,
title=f"{session.current_project.display_name}: Running Analysis",
show_back_button=False, # Prevent navigation during analysis
show_footer=True,
)

def render_content(self) -> None:
"""Render the analysis execution page with progress tracking."""
# Validate required session data
if not self.session.selected_analyzer:
self.notify_warning("No analyzer selected. Redirecting...")
self.navigate_to(gui_routes.select_analyzer)
return

if not self.session.column_mapping:
self.notify_warning("Column mapping not configured. Redirecting...")
self.navigate_to(gui_routes.configure_analysis)
return

if self.session.analysis_params is None:
self.notify_warning("Parameters not configured. Redirecting...")
self.navigate_to(gui_routes.configure_analysis_parameters)
return

analyzer = self.session.selected_analyzer
project = self.session.current_project

# Create the analysis context
try:
analysis = project.create_analysis(
analyzer.id,
self.session.column_mapping,
self.session.analysis_params,
)
except Exception as e:
self.notify_error(f"Failed to create analysis: {str(e)}")
print(f"Analysis creation error:\n{format_exc()}")
self.navigate_to(gui_routes.configure_analysis_parameters)
return

# Calculate total steps for progress tracking
secondary_analyzers = (
self.session.app.context.suite.find_toposorted_secondary_analyzers(analyzer)
)
total_steps = 1 + len(secondary_analyzers) # primary + secondaries

# State tracking for cancellation
cancel_requested = False

def request_cancel():
"""Handle cancel button click."""
nonlocal cancel_requested
cancel_requested = True
cancel_btn.text = "Canceling..."
cancel_btn.disable()

# Main content area
with (
ui.column()
.classes("items-center justify-center gap-6")
.style("width: 50%; max-width: 800px; margin: 0 auto; height: 80vh;")
):
# Progress bar (value from 0.0 to 1.0)
progress_bar = ui.linear_progress(value=0).classes("w-full")

# Create checkboxes for each analysis step (read-only, stacked vertically)
step_checkboxes = []
with ui.column().classes("w-full gap-1"):
# Primary analyzer checkbox
primary_checkbox = ui.checkbox(analyzer.name, value=False).props(
"disable"
)
step_checkboxes.append(primary_checkbox)

# Secondary analyzer checkboxes
for secondary in secondary_analyzers:
checkbox = ui.checkbox(secondary.name, value=False).props("disable")
step_checkboxes.append(checkbox)

# Log container (plain, no card wrapper)
log_container = ui.column().classes("w-full gap-1")

# Buttons container
buttons_container = ui.row().classes("gap-4")
with buttons_container:
# Cancel button
cancel_btn = ui.button(
"Cancel Analysis",
icon="stop",
color="secondary",
on_click=request_cancel,
).props("outline")

# Return button for errors (initially hidden)
return_btn = ui.button(
"Return to Parameters",
icon="arrow_back",
color="secondary",
on_click=lambda: self.navigate_to(
gui_routes.configure_analysis_parameters
),
)
return_btn.set_visibility(False)

# Success button (initially hidden)
success_btn = ui.button(
"Continue",
icon="arrow_forward",
color="primary",
on_click=lambda: self.navigate_to(gui_routes.analysis_options),
)
success_btn.set_visibility(False)

# Run analysis asynchronously
async def run_analysis_task():
nonlocal cancel_requested
current_step = 0

try:
for event in analysis.run():
# Check for cancellation
if cancel_requested:
with log_container:
ui.label("Analysis canceled by user").classes(
"text-warning text-sm"
)
raise KeyboardInterrupt("User canceled analysis")

if event.event == "start":
current_step += 1

# Update progress bar value (0 to 1)
progress_bar.value = current_step / total_steps

elif event.event == "finish":
# Check the corresponding checkbox
if current_step > 0 and current_step <= len(step_checkboxes):
step_checkboxes[current_step - 1].value = True

# Allow UI to update
await asyncio.sleep(0.01)

# Store completed analysis in session
self.session.current_analysis = analysis

# Update buttons
success_btn.set_visibility(True)
return_btn.set_visibility(True)
cancel_btn.disable()

self.notify_success("Analysis completed!")

except KeyboardInterrupt:
# User canceled
self.notify_warning("Analysis was canceled")

# Update buttons
cancel_btn.disable()
return_btn.set_visibility(True)

except Exception as e:
# Error occurred
self.notify_error(f"Analysis error: {str(e)}")

with log_container:
ui.label(f"Error: {str(e)}").classes(
"text-negative font-bold text-sm"
)

print(f"Analysis error:\n{format_exc()}")

# Update buttons
cancel_btn.disable()
return_btn.set_visibility(True)

finally:
# Cleanup: delete draft analysis if not completed
if analysis.is_draft:
analysis.delete()

# Start the analysis task after a short delay to allow UI to render
ui.timer(UI_RENDER_DELAY, run_analysis_task, once=True)
2 changes: 1 addition & 1 deletion gui/pages/analyzer_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def _on_proceed():
self.session.selected_analyzer = selected_analyzer
self.session.selected_analyzer_name = new_selection

self.navigate_to(gui_routes.configure_analysis)
self.navigate_to(gui_routes.configure_analysis_dataset)

ui.button(
"Configure Analysis",
Expand Down
Loading