Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 28 additions & 6 deletions src/pr_analyzer/ui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
1. Page configuration
2. CSS injection
3. Session-state bootstrap (including the raw_prs tuple from the pipeline)
4. Sidebar rendering -> filter values + LLM toggle
4. Sidebar rendering filter values + LLM toggle
5. Filtering the active DataFrame (and optionally enriching via dev3+dev4)
6. Routing to the correct main-area view (empty state OR tabs)

Expand All @@ -18,12 +18,14 @@

import streamlit as st

# ── Page config (must be the very first Streamlit call) ───────────────────────
st.set_page_config(
page_title="GitAnalyzer",
page_icon="🧬",
layout="wide",
)

# ── Internal imports (after set_page_config) ──────────────────────────────────
from components.sidebar import render_sidebar # noqa: E402
from components.tabs import ( # noqa: E402
render_tab_dashboard,
Expand All @@ -33,15 +35,18 @@
from utils.constants import APP_NAME, APP_VERSION # noqa: E402
from utils.data import apply_filters, get_mock_data # noqa: E402
from utils.pipeline_bridge import ( # noqa: E402
CacheCounter,
enrich_prs,
enriched_to_dataframe,
)
from utils.styles import inject_css # noqa: E402

from pr_analyzer.llm.client import create_llm_client # noqa: E402

# ── CSS ───────────────────────────────────────────────────────────────────────
inject_css()

# ── Session-state bootstrap ───────────────────────────────────────────────────
if "file_loaded" not in st.session_state:
st.session_state.file_loaded = False
if "fname" not in st.session_state:
Expand All @@ -59,9 +64,13 @@
if "llm_enriched" not in st.session_state:
st.session_state.llm_enriched = False

sel_lang, sel_nature, cleaning, llm_tag, metrics = render_sidebar()
# ── Sidebar ───────────────────────────────────────────────────────────────────
sel_lang, sel_nature, sel_type, sel_clarity, cleaning, llm_tag, metrics = (
render_sidebar()
)


# ── LLM enrichment (TASK-39) ──────────────────────────────────────────────────
def _maybe_enrich() -> None:
"""Classifica PRs com o LLM configurado. Roda apenas uma vez por dataset carregado."""
if not llm_tag:
Expand Down Expand Up @@ -93,6 +102,7 @@ def _maybe_enrich() -> None:
st.error(f"Erro ao conectar ao {backend.upper()}: {exc}")
return

counter = CacheCounter()
enriched_list = []

with st.status(
Expand All @@ -101,25 +111,35 @@ def _maybe_enrich() -> None:
) as status:
st.caption(f"Modelo: `{model}`")
bar = st.progress(0.0)
for i, ep in enumerate(enrich_prs(prs, client=client), 1):
for i, ep in enumerate(enrich_prs(prs, client=client, cache=counter), 1):
enriched_list.append(ep)
bar.progress(i / n)
status.update(
label=f"✓ {n} PRs classificados com {backend.upper()}",
label=f"✓ {n} PRs classificados — {counter.cache_hits} do cache, {counter.calls_made} chamadas reais",
state="complete",
expanded=False,
)

st.session_state.df = enriched_to_dataframe(enriched_list)
st.session_state.llm_cache_stats = {"cache_hits": 0, "calls_made": n, "total": n}
st.session_state.llm_cache_stats = {
"cache_hits": counter.cache_hits,
"calls_made": counter.calls_made,
"total": counter.total,
}
st.session_state.llm_enriched = True


_maybe_enrich()

df = apply_filters(st.session_state.df, sel_lang, sel_nature)
# ── Filtered DataFrame (pure transform) ───────────────────────────────────────
df = apply_filters(st.session_state.df, sel_lang, sel_nature, sel_type, sel_clarity)

# ══════════════════════════════════════════════════════════════════════════════
# MAIN AREA
# ══════════════════════════════════════════════════════════════════════════════

if not st.session_state.file_loaded:
# ── Empty / landing state ─────────────────────────────────────────────────
st.markdown(
"""
<div class="empty-state">
Expand All @@ -140,6 +160,7 @@ def _maybe_enrich() -> None:
st.rerun()

else:
# ── Main tabs ─────────────────────────────────────────────────────────────
tab_dash, tab_explore, tab_export = st.tabs(["DASH", "EXPLORAR", "EXPORTAR"])

with tab_dash:
Expand All @@ -151,6 +172,7 @@ def _maybe_enrich() -> None:
with tab_export:
render_tab_export(df)

# ── Footer ────────────────────────────────────────────────────────────────────
st.markdown(
f"<br><p style='text-align:center;font-size:9px;color:#3f3f46;"
f"font-weight:900;letter-spacing:2px;'>"
Expand Down
110 changes: 110 additions & 0 deletions src/pr_analyzer/ui/components/charts.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,116 @@ def render_clarity_gauge(df: pd.DataFrame) -> None:
st.plotly_chart(fig, use_container_width=True, config=_chart_cfg())


def render_body_size_chart(df: pd.DataFrame) -> None:
"""Bar chart: avg description length (chars and words) grouped by a dimension.

US06 — visualizar distribuição de tamanho de descrição estratificada.
"""
_panel_header(
"📝 Tamanho de Descrição por Dimensão",
"linear-gradient(180deg,#34d399,#059669)",
)

if "chars" not in df.columns or len(df) == 0:
st.info(_NO_DATA_MSG)
return

dim: str = st.selectbox(
"Agrupar por",
["lang", "type", "nature"],
format_func=lambda x: {
"lang": "Linguagem",
"type": "Tipo",
"nature": "Natureza",
}[x],
key="body_size_dim",
label_visibility="collapsed",
)

if dim not in df.columns:
st.info(_NO_DATA_MSG)
return

agg = (
df[df[dim] != "—"]
.groupby(dim)[["chars", "words"]]
.mean()
.round(0)
.reset_index()
.sort_values("chars", ascending=False)
)

if agg.empty:
st.info(_NO_DATA_MSG)
return

fig = px.bar(
agg,
x=dim,
y=["chars", "words"],
barmode="group",
color_discrete_map={"chars": "#6366f1", "words": "#34d399"},
labels={dim: "", "value": "Média", "variable": "Métrica"},
)
fig.update_layout(
**PLOT_BASE,
legend={"orientation": "h", "y": 1.1, "x": 0},
yaxis=_axis_style(grid=True),
xaxis=_axis_style(grid=False),
)
st.plotly_chart(fig, use_container_width=True, config=_chart_cfg())


def render_clarity_cross_chart(df: pd.DataFrame) -> None:
"""Grouped bar: clarity distribution crossed with project type or nature.

US07 — visualizar relação entre clareza, tipo, natureza e linguagem.
"""
_panel_header("🔗 Clareza por Dimensão", "linear-gradient(180deg,#f472b6,#db2777)")

required = {"clarity", "type", "nature", "lang"}
if not required.issubset(df.columns) or len(df) == 0:
st.info(_NO_DATA_MSG)
return

dim: str = st.selectbox(
"Cruzar clareza com",
["type", "nature", "lang"],
format_func=lambda x: {
"type": "Tipo de Projeto",
"nature": "Natureza",
"lang": "Linguagem",
}[x],
key="clarity_cross_dim",
label_visibility="collapsed",
)

filtered = df[(df["clarity"] != "—") & (df[dim] != "—")]
if filtered.empty:
st.info(_NO_DATA_MSG)
return

counts = filtered.groupby([dim, "clarity"]).size().reset_index(name="count")

fig = px.bar(
counts,
x=dim,
y="count",
color="clarity",
barmode="group",
color_discrete_map=CLARITY_COLOR,
category_orders={"clarity": CLARITY_ORDER},
labels={dim: "", "count": "PRs", "clarity": "Clareza"},
)
fig.update_layout(
**PLOT_BASE,
legend={"orientation": "h", "y": 1.1, "x": 0},
yaxis=_axis_style(grid=True),
xaxis=_axis_style(grid=False),
)
st.plotly_chart(fig, use_container_width=True, config=_chart_cfg())


# ── Private helpers ──────────────────────────────────────────────────────────


Expand Down
Loading
Loading