Tesneem commited on
Commit
feeb79b
·
verified ·
1 Parent(s): 40fa377

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +386 -148
app.py CHANGED
@@ -1,8 +1,7 @@
1
- # app.py
2
  import os
3
  import json
4
- import math
5
- from datetime import datetime
6
  from typing import Dict, List
7
 
8
  import numpy as np
@@ -10,7 +9,6 @@ import pandas as pd
10
  import plotly.graph_objects as go
11
  import streamlit as st
12
  from pymongo import MongoClient
13
- from urllib.parse import quote_plus
14
 
15
  st.set_page_config(page_title="Student Skill Radar", layout="wide")
16
 
@@ -33,24 +31,17 @@ SKILLS = [
33
 
34
  SKILL_GROUPS = {
35
  "Problem-Solving, Critical Thinking, Analytical Reasoning": [
36
- "Problem-Solving",
37
- "Critical Thinking",
38
- "Analytical Reasoning",
39
  ],
40
  "Adaptability, Continuous Learning, Creativity": [
41
- "Adaptability",
42
- "Continuous Learning",
43
- "Creativity",
44
  ],
45
  "Time Management": ["Time Management"],
46
  "Communication, Teamwork, Collaboration, Community Engagement": [
47
- "Communication",
48
- "Collaboration",
49
- "Community Engagement",
50
  ],
51
  "Emotional Intelligence, Ethical Decision Making": [
52
- "Emotional Intelligence",
53
- "Ethical Decision-Making",
54
  ],
55
  "Tech Aptitude": ["Tech Aptitude"],
56
  }
@@ -65,16 +56,21 @@ def to_frame(records: List[dict]) -> pd.DataFrame:
65
  if not records:
66
  return pd.DataFrame()
67
  df = pd.DataFrame(records)
68
- # Expand skills into columns
69
- skill_df = pd.json_normalize(df["skills"]).reindex(columns=SKILLS)
70
  for k in SKILLS:
71
  if k not in skill_df:
72
  skill_df[k] = 0.0
73
- df = pd.concat([df.drop(columns=["skills"]), skill_df], axis=1)
74
  return df
75
 
76
 
77
- def summarize_records(records: List[dict], level: str = "student") -> pd.DataFrame:
 
 
 
 
 
78
  df = to_frame(records)
79
  if df.empty:
80
  return df
@@ -85,14 +81,7 @@ def summarize_records(records: List[dict], level: str = "student") -> pd.DataFra
85
  return df.groupby("label")[SKILLS].mean().reset_index()
86
 
87
 
88
- def aggregate_groups(row: pd.Series) -> Dict[str, float]:
89
- out = {}
90
- for group, members in SKILL_GROUPS.items():
91
- out[group] = safe_mean([float(row.get(m, 0.0)) for m in members])
92
- return out
93
-
94
-
95
- def polar_radar(df: pd.DataFrame, grouped: bool, title: str):
96
  if df.empty:
97
  return go.Figure()
98
 
@@ -100,10 +89,15 @@ def polar_radar(df: pd.DataFrame, grouped: bool, title: str):
100
  labels = list(SKILL_GROUPS.keys())
101
  traces = []
102
  for _, r in df.iterrows():
103
- grp = aggregate_groups(r)
104
  values = [grp[k] for k in labels]
105
  traces.append(
106
- go.Scatterpolar(r=values + [values[0]], theta=labels + [labels[0]], name=r["label"], fill="toself")
 
 
 
 
 
107
  )
108
  else:
109
  labels = SKILLS
@@ -111,7 +105,12 @@ def polar_radar(df: pd.DataFrame, grouped: bool, title: str):
111
  for _, r in df.iterrows():
112
  values = [float(r.get(k, 0.0)) for k in SKILLS]
113
  traces.append(
114
- go.Scatterpolar(r=values + [values[0]], theta=labels + [labels[0]], name=r["label"], fill="toself")
 
 
 
 
 
115
  )
116
 
117
  fig = go.Figure(traces)
@@ -124,39 +123,40 @@ def polar_radar(df: pd.DataFrame, grouped: bool, title: str):
124
  return fig
125
 
126
 
127
- # ------------------- Data Loaders -------------------
128
  @st.cache_data(show_spinner=False)
129
- def parse_summary_files(files) -> pd.DataFrame:
130
- """Uploads: list of per-student summary JSON files"""
131
- rows = []
132
- for f in files or []:
133
- try:
134
- data = json.loads(f.read().decode("utf-8"))
135
- except Exception:
136
- f.seek(0)
137
- data = json.load(f)
138
- name = data.get("Name") or data.get("Student") or "Unknown"
139
- scores = data.get("Average Skill Scores") or {}
140
- row = {"label": name}
141
- for k in SKILLS:
142
- row[k] = float(scores.get(k, 0.0))
143
- rows.append(row)
144
- return pd.DataFrame(rows)
 
 
145
 
146
 
147
  @st.cache_data(show_spinner=False)
148
- def mongo_records(db_name: str, coll_name: str, student: str | None, source: str | None, start: str | None, end: str | None) -> List[dict]:
149
- if not (db_name and coll_name):
 
 
 
 
150
  return []
151
- user = quote_plus(os.getenv("MONGO_USER"))
152
- password = quote_plus(os.getenv("MONGO_PASS"))
153
- cluster = os.getenv("MONGO_CLUSTER")
154
- # db_name = os.environ.get("MONGO_DB", "grant_docs")
155
- mongo_uri = f"mongodb+srv://{user}:{password}@{cluster}/{db_name}?retryWrites=true&w=majority&tls=true&tlsAllowInvalidCertificates=true"
156
- client = MongoClient(mongo_uri, tls=True, tlsAllowInvalidCertificates=True, serverSelectionTimeoutMS=20000)
157
- # client = MongoClient(uri, serverSelectionTimeoutMS=6000)
158
- coll = client[db_name][coll_name]
159
 
 
 
 
160
  q = {}
161
  if student and student != "(All)":
162
  q["student"] = student
@@ -168,118 +168,356 @@ def mongo_records(db_name: str, coll_name: str, student: str | None, source: str
168
  q["date"]["$gte"] = start
169
  if end:
170
  q["date"]["$lte"] = end
171
-
172
- cur = coll.find(q, {"_id": 0, "student": 1, "source": 1, "date": 1, "skills": 1})
173
- recs = []
174
- for r in cur:
175
- r.setdefault("skills", {})
176
- r["skills"] = {k: float(r["skills"].get(k, 0.0)) for k in SKILLS}
177
- recs.append(r)
178
- return recs
179
-
180
-
181
- @st.cache_data(show_spinner=False)
182
- def mongo_distinct( db_name: str, coll_name: str, field: str) -> List[str]:
183
- if not (db_name and coll_name):
184
- return []
185
  try:
186
- user = quote_plus(os.getenv("MONGO_USER"))
187
- password = quote_plus(os.getenv("MONGO_PASS"))
188
- cluster = os.getenv("MONGO_CLUSTER")
189
- mongo_uri = f"mongodb+srv://{user}:{password}@{cluster}/{db_name}?retryWrites=true&w=majority&tls=true&tlsAllowInvalidCertificates=true"
190
- client = MongoClient(mongo_uri, tls=True, tlsAllowInvalidCertificates=True, serverSelectionTimeoutMS=20000)
191
- # client = MongoClient(uri, serverSelectionTimeoutMS=6000)
192
- coll = client[db_name][coll_name]
193
- vals = coll.distinct(field)
194
- return sorted([v for v in vals if isinstance(v, str) and v.strip()])
195
  except Exception:
196
  return []
197
 
198
 
199
  # ------------------- UI -------------------
200
- st.title("Student Skill Radar — Streamlit")
201
 
202
  with st.sidebar:
203
- st.subheader("Data Source")
204
- data_source = st.radio("Select source", ["Upload JSON summaries", "MongoDB"], index=0)
205
- use_groups = st.toggle("Grouped skills (skill clusters)", value=False)
206
- agg_level = st.selectbox("Aggregation level", ["student", "student+source"], index=0, help="How to average records before plotting")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  chart_title = st.text_input("Chart title", value="")
208
 
209
- if data_source == "Upload JSON summaries":
210
- files = st.file_uploader("Upload 1+ summary JSON files", type=["json"], accept_multiple_files=True)
211
- df = parse_summary_files(files)
212
-
213
- # Student dropdown based on uploaded files
214
- labels = ["(All)"] + (sorted(df["label"].unique().tolist()) if not df.empty else [])
215
- selected = st.sidebar.selectbox("Select student", labels)
216
-
217
- if selected != "(All)" and not df.empty:
218
- df = df[df["label"] == selected]
219
-
220
- else:
221
- st.sidebar.subheader("MongoDB Settings")
222
- # default_uri = st.secrets.get("MONGO_URI", "")
223
- # mongo_uri = st.sidebar.text_input("MongoDB URI", value=default_uri, type="password")
224
- db_name = st.sidebar.text_input("Database name", value="grant_docs")
225
- coll_name = st.sidebar.text_input("Collection name", value="doc_chunks")
226
-
227
- # Dynamic dropdowns from MongoDB
228
- students = ["(All)"] + mongo_distinct(db_name, coll_name, "student")
229
- sources = ["(All)"] + mongo_distinct(db_name, coll_name, "source")
230
-
231
- student_choice = st.sidebar.selectbox("Select student", students)
232
- source_choice = st.sidebar.selectbox("Select source/week", sources)
233
-
234
- c1, c2 = st.sidebar.columns(2)
235
- start_date = c1.text_input("Start date (YYYY-MM-DD)", value="")
236
- end_date = c2.text_input("End date (YYYY-MM-DD)", value="")
237
-
238
- recs = mongo_records(db_name, coll_name, student_choice, source_choice, start_date or None, end_date or None)
239
- df_raw = to_frame(recs)
240
- if not df_raw.empty:
241
- if agg_level == "student+source":
242
- df_raw["label"] = df_raw["student"].astype(str) + " — " + df_raw["source"].astype(str)
243
- else:
244
- df_raw["label"] = df_raw["student"].astype(str)
245
- df = df_raw.groupby("label")[SKILLS].mean().reset_index()
246
- else:
247
- df = pd.DataFrame()
248
 
249
  # ------------------- Output -------------------
250
  left, right = st.columns([2, 1])
251
 
252
  with left:
253
- fig = polar_radar(df if not df.empty else pd.DataFrame(), use_groups, chart_title)
254
  st.plotly_chart(fig, use_container_width=True)
255
 
256
  with right:
257
- st.subheader("Averaged Scores")
258
  if df.empty:
259
- st.info("No data yet. Upload summaries or configure MongoDB, then select a student.")
260
  else:
261
  st.dataframe(df, use_container_width=True, height=450)
262
- # CSV download
263
  csv = df.to_csv(index=False).encode("utf-8")
264
  st.download_button("Download CSV", data=csv, file_name="skill_scores.csv", mime="text/csv")
265
 
266
- # # --------------- README (for reference in Space) ---------------
267
- # """
268
- # To deploy on Hugging Face Spaces:
269
- # 1) Create a new Space → SDK: Streamlit → Python.
270
- # 2) Add `app.py` and `requirements.txt` below.
271
- # 3) (Optional) Add a Secret named `MONGO_URI` for your Mongo connection.
272
-
273
- # Accepted Schemas
274
- # - Summary JSON (per student):
275
- # {
276
- # "Name": "Student Name",
277
- # "Average Skill Scores": {"Problem-Solving": 0.6, ...}
278
- # }
279
- # - MongoDB record (per response):
280
- # {
281
- # "uid": "...", "student": "...", "source": "week_2", "date": "YYYY-MM-DD",
282
- # "prompt": "...", "answer": "...",
283
- # "skills": { "Problem-Solving": 0.6, "Collaboration": 0.7, ... }
284
- # }
285
- # """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py — Streamlit radar charts from MongoDB (scores 0–1)
2
  import os
3
  import json
4
+ from datetime import date
 
5
  from typing import Dict, List
6
 
7
  import numpy as np
 
9
  import plotly.graph_objects as go
10
  import streamlit as st
11
  from pymongo import MongoClient
 
12
 
13
  st.set_page_config(page_title="Student Skill Radar", layout="wide")
14
 
 
31
 
32
  SKILL_GROUPS = {
33
  "Problem-Solving, Critical Thinking, Analytical Reasoning": [
34
+ "Problem-Solving", "Critical Thinking", "Analytical Reasoning"
 
 
35
  ],
36
  "Adaptability, Continuous Learning, Creativity": [
37
+ "Adaptability", "Continuous Learning", "Creativity"
 
 
38
  ],
39
  "Time Management": ["Time Management"],
40
  "Communication, Teamwork, Collaboration, Community Engagement": [
41
+ "Communication", "Collaboration", "Community Engagement"
 
 
42
  ],
43
  "Emotional Intelligence, Ethical Decision Making": [
44
+ "Emotional Intelligence", "Ethical Decision-Making"
 
45
  ],
46
  "Tech Aptitude": ["Tech Aptitude"],
47
  }
 
56
  if not records:
57
  return pd.DataFrame()
58
  df = pd.DataFrame(records)
59
+ # Expand skills into columns in SKILLS order
60
+ skill_df = pd.json_normalize(df.get("skills", {})).reindex(columns=SKILLS)
61
  for k in SKILLS:
62
  if k not in skill_df:
63
  skill_df[k] = 0.0
64
+ df = pd.concat([df.drop(columns=["skills"], errors="ignore"), skill_df], axis=1)
65
  return df
66
 
67
 
68
+ def aggregate_groups_row(row: pd.Series) -> Dict[str, float]:
69
+ return {g: safe_mean([float(row.get(s, 0.0)) for s in members]) for g, members in SKILL_GROUPS.items()}
70
+
71
+
72
+ def summarize(records: List[dict], level: str = "student") -> pd.DataFrame:
73
+ """Average per label over SKILLS; level in {student, student+source}."""
74
  df = to_frame(records)
75
  if df.empty:
76
  return df
 
81
  return df.groupby("label")[SKILLS].mean().reset_index()
82
 
83
 
84
+ def plot_radar(df: pd.DataFrame, grouped: bool, title: str):
 
 
 
 
 
 
 
85
  if df.empty:
86
  return go.Figure()
87
 
 
89
  labels = list(SKILL_GROUPS.keys())
90
  traces = []
91
  for _, r in df.iterrows():
92
+ grp = aggregate_groups_row(r)
93
  values = [grp[k] for k in labels]
94
  traces.append(
95
+ go.Scatterpolar(
96
+ r=values + [values[0]],
97
+ theta=labels + [labels[0]],
98
+ name=r["label"],
99
+ fill="toself",
100
+ )
101
  )
102
  else:
103
  labels = SKILLS
 
105
  for _, r in df.iterrows():
106
  values = [float(r.get(k, 0.0)) for k in SKILLS]
107
  traces.append(
108
+ go.Scatterpolar(
109
+ r=values + [values[0]],
110
+ theta=labels + [labels[0]],
111
+ name=r["label"],
112
+ fill="toself",
113
+ )
114
  )
115
 
116
  fig = go.Figure(traces)
 
123
  return fig
124
 
125
 
126
+ # ------------------- Mongo Access -------------------
127
  @st.cache_data(show_spinner=False)
128
+ def _client(uri: str):
129
+ return MongoClient(uri, serverSelectionTimeoutMS=10000)
130
+
131
+
132
+ def get_mongo_uri(db_name: str | None = None) -> str | None:
133
+ """Priority: st.secrets.MONGO_URI -> env MONGO_URI -> compose from MONGO_USER/PASS/CLUSTER."""
134
+ uri = st.secrets.get("MONGO_URI") if hasattr(st, "secrets") else None
135
+ uri = uri or os.getenv("MONGO_URI")
136
+ if uri:
137
+ return uri
138
+ user = os.getenv("MONGO_USER")
139
+ pw = os.getenv("MONGO_PASS")
140
+ cluster = os.getenv("MONGO_CLUSTER")
141
+ if user and pw and cluster:
142
+ # allow db_name in path for SRV
143
+ db_path = f"/{db_name}" if db_name else ""
144
+ return f"mongodb+srv://{user}:{pw}@{cluster}{db_path}?retryWrites=true&w=majority"
145
+ return None
146
 
147
 
148
  @st.cache_data(show_spinner=False)
149
+ def mongo_distinct(uri: str, db: str, coll: str, field: str) -> List[str]:
150
+ try:
151
+ c = _client(uri)
152
+ vals = c[db][coll].distinct(field)
153
+ return sorted([v for v in vals if isinstance(v, str) and v.strip()])
154
+ except Exception:
155
  return []
 
 
 
 
 
 
 
 
156
 
157
+
158
+ @st.cache_data(show_spinner=False)
159
+ def mongo_records(uri: str, db: str, coll: str, student: str | None, source: str | None, start: str | None, end: str | None) -> List[dict]:
160
  q = {}
161
  if student and student != "(All)":
162
  q["student"] = student
 
168
  q["date"]["$gte"] = start
169
  if end:
170
  q["date"]["$lte"] = end
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  try:
172
+ c = _client(uri)
173
+ proj = {"_id": 0, "student": 1, "source": 1, "date": 1, "skills": 1}
174
+ out = list(c[db][coll].find(q, proj))
175
+ # normalize scores to floats; default 0.0
176
+ for r in out:
177
+ r.setdefault("skills", {})
178
+ r["skills"] = {k: float(r["skills"].get(k, 0.0)) for k in SKILLS}
179
+ return out
 
180
  except Exception:
181
  return []
182
 
183
 
184
  # ------------------- UI -------------------
185
+ st.title("📊 Student Skill Radar — MongoDB only")
186
 
187
  with st.sidebar:
188
+ st.subheader("MongoDB Settings")
189
+ db_name = st.text_input("Database name", value="student_skills")
190
+ coll_name = st.text_input("Collection name", value="responses_IFE_2025")
191
+
192
+ # URI handling
193
+ detected_uri = get_mongo_uri(db_name)
194
+ uri_override = st.text_input("Override MONGO_URI (optional)", type="password")
195
+ mongo_uri = uri_override.strip() or (detected_uri or "")
196
+ if not mongo_uri:
197
+ st.warning("No Mongo URI found. Set MONGO_URI (or MONGO_USER/PASS/CLUSTER) in Space secrets, or paste an override.")
198
+
199
+ # Filters
200
+ students = ["(All)"] + (mongo_distinct(mongo_uri, db_name, coll_name, "student") if mongo_uri else [])
201
+ sources = ["(All)"] + (mongo_distinct(mongo_uri, db_name, coll_name, "source") if mongo_uri else [])
202
+
203
+ student_choice = st.selectbox("Select student", students)
204
+ source_choice = st.selectbox("Select source/week", sources)
205
+
206
+ c1, c2 = st.columns(2)
207
+ start_dt = c1.date_input("Start date", value=None)
208
+ end_dt = c2.date_input("End date", value=None)
209
+
210
+ agg_level = st.selectbox("Aggregation level", ["student", "student+source"], index=0)
211
+ grouped = st.toggle("Grouped skills (skill clusters)", value=False)
212
  chart_title = st.text_input("Chart title", value="")
213
 
214
+ # Convert dates to strings (YYYY-MM-DD)
215
+ start_str = start_dt.strftime("%Y-%m-%d") if isinstance(start_dt, date) else None
216
+ end_str = end_dt.strftime("%Y-%m-%d") if isinstance(end_dt, date) else None
217
+
218
+ # Fetch + aggregate
219
+ records = mongo_records(mongo_uri, db_name, coll_name, student_choice, source_choice, start_str, end_str) if mongo_uri else []
220
+
221
+ df = summarize(records, level=agg_level) if records else pd.DataFrame()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
  # ------------------- Output -------------------
224
  left, right = st.columns([2, 1])
225
 
226
  with left:
227
+ fig = plot_radar(df, grouped, chart_title)
228
  st.plotly_chart(fig, use_container_width=True)
229
 
230
  with right:
231
+ st.subheader("Averaged Scores (0–1)")
232
  if df.empty:
233
+ st.info("No data. Adjust filters or check Mongo connection.")
234
  else:
235
  st.dataframe(df, use_container_width=True, height=450)
 
236
  csv = df.to_csv(index=False).encode("utf-8")
237
  st.download_button("Download CSV", data=csv, file_name="skill_scores.csv", mime="text/csv")
238
 
239
+ # # app.py
240
+ # import os
241
+ # import json
242
+ # import math
243
+ # from datetime import datetime
244
+ # from typing import Dict, List
245
+
246
+ # import numpy as np
247
+ # import pandas as pd
248
+ # import plotly.graph_objects as go
249
+ # import streamlit as st
250
+ # from pymongo import MongoClient
251
+ # from urllib.parse import quote_plus
252
+
253
+ # st.set_page_config(page_title="Student Skill Radar", layout="wide")
254
+
255
+ # # ------------------- Constants -------------------
256
+ # SKILLS = [
257
+ # "Problem-Solving",
258
+ # "Critical Thinking",
259
+ # "Analytical Reasoning",
260
+ # "Adaptability",
261
+ # "Continuous Learning",
262
+ # "Creativity",
263
+ # "Communication",
264
+ # "Collaboration",
265
+ # "Community Engagement",
266
+ # "Emotional Intelligence",
267
+ # "Ethical Decision-Making",
268
+ # "Time Management",
269
+ # "Tech Aptitude",
270
+ # ]
271
+
272
+ # SKILL_GROUPS = {
273
+ # "Problem-Solving, Critical Thinking, Analytical Reasoning": [
274
+ # "Problem-Solving",
275
+ # "Critical Thinking",
276
+ # "Analytical Reasoning",
277
+ # ],
278
+ # "Adaptability, Continuous Learning, Creativity": [
279
+ # "Adaptability",
280
+ # "Continuous Learning",
281
+ # "Creativity",
282
+ # ],
283
+ # "Time Management": ["Time Management"],
284
+ # "Communication, Teamwork, Collaboration, Community Engagement": [
285
+ # "Communication",
286
+ # "Collaboration",
287
+ # "Community Engagement",
288
+ # ],
289
+ # "Emotional Intelligence, Ethical Decision Making": [
290
+ # "Emotional Intelligence",
291
+ # "Ethical Decision-Making",
292
+ # ],
293
+ # "Tech Aptitude": ["Tech Aptitude"],
294
+ # }
295
+
296
+ # # ------------------- Helpers -------------------
297
+ # def safe_mean(vals):
298
+ # vals = [v for v in vals if v is not None]
299
+ # return float(np.mean(vals)) if vals else 0.0
300
+
301
+
302
+ # def to_frame(records: List[dict]) -> pd.DataFrame:
303
+ # if not records:
304
+ # return pd.DataFrame()
305
+ # df = pd.DataFrame(records)
306
+ # # Expand skills into columns
307
+ # skill_df = pd.json_normalize(df["skills"]).reindex(columns=SKILLS)
308
+ # for k in SKILLS:
309
+ # if k not in skill_df:
310
+ # skill_df[k] = 0.0
311
+ # df = pd.concat([df.drop(columns=["skills"]), skill_df], axis=1)
312
+ # return df
313
+
314
+
315
+ # def summarize_records(records: List[dict], level: str = "student") -> pd.DataFrame:
316
+ # df = to_frame(records)
317
+ # if df.empty:
318
+ # return df
319
+ # if level == "student+source":
320
+ # df["label"] = df["student"].astype(str) + " — " + df["source"].astype(str)
321
+ # else:
322
+ # df["label"] = df["student"].astype(str)
323
+ # return df.groupby("label")[SKILLS].mean().reset_index()
324
+
325
+
326
+ # def aggregate_groups(row: pd.Series) -> Dict[str, float]:
327
+ # out = {}
328
+ # for group, members in SKILL_GROUPS.items():
329
+ # out[group] = safe_mean([float(row.get(m, 0.0)) for m in members])
330
+ # return out
331
+
332
+
333
+ # def polar_radar(df: pd.DataFrame, grouped: bool, title: str):
334
+ # if df.empty:
335
+ # return go.Figure()
336
+
337
+ # if grouped:
338
+ # labels = list(SKILL_GROUPS.keys())
339
+ # traces = []
340
+ # for _, r in df.iterrows():
341
+ # grp = aggregate_groups(r)
342
+ # values = [grp[k] for k in labels]
343
+ # traces.append(
344
+ # go.Scatterpolar(r=values + [values[0]], theta=labels + [labels[0]], name=r["label"], fill="toself")
345
+ # )
346
+ # else:
347
+ # labels = SKILLS
348
+ # traces = []
349
+ # for _, r in df.iterrows():
350
+ # values = [float(r.get(k, 0.0)) for k in SKILLS]
351
+ # traces.append(
352
+ # go.Scatterpolar(r=values + [values[0]], theta=labels + [labels[0]], name=r["label"], fill="toself")
353
+ # )
354
+
355
+ # fig = go.Figure(traces)
356
+ # fig.update_layout(
357
+ # title=title or "Skill Radar",
358
+ # showlegend=True,
359
+ # polar=dict(radialaxis=dict(range=[0, 1.0], tickvals=[0.2, 0.4, 0.6, 0.8])),
360
+ # margin=dict(l=30, r=30, t=60, b=30),
361
+ # )
362
+ # return fig
363
+
364
+
365
+ # # ------------------- Data Loaders -------------------
366
+ # @st.cache_data(show_spinner=False)
367
+ # def parse_summary_files(files) -> pd.DataFrame:
368
+ # """Uploads: list of per-student summary JSON files"""
369
+ # rows = []
370
+ # for f in files or []:
371
+ # try:
372
+ # data = json.loads(f.read().decode("utf-8"))
373
+ # except Exception:
374
+ # f.seek(0)
375
+ # data = json.load(f)
376
+ # name = data.get("Name") or data.get("Student") or "Unknown"
377
+ # scores = data.get("Average Skill Scores") or {}
378
+ # row = {"label": name}
379
+ # for k in SKILLS:
380
+ # row[k] = float(scores.get(k, 0.0))
381
+ # rows.append(row)
382
+ # return pd.DataFrame(rows)
383
+
384
+
385
+ # @st.cache_data(show_spinner=False)
386
+ # def mongo_records(db_name: str, coll_name: str, student: str | None, source: str | None, start: str | None, end: str | None) -> List[dict]:
387
+ # if not (db_name and coll_name):
388
+ # return []
389
+ # user = quote_plus(os.getenv("MONGO_USER"))
390
+ # password = quote_plus(os.getenv("MONGO_PASS"))
391
+ # cluster = os.getenv("MONGO_CLUSTER")
392
+ # # db_name = os.environ.get("MONGO_DB", "grant_docs")
393
+ # mongo_uri = f"mongodb+srv://{user}:{password}@{cluster}/{db_name}?retryWrites=true&w=majority&tls=true&tlsAllowInvalidCertificates=true"
394
+ # client = MongoClient(mongo_uri, tls=True, tlsAllowInvalidCertificates=True, serverSelectionTimeoutMS=20000)
395
+ # # client = MongoClient(uri, serverSelectionTimeoutMS=6000)
396
+ # coll = client[db_name][coll_name]
397
+
398
+ # q = {}
399
+ # if student and student != "(All)":
400
+ # q["student"] = student
401
+ # if source and source != "(All)":
402
+ # q["source"] = source
403
+ # if start or end:
404
+ # q["date"] = {}
405
+ # if start:
406
+ # q["date"]["$gte"] = start
407
+ # if end:
408
+ # q["date"]["$lte"] = end
409
+
410
+ # cur = coll.find(q, {"_id": 0, "student": 1, "source": 1, "date": 1, "skills": 1})
411
+ # recs = []
412
+ # for r in cur:
413
+ # r.setdefault("skills", {})
414
+ # r["skills"] = {k: float(r["skills"].get(k, 0.0)) for k in SKILLS}
415
+ # recs.append(r)
416
+ # return recs
417
+
418
+
419
+ # @st.cache_data(show_spinner=False)
420
+ # def mongo_distinct( db_name: str, coll_name: str, field: str) -> List[str]:
421
+ # if not (db_name and coll_name):
422
+ # return []
423
+ # try:
424
+ # user = quote_plus(os.getenv("MONGO_USER"))
425
+ # password = quote_plus(os.getenv("MONGO_PASS"))
426
+ # cluster = os.getenv("MONGO_CLUSTER")
427
+ # mongo_uri = f"mongodb+srv://{user}:{password}@{cluster}/{db_name}?retryWrites=true&w=majority&tls=true&tlsAllowInvalidCertificates=true"
428
+ # client = MongoClient(mongo_uri, tls=True, tlsAllowInvalidCertificates=True, serverSelectionTimeoutMS=20000)
429
+ # # client = MongoClient(uri, serverSelectionTimeoutMS=6000)
430
+ # coll = client[db_name][coll_name]
431
+ # vals = coll.distinct(field)
432
+ # return sorted([v for v in vals if isinstance(v, str) and v.strip()])
433
+ # except Exception:
434
+ # return []
435
+
436
+
437
+ # # ------------------- UI -------------------
438
+ # st.title("Student Skill Radar — Streamlit")
439
+
440
+ # with st.sidebar:
441
+ # st.subheader("Data Source")
442
+ # data_source = st.radio("Select source", ["Upload JSON summaries", "MongoDB"], index=0)
443
+ # use_groups = st.toggle("Grouped skills (skill clusters)", value=False)
444
+ # agg_level = st.selectbox("Aggregation level", ["student", "student+source"], index=0, help="How to average records before plotting")
445
+ # chart_title = st.text_input("Chart title", value="")
446
+
447
+ # if data_source == "Upload JSON summaries":
448
+ # files = st.file_uploader("Upload 1+ summary JSON files", type=["json"], accept_multiple_files=True)
449
+ # df = parse_summary_files(files)
450
+
451
+ # # Student dropdown based on uploaded files
452
+ # labels = ["(All)"] + (sorted(df["label"].unique().tolist()) if not df.empty else [])
453
+ # selected = st.sidebar.selectbox("Select student", labels)
454
+
455
+ # if selected != "(All)" and not df.empty:
456
+ # df = df[df["label"] == selected]
457
+
458
+ # else:
459
+ # st.sidebar.subheader("MongoDB Settings")
460
+ # # default_uri = st.secrets.get("MONGO_URI", "")
461
+ # # mongo_uri = st.sidebar.text_input("MongoDB URI", value=default_uri, type="password")
462
+ # db_name = st.sidebar.text_input("Database name", value="student_skills")
463
+ # coll_name = st.sidebar.text_input("Collection name", value="responses_IFE_2025")
464
+
465
+ # # Dynamic dropdowns from MongoDB
466
+ # students = ["(All)"] + mongo_distinct(db_name, coll_name, "student")
467
+ # sources = ["(All)"] + mongo_distinct(db_name, coll_name, "source")
468
+
469
+ # student_choice = st.sidebar.selectbox("Select student", students)
470
+ # source_choice = st.sidebar.selectbox("Select source/week", sources)
471
+
472
+ # c1, c2 = st.sidebar.columns(2)
473
+ # start_date = c1.text_input("Start date (YYYY-MM-DD)", value="")
474
+ # end_date = c2.text_input("End date (YYYY-MM-DD)", value="")
475
+
476
+ # recs = mongo_records(db_name, coll_name, student_choice, source_choice, start_date or None, end_date or None)
477
+ # df_raw = to_frame(recs)
478
+ # if not df_raw.empty:
479
+ # if agg_level == "student+source":
480
+ # df_raw["label"] = df_raw["student"].astype(str) + " — " + df_raw["source"].astype(str)
481
+ # else:
482
+ # df_raw["label"] = df_raw["student"].astype(str)
483
+ # df = df_raw.groupby("label")[SKILLS].mean().reset_index()
484
+ # else:
485
+ # df = pd.DataFrame()
486
+
487
+ # # ------------------- Output -------------------
488
+ # left, right = st.columns([2, 1])
489
+
490
+ # with left:
491
+ # fig = polar_radar(df if not df.empty else pd.DataFrame(), use_groups, chart_title)
492
+ # st.plotly_chart(fig, use_container_width=True)
493
+
494
+ # with right:
495
+ # st.subheader("Averaged Scores")
496
+ # if df.empty:
497
+ # st.info("No data yet. Upload summaries or configure MongoDB, then select a student.")
498
+ # else:
499
+ # st.dataframe(df, use_container_width=True, height=450)
500
+ # # CSV download
501
+ # csv = df.to_csv(index=False).encode("utf-8")
502
+ # st.download_button("Download CSV", data=csv, file_name="skill_scores.csv", mime="text/csv")
503
+
504
+ # # # --------------- README (for reference in Space) ---------------
505
+ # # """
506
+ # # To deploy on Hugging Face Spaces:
507
+ # # 1) Create a new Space → SDK: Streamlit → Python.
508
+ # # 2) Add `app.py` and `requirements.txt` below.
509
+ # # 3) (Optional) Add a Secret named `MONGO_URI` for your Mongo connection.
510
+
511
+ # # Accepted Schemas
512
+ # # - Summary JSON (per student):
513
+ # # {
514
+ # # "Name": "Student Name",
515
+ # # "Average Skill Scores": {"Problem-Solving": 0.6, ...}
516
+ # # }
517
+ # # - MongoDB record (per response):
518
+ # # {
519
+ # # "uid": "...", "student": "...", "source": "week_2", "date": "YYYY-MM-DD",
520
+ # # "prompt": "...", "answer": "...",
521
+ # # "skills": { "Problem-Solving": 0.6, "Collaboration": 0.7, ... }
522
+ # # }
523
+ # # """