"""DataTools โ€” Data Cleaning Mastery Suite. Launch: streamlit run src/gui/app.py This module is the navigation manager for the full GUI: it registers every tool page with ``st.navigation`` so the sidebar can render section headers ("Data Review", "Data Cleaners", "Transformations", "Automations") instead of the flat numbered list Streamlit's auto-page-discovery would produce. The Home page itself is registered as a callable defined below so the entry script remains the single file users invoke. """ from __future__ import annotations import sys from pathlib import Path import streamlit as st # Ensure project root is on sys.path so `src.core` imports work _project_root = Path(__file__).resolve().parent.parent.parent if str(_project_root) not in sys.path: sys.path.insert(0, str(_project_root)) # --------------------------------------------------------------------------- # Home page (rendered when the user selects the default nav entry) # --------------------------------------------------------------------------- def _home_page() -> None: """Render the home page โ€” multi-file upload + per-file analysis.""" from src.gui.components import hide_streamlit_chrome, render_findings_panel from src.gui.components._legacy import _run_analysis_on_upload from src.i18n import t st.set_page_config( page_title=t("home.page_title"), page_icon="๐Ÿงน", layout="wide", ) hide_streamlit_chrome() st.title(t("home.title")) st.caption(t("home.caption")) st.divider() st.markdown(f"### {t('upload.heading')}") st.caption(t("upload.intro_multi")) uploaded_files = st.file_uploader( t("upload.uploader_label_multi"), type=["csv", "tsv", "xlsx", "xls"], accept_multiple_files=True, key="home_upload", help=t("upload.uploader_help"), ) if not uploaded_files: st.info(t("upload.empty_state")) return # Keep tool pages working: they consume a single ``home_uploaded_*`` # set via ``pickup_or_upload``. Expose the first uploaded file as # the "active" upload for that contract; the rest live alongside # for per-file analysis on this page. first = uploaded_files[0] if ( st.session_state.get("home_uploaded_name") != first.name or st.session_state.get("home_uploaded_size") != first.size ): st.session_state["home_uploaded_name"] = first.name st.session_state["home_uploaded_size"] = first.size st.session_state["home_uploaded_bytes"] = first.getvalue() # Per-file findings live in a dict so removing a file from the # uploader (Streamlit's "x" button) drops its results too. We only # re-analyze files we haven't already analyzed in this session. findings_by_file: dict = st.session_state.setdefault( "home_findings_by_file", {} ) current_names = {f.name for f in uploaded_files} findings_by_file = { name: result for name, result in findings_by_file.items() if name in current_names } st.session_state["home_findings_by_file"] = findings_by_file pending = [f for f in uploaded_files if f.name not in findings_by_file] col_run, col_clear, _ = st.columns([1, 1, 4]) with col_run: run_clicked = st.button( t("upload.run_button"), type="primary", key="home_run_analysis", disabled=not pending, use_container_width=True, ) with col_clear: clear_clicked = st.button( t("upload.clear_results"), key="home_clear_results", disabled=not findings_by_file, use_container_width=True, ) if clear_clicked: st.session_state["home_findings_by_file"] = {} st.rerun() if run_clicked: progress = st.progress(0.0, text=t("upload.scanning")) for i, f in enumerate(pending, start=1): findings_by_file[f.name] = _run_analysis_on_upload(f) progress.progress(i / len(pending), text=f"{f.name}") st.session_state["home_findings_by_file"] = findings_by_file progress.empty() st.rerun() if findings_by_file: st.divider() # Preserve uploader order so the user sees results in the same # order they appear in the file list above. for f in uploaded_files: if f.name not in findings_by_file: continue findings = findings_by_file[f.name] with st.container(border=True): if not findings: st.markdown(f"### ๐Ÿ“„ {f.name}") st.success(t("findings.none")) else: render_findings_panel(findings, header=f"๐Ÿ“„ {f.name}") st.divider() st.caption(t("chrome.footer")) # --------------------------------------------------------------------------- # Navigation registration # --------------------------------------------------------------------------- # # ``st.navigation`` overrides Streamlit's auto-discovery of the # ``pages/`` directory, so every page we want in the sidebar must be # listed here. The dict key becomes the section header rendered above # the entries; an empty-string key suppresses the header so the Home # entry sits at the top without a label above it. from src.gui.tools_registry import TOOLS, section_label # noqa: E402 from src.i18n import t as _t # noqa: E402 def _page_for(tool_id: str, *, page_slug: str, icon: str, title: str) -> "st.Page": return st.Page( f"pages/{page_slug}.py", title=title, icon=icon, url_path=tool_id, ) def _build_navigation() -> dict[str, list]: by_section: dict[str, list] = { "cleaners": [], "transformations": [], "automations": [], } for tool in TOOLS: by_section[tool.section].append( _page_for( tool.tool_id, page_slug=tool.page_slug, icon=tool.icon, title=tool.name, ) ) home = st.Page( _home_page, title=_t("nav.home_page_title") or "Home", icon="๐Ÿงน", default=True, url_path="home", ) activate = st.Page( "pages/_Activate.py", title=_t("nav.activate_title") or "Activate", icon="๐Ÿ”‘", url_path="activate", ) close = st.Page( "pages/99_Close.py", title=_t("nav.close_title") or "Close", icon="๐Ÿ›‘", url_path="close", ) account_header = _t("nav.section_account") or "Account" return { "": [home], section_label("cleaners"): by_section["cleaners"], section_label("transformations"): by_section["transformations"], section_label("automations"): by_section["automations"], account_header: [activate, close], } pg = st.navigation(_build_navigation()) pg.run()