feat(gui): sidebar sections + non-technical tool labels

Sidebar nav now groups tools under Data Review / Data Cleaners /
Transformations / Automations via st.navigation, replacing the flat
auto-discovered list. Tool display names switch to action-first
phrasing (Find Duplicates, Fix Missing Values, Find Unusual Values,
Standardize Formats, Clean Text, Quality Check, Map Columns, Combine
Files, Automated Workflows) in EN + ES packs and on each page's H1.

The Data Cleaners section follows the requested order: Missing
Values → Outliers → Text Cleaner → Format Standardizer → Deduplicator
→ Quality Check. (Text Cleaner kept inside cleaners since the request
didn't list it but the tool still ships.) Registry now carries a
section field; helpers added: tools_in_section(), section_label().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 19:36:01 +00:00
parent 624f99653e
commit 93e43fc0d9
19 changed files with 356 additions and 199 deletions

View File

@@ -2,6 +2,14 @@
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
@@ -18,102 +26,186 @@ if str(_project_root) not in sys.path:
# ---------------------------------------------------------------------------
# Page config
# Home page (rendered when the user selects the default nav entry)
# ---------------------------------------------------------------------------
from src.gui.components import (
findings_count_for_tool,
hide_streamlit_chrome,
upload_and_analyze_section,
)
from src.i18n import t
def _home_page() -> None:
"""Render the home page — upload section + tool grid + footer."""
from src.gui.components import (
findings_count_for_tool,
hide_streamlit_chrome,
upload_and_analyze_section,
)
from src.gui.tools_registry import (
TOOLS,
section_label,
tool_description,
tool_name,
)
from src.i18n import t
from src.license import get_manager
st.set_page_config(
page_title=t("home.page_title"),
page_icon="🧹",
layout="wide",
)
st.set_page_config(
page_title=t("home.page_title"),
page_icon="🧹",
layout="wide",
)
hide_streamlit_chrome()
# ``hide_streamlit_chrome`` also renders the sidebar language selector,
# so every page that hides chrome picks up the same picker.
hide_streamlit_chrome()
st.title(t("home.title"))
st.caption(t("home.caption"))
st.divider()
upload_and_analyze_section()
st.divider()
# Per-tier feature set for badging — Ready tools that the user's
# tier doesn't unlock get a red "🔒 Locked" pill instead of the green
# "Ready" so they see at a glance which tools an upgrade would
# unlock. Dev-mode skips the lock check so the home grid renders the
# full SKU during development.
_license_state = get_manager().current_state()
_unlocked_features = (
set(_license_state.features) if _license_state.valid else set()
)
_dev_mode = get_manager().dev_mode
# Group tool cards by sidebar section so the home grid mirrors the
# left-nav layout — same vocabulary, same ordering.
sections: list[tuple[str, list]] = []
for section in ("review", "cleaners", "transformations", "automations"):
tools = [tool for tool in TOOLS if tool.section == section]
if not tools:
continue
sections.append((section_label(section), tools))
for header, tools in sections:
st.subheader(header)
for row_start in range(0, len(tools), 3):
cols = st.columns(3)
for i, col in enumerate(cols):
idx = row_start + i
if idx >= len(tools):
break
tool = tools[idx]
with col:
tool_locked = (
tool.status == "Ready"
and not _dev_mode
and tool.tool_id not in _unlocked_features
)
if tool_locked:
status_key = "license.status_locked"
status_color = "red"
elif tool.status == "Ready":
status_key = "status.ready"
status_color = "green"
else:
status_key = "status.coming_soon"
status_color = "orange"
badge = ""
n = findings_count_for_tool(tool.tool_id)
if n:
badge_key = (
"home.findings_badge_one"
if n == 1
else "home.findings_badge_other"
)
badge = f" :red-background[**{t(badge_key, n=n)}**]"
lock_glyph = "🔒 " if tool_locked else ""
st.markdown(
f"### {tool.icon} {tool_name(tool.tool_id)}{badge}\n\n"
f"{tool_description(tool.tool_id)}\n\n"
f":{status_color}[**{lock_glyph}{t(status_key)}**]"
)
st.divider()
st.caption(t("chrome.footer"))
# ---------------------------------------------------------------------------
# Home page
# 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.
st.title(t("home.title"))
st.caption(t("home.caption"))
from src.gui.tools_registry import TOOLS, section_label # noqa: E402
from src.i18n import t as _t # noqa: E402
st.divider()
# ---------------------------------------------------------------------------
# Upload & analyze (optional onboarding step)
# ---------------------------------------------------------------------------
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,
)
upload_and_analyze_section()
st.divider()
# ---------------------------------------------------------------------------
# Tool cards
# ---------------------------------------------------------------------------
from src.gui.tools_registry import TOOLS, tool_description, tool_name
from src.license import get_manager
# Per-tier feature set for badging — Ready tools that the user's
# tier doesn't unlock get a red "🔒 Locked" pill instead of the green
# "Ready" so they see at a glance which tools an upgrade would
# unlock. Dev-mode skips the lock check so the home grid renders the
# full SKU during development.
_license_state = get_manager().current_state()
_unlocked_features = set(_license_state.features) if _license_state.valid else set()
_dev_mode = get_manager().dev_mode
# Render tool cards in a 3-column grid. Cards picked up by the analyzer get a
# coloured "N findings" badge so the user can see at a glance which tools
# would help with the just-uploaded file.
for row_start in range(0, len(TOOLS), 3):
cols = st.columns(3)
for i, col in enumerate(cols):
idx = row_start + i
if idx >= len(TOOLS):
break
tool = TOOLS[idx]
with col:
tool_locked = (
tool.status == "Ready"
and not _dev_mode
and tool.tool_id not in _unlocked_features
def _build_navigation() -> dict[str, list]:
by_section: dict[str, list] = {
"review": [],
"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,
)
if tool_locked:
status_key = "license.status_locked"
status_color = "red"
elif tool.status == "Ready":
status_key = "status.ready"
status_color = "green"
else:
status_key = "status.coming_soon"
status_color = "orange"
)
badge = ""
n = findings_count_for_tool(tool.tool_id)
if n:
badge_key = "home.findings_badge_one" if n == 1 else "home.findings_badge_other"
badge = f" :red-background[**{t(badge_key, n=n)}**]"
lock_glyph = "🔒 " if tool_locked else ""
st.markdown(
f"### {tool.icon} {tool_name(tool.tool_id)}{badge}\n\n"
f"{tool_description(tool.tool_id)}\n\n"
f":{status_color}[**{lock_glyph}{t(status_key)}**]"
)
# The Review gate has no entry in the registry (it isn't a "tool")
# so register it by hand at the top of its section.
review_page = st.Page(
"pages/0_Review.py",
title=_t("nav.review_page_title") or "Review",
icon="🛡️",
url_path="review",
)
by_section["review"].insert(0, review_page)
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("review"): by_section["review"],
section_label("cleaners"): by_section["cleaners"],
section_label("transformations"): by_section["transformations"],
section_label("automations"): by_section["automations"],
account_header: [activate, close],
}
# ---------------------------------------------------------------------------
# Footer
# ---------------------------------------------------------------------------
st.divider()
st.caption(t("chrome.footer"))
pg = st.navigation(_build_navigation())
pg.run()