Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -178,6 +178,39 @@ def mongo_get_likert_grouped(uri: str, db: str, coll: str, student: str, stage:
|
|
| 178 |
return {g: _norm_01(avg.get(g)) for g in SKILL_GROUPS.keys()}
|
| 179 |
except Exception:
|
| 180 |
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
# ------------------- UI -------------------
|
| 183 |
st.title("π Student Skill Radar")
|
|
@@ -199,6 +232,8 @@ with st.sidebar:
|
|
| 199 |
overlay_sources = st.toggle("Overlay all sources when '(All)' selected", value=False)
|
| 200 |
chart_title = st.text_input("Chart title", value="")
|
| 201 |
|
|
|
|
|
|
|
| 202 |
# start_str = start_dt.strftime("%Y-%m-%d") if isinstance(start_dt, date) else None
|
| 203 |
# end_str = end_dt.strftime("%Y-%m-%d") if isinstance(end_dt, date) else None
|
| 204 |
|
|
@@ -494,30 +529,107 @@ def fetch_student_stage_summary(
|
|
| 494 |
"notable_quotes": notable_quotes,
|
| 495 |
}
|
| 496 |
|
| 497 |
-
# ---------- Render the summary panel dynamically ----------
|
| 498 |
-
if mongo_uri and student_choice != "(All)" and source_choice != "(All)":
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
|
| 522 |
|
| 523 |
|
|
|
|
| 178 |
return {g: _norm_01(avg.get(g)) for g in SKILL_GROUPS.keys()}
|
| 179 |
except Exception:
|
| 180 |
return {}
|
| 181 |
+
|
| 182 |
+
# ---- Analyses (Markdown) helpers ----
|
| 183 |
+
ANALYSES_DIR = os.getenv("ANALYSES_DIR", "student_analyses") # folder in your HF Space
|
| 184 |
+
|
| 185 |
+
def _normalize_name(s: str) -> str:
|
| 186 |
+
# Lower, remove non-alphanumerics, collapse spaces/underscores
|
| 187 |
+
import re, unicodedata
|
| 188 |
+
s = unicodedata.normalize("NFKC", s or "").strip().lower()
|
| 189 |
+
s = re.sub(r"[^\w\s]", "", s)
|
| 190 |
+
s = re.sub(r"[\s_]+", " ", s).strip()
|
| 191 |
+
return s
|
| 192 |
+
|
| 193 |
+
@st.cache_data(show_spinner=False)
|
| 194 |
+
def _build_analysis_index(analyses_dir: str) -> dict:
|
| 195 |
+
"""Return dict: normalized_name -> file_path for *.md under analyses_dir."""
|
| 196 |
+
import os, glob
|
| 197 |
+
index = {}
|
| 198 |
+
if not os.path.isdir(analyses_dir):
|
| 199 |
+
return index
|
| 200 |
+
for path in glob.glob(os.path.join(analyses_dir, "*.md")):
|
| 201 |
+
base = os.path.splitext(os.path.basename(path))[0] # "Student_Name"
|
| 202 |
+
# accept both "Student Name" and "Student_Name" as same
|
| 203 |
+
norm = _normalize_name(base.replace("_", " "))
|
| 204 |
+
index[norm] = path
|
| 205 |
+
return index
|
| 206 |
+
|
| 207 |
+
@st.cache_data(show_spinner=False)
|
| 208 |
+
def _load_markdown(path: str) -> str:
|
| 209 |
+
try:
|
| 210 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 211 |
+
return f.read()
|
| 212 |
+
except Exception:
|
| 213 |
+
return ""
|
| 214 |
|
| 215 |
# ------------------- UI -------------------
|
| 216 |
st.title("π Student Skill Radar")
|
|
|
|
| 232 |
overlay_sources = st.toggle("Overlay all sources when '(All)' selected", value=False)
|
| 233 |
chart_title = st.text_input("Chart title", value="")
|
| 234 |
|
| 235 |
+
|
| 236 |
+
|
| 237 |
# start_str = start_dt.strftime("%Y-%m-%d") if isinstance(start_dt, date) else None
|
| 238 |
# end_str = end_dt.strftime("%Y-%m-%d") if isinstance(end_dt, date) else None
|
| 239 |
|
|
|
|
| 529 |
"notable_quotes": notable_quotes,
|
| 530 |
}
|
| 531 |
|
| 532 |
+
# # ---------- Render the summary panel dynamically ----------
|
| 533 |
+
# if mongo_uri and student_choice != "(All)" and source_choice != "(All)":
|
| 534 |
+
# stage = SOURCE_TO_STAGE.get(source_choice.strip())
|
| 535 |
+
# if stage:
|
| 536 |
+
# # set to your actual summaries collection name
|
| 537 |
+
# summaries_coll_name = "summaries_IFE_2025"
|
| 538 |
+
# summary = fetch_student_stage_summary(
|
| 539 |
+
# mongo_uri, db_name, summaries_coll_name, coll_name,
|
| 540 |
+
# student=student_choice, stage=stage
|
| 541 |
+
# )
|
| 542 |
+
# if summary:
|
| 543 |
+
# st.markdown("---")
|
| 544 |
+
# st.subheader(f"Summary β {student_choice} ({stage.replace('_', ' ').title()})")
|
| 545 |
+
# c1, c2 = st.columns(2)
|
| 546 |
+
# with c1:
|
| 547 |
+
# st.markdown(f"**Most Consistent:** {summary.get('most_consistent') or 'β'}")
|
| 548 |
+
# st.markdown(f"**Most Developed:** {summary.get('most_developed') or 'β'}")
|
| 549 |
+
# with c2:
|
| 550 |
+
# strengths = summary.get("top_strengths") or []
|
| 551 |
+
# st.markdown("**Top Strengths:** " + (", ".join(strengths) if strengths else "β"))
|
| 552 |
+
|
| 553 |
+
# st.markdown("**Notable Quotes:**")
|
| 554 |
+
# for q in (summary.get("notable_quotes") or [])[:3]:
|
| 555 |
+
# st.markdown(f"> {q}")
|
| 556 |
+
# ------------------- Output (Tabs) -------------------
|
| 557 |
+
tab_radar, tab_analyses = st.tabs(["π Radar", "π Analyses"])
|
| 558 |
+
|
| 559 |
+
with tab_radar:
|
| 560 |
+
# plot the radar (uses df_plot, avg_label already computed above)
|
| 561 |
+
fig = plot_radar(df_plot, grouped, chart_title, avg_label=avg_label)
|
| 562 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 563 |
+
st.caption(f"{len(df_final)} line(s) aggregated." if not df_final.empty else "No data.")
|
| 564 |
+
|
| 565 |
+
# Dynamic stage summary panel (only if a specific student + source selected,
|
| 566 |
+
# and only if they actually answered that week)
|
| 567 |
+
if mongo_uri and student_choice != "(All)" and source_choice != "(All)":
|
| 568 |
+
# Map the selected source (e.g., 'onboarding_responses') to stage (e.g., 'onboarding')
|
| 569 |
+
stage = SOURCE_TO_STAGE.get(source_choice.strip())
|
| 570 |
+
if stage:
|
| 571 |
+
# summaries collection name (adjust if needed)
|
| 572 |
+
summaries_coll_name = "summaries_IFE_2025"
|
| 573 |
+
|
| 574 |
+
summary = fetch_student_stage_summary(
|
| 575 |
+
mongo_uri, db_name, summaries_coll_name, coll_name,
|
| 576 |
+
student=student_choice, stage=stage
|
| 577 |
+
)
|
| 578 |
+
if summary:
|
| 579 |
+
st.markdown("---")
|
| 580 |
+
st.subheader(f"Summary β {student_choice} ({stage.replace('_', ' ').title()})")
|
| 581 |
+
|
| 582 |
+
c1, c2 = st.columns(2)
|
| 583 |
+
with c1:
|
| 584 |
+
st.markdown(f"**Most Consistent:** {summary.get('most_consistent') or 'β'}")
|
| 585 |
+
st.markdown(f"**Most Developed:** {summary.get('most_developed') or 'β'}")
|
| 586 |
+
with c2:
|
| 587 |
+
strengths = summary.get("top_strengths") or []
|
| 588 |
+
st.markdown("**Top Strengths:** " + (", ".join(strengths) if strengths else "β"))
|
| 589 |
+
|
| 590 |
+
st.markdown("**Notable Quotes:**")
|
| 591 |
+
for q in (summary.get("notable_quotes") or [])[:3]:
|
| 592 |
+
st.markdown(f"> {q}")
|
| 593 |
+
|
| 594 |
+
with tab_analyses:
|
| 595 |
+
st.subheader("Student Analysis (Markdown)")
|
| 596 |
+
|
| 597 |
+
# Use the folder you defined at top (ANALYSES_DIR), or expose it in the sidebar if you prefer.
|
| 598 |
+
idx = _build_analysis_index(ANALYSES_DIR)
|
| 599 |
+
|
| 600 |
+
if student_choice == "(All)":
|
| 601 |
+
st.info("Pick a specific student on the left to view their analysis.")
|
| 602 |
+
# (Optional) show what's available so you can browse:
|
| 603 |
+
if idx:
|
| 604 |
+
st.caption("Available analyses:")
|
| 605 |
+
st.write(", ".join(sorted({name.title() for name in idx.keys()})))
|
| 606 |
+
else:
|
| 607 |
+
# Normalize the selected student name to match filenames
|
| 608 |
+
norm = _normalize_name(student_choice)
|
| 609 |
+
path = idx.get(norm)
|
| 610 |
+
|
| 611 |
+
# If exact match not found, try simple underscore variant
|
| 612 |
+
if not path:
|
| 613 |
+
alt = student_choice.replace(" ", "_")
|
| 614 |
+
path = idx.get(_normalize_name(alt))
|
| 615 |
+
|
| 616 |
+
if path:
|
| 617 |
+
md = _load_markdown(path)
|
| 618 |
+
if md.strip():
|
| 619 |
+
st.markdown(md, unsafe_allow_html=False)
|
| 620 |
+
# Optional download button
|
| 621 |
+
with open(path, "rb") as f:
|
| 622 |
+
st.download_button(
|
| 623 |
+
"Download analysis (.md)", f,
|
| 624 |
+
file_name=os.path.basename(path), mime="text/markdown"
|
| 625 |
+
)
|
| 626 |
+
else:
|
| 627 |
+
st.warning("Analysis file found but empty.")
|
| 628 |
+
else:
|
| 629 |
+
st.warning(f"No analysis found for **{student_choice}** in `{ANALYSES_DIR}` yet.")
|
| 630 |
+
if idx:
|
| 631 |
+
st.caption("Available analyses:")
|
| 632 |
+
st.write(", ".join(sorted({name.title() for name in idx.keys()})))
|
| 633 |
|
| 634 |
|
| 635 |
|