ckharche commited on
Commit
61320cd
Β·
verified Β·
1 Parent(s): c72d725

Upload ui.py

Browse files
Files changed (1) hide show
  1. src/ui.py +421 -0
src/ui.py ADDED
@@ -0,0 +1,421 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pickle
3
+ import os
4
+ import time
5
+ import json
6
+
7
+ # Import the optimizer and visualizer
8
+ from curriculum_optimizer import HybridOptimizer, StudentProfile
9
+ from interactive_visualizer import CurriculumVisualizer
10
+
11
+ # --- Page Configuration ---
12
+ st.set_page_config(page_title="Curriculum Optimizer", layout="wide", initial_sidebar_state="expanded")
13
+
14
+ # Initialize session state
15
+ if "display_plan" not in st.session_state:
16
+ st.session_state.display_plan = None
17
+ if "metrics" not in st.session_state:
18
+ st.session_state.metrics = None
19
+ if "reasoning" not in st.session_state:
20
+ st.session_state.reasoning = ""
21
+ if "graph_data_loaded" not in st.session_state:
22
+ st.session_state.graph_data_loaded = False
23
+ if "last_profile" not in st.session_state:
24
+ st.session_state.last_profile = None
25
+ if "visualizer" not in st.session_state:
26
+ st.session_state.visualizer = None
27
+
28
+ # Title
29
+ st.title("πŸ§‘β€πŸŽ“ Next-Gen Curriculum Optimizer")
30
+
31
+ # --- Caching and Initialization ---
32
+ @st.cache_resource
33
+ def get_optimizer():
34
+ """Loads and caches the main optimizer class and its models."""
35
+ try:
36
+ optimizer = HybridOptimizer()
37
+ optimizer.load_models()
38
+ return optimizer
39
+ except Exception as e:
40
+ st.error(f"Fatal error during model loading: {e}")
41
+ st.info("Please ensure you have the required libraries installed.")
42
+ st.stop()
43
+ return None
44
+
45
+ optimizer = get_optimizer()
46
+
47
+ # Create tabs
48
+ tab1, tab2, tab3 = st.tabs(["πŸ“ Plan Generator", "πŸ—ΊοΈ Curriculum Map", "πŸ“Š Analytics"])
49
+
50
+ # TAB 1: PLAN GENERATOR (Your existing code)
51
+ with tab1:
52
+ # --- SIDEBAR FOR STUDENT PROFILE ---
53
+ with st.sidebar:
54
+ st.header("Student Profile")
55
+ name = st.text_input("Name", "Chaitanya Kharche")
56
+ gpa = st.slider("GPA", 0.0, 4.0, 3.5, 0.1)
57
+ career_goal = st.text_area("Career Goal", "AI Engineer specializing in Large Language Models")
58
+ interests = st.text_input("Interests (comma-separated)", "AI, Machine Learning, LLMs, Agentic AI")
59
+ learning_style = st.selectbox("Learning Style", ["Visual", "Hands-on", "Auditory"])
60
+ time_commit = st.number_input("Weekly Study Hours", 10, 60, 40, 5)
61
+ difficulty = st.selectbox("Preferred Difficulty", ["easy", "moderate", "challenging"])
62
+ completed_courses_input = st.text_area("Completed Courses (comma-separated)", "CS1800, CS2500")
63
+
64
+ # Show profile impact
65
+ st.markdown("---")
66
+ st.markdown("**Profile Impact:**")
67
+ if time_commit < 20:
68
+ st.info("πŸ•’ Part-time load (3 courses/semester)")
69
+ elif time_commit >= 40:
70
+ st.info("πŸ”₯ Intensive load (up to 5 courses/semester)")
71
+ else:
72
+ st.info("πŸ“š Standard load (4 courses/semester)")
73
+
74
+ if difficulty == "easy":
75
+ st.info("😌 Focuses on foundational courses")
76
+ elif difficulty == "challenging":
77
+ st.info("πŸš€ Includes advanced/specialized courses")
78
+ else:
79
+ st.info("βš–οΈ Balanced difficulty progression")
80
+
81
+ # LOAD DATA
82
+ st.subheader("1. Load Curriculum Data")
83
+ uploaded_file = st.file_uploader("Upload `neu_graph_analyzed_clean.pkl`", type=["pkl"])
84
+
85
+ if uploaded_file and not st.session_state.graph_data_loaded:
86
+ with st.spinner("Loading curriculum data and preparing embeddings..."):
87
+ try:
88
+ graph_data = pickle.load(uploaded_file)
89
+ optimizer.load_data(graph_data)
90
+ # Also create visualizer
91
+ st.session_state.visualizer = CurriculumVisualizer(graph_data)
92
+ st.session_state.graph_data = graph_data
93
+ st.session_state.graph_data_loaded = True
94
+ st.success(f"Successfully loaded and processed '{uploaded_file.name}'!")
95
+ time.sleep(1)
96
+ st.rerun()
97
+ except Exception as e:
98
+ st.error(f"Error processing .pkl file: {e}")
99
+ st.session_state.graph_data_loaded = False
100
+ elif st.session_state.graph_data_loaded:
101
+ st.success("Curriculum data is loaded and ready.")
102
+
103
+ # GENERATE PLAN
104
+ st.subheader("2. Generate a Plan")
105
+ if not st.session_state.graph_data_loaded:
106
+ st.info("Please load a curriculum file above to enable plan generation.")
107
+ else:
108
+ # Create student profile
109
+ profile = StudentProfile(
110
+ completed_courses=[c.strip().upper() for c in completed_courses_input.split(',') if c.strip()],
111
+ current_gpa=gpa,
112
+ interests=[i.strip() for i in interests.split(',') if i.strip()],
113
+ career_goals=career_goal,
114
+ learning_style=learning_style,
115
+ time_commitment=time_commit,
116
+ preferred_difficulty=difficulty
117
+ )
118
+
119
+ # Check if profile changed
120
+ profile_changed = st.session_state.last_profile != profile
121
+ if profile_changed:
122
+ st.session_state.last_profile = profile
123
+
124
+ col1, col2, col3 = st.columns(3)
125
+
126
+ if col1.button("🧠 AI-Optimized Plan", use_container_width=True, type="primary"):
127
+ with st.spinner("πŸš€ Using LLM for intelligent course selection..."):
128
+ start_time = time.time()
129
+ result = optimizer.generate_llm_plan(profile)
130
+ generation_time = time.time() - start_time
131
+
132
+ plan_raw = result.get('pathway', {})
133
+ st.session_state.reasoning = plan_raw.get("reasoning", "")
134
+ st.session_state.metrics = plan_raw.get("complexity_analysis", {})
135
+ st.session_state.display_plan = plan_raw
136
+ st.session_state.plan_type = "AI-Optimized"
137
+ st.session_state.generation_time = generation_time
138
+ st.success(f"πŸŽ‰ AI-optimized plan generated in {generation_time:.1f}s!")
139
+
140
+ if col2.button("⚑ Smart Rule-Based Plan", use_container_width=True):
141
+ with st.spinner("Generating personalized rule-based plan..."):
142
+ start_time = time.time()
143
+ result = optimizer.generate_simple_plan(profile)
144
+ generation_time = time.time() - start_time
145
+
146
+ plan_raw = result.get('pathway', {})
147
+ st.session_state.reasoning = plan_raw.get("reasoning", "")
148
+ st.session_state.metrics = plan_raw.get("complexity_analysis", {})
149
+ st.session_state.display_plan = plan_raw
150
+ st.session_state.plan_type = "Smart Rule-Based"
151
+ st.session_state.generation_time = generation_time
152
+ st.success(f"πŸŽ‰ Smart rule-based plan generated in {generation_time:.1f}s!")
153
+
154
+ if col3.button("πŸ”„ Clear Plan", use_container_width=True):
155
+ st.session_state.display_plan = None
156
+ st.session_state.metrics = None
157
+ st.session_state.reasoning = ""
158
+ st.rerun()
159
+
160
+ # Show profile change notification
161
+ if st.session_state.display_plan and profile_changed:
162
+ st.warning("⚠️ Student profile changed! Generate a new plan to see updated recommendations.")
163
+
164
+ # DISPLAY RESULTS
165
+ if st.session_state.display_plan:
166
+ st.subheader(f"πŸ“š {st.session_state.get('plan_type', 'Optimized')} Degree Plan")
167
+
168
+ # Display generation info
169
+ col_info1, col_info2, col_info3 = st.columns(3)
170
+ with col_info1:
171
+ st.metric("Generation Time", f"{st.session_state.get('generation_time', 0):.1f}s")
172
+ with col_info2:
173
+ st.metric("Plan Type", st.session_state.get('plan_type', 'Unknown'))
174
+ with col_info3:
175
+ if time_commit < 20:
176
+ load_type = "Part-time"
177
+ elif time_commit >= 40:
178
+ load_type = "Intensive"
179
+ else:
180
+ load_type = "Standard"
181
+ st.metric("Course Load", load_type)
182
+
183
+ # Display reasoning and metrics
184
+ if st.session_state.reasoning or st.session_state.metrics:
185
+ st.markdown("##### πŸ“Š Plan Analysis")
186
+
187
+ if st.session_state.reasoning:
188
+ st.info(f"**Strategy:** {st.session_state.reasoning}")
189
+
190
+ if st.session_state.metrics:
191
+ m = st.session_state.metrics
192
+ c1, c2, c3, c4 = st.columns(4)
193
+
194
+ c1.metric("Avg Complexity", f"{m.get('average_semester_complexity', 0):.1f}")
195
+ c2.metric("Peak Complexity", f"{m.get('peak_semester_complexity', 0):.1f}")
196
+ c3.metric("Total Complexity", f"{m.get('total_complexity', 0):.0f}")
197
+ c4.metric("Balance Score", f"{m.get('balance_score (std_dev)', 0):.2f}")
198
+
199
+ st.divider()
200
+
201
+ # Display the actual plan
202
+ plan = st.session_state.display_plan
203
+ total_courses = 0
204
+
205
+ for year_num in range(1, 5):
206
+ year_key = f"year_{year_num}"
207
+ year_data = plan.get(year_key, {})
208
+
209
+ st.markdown(f"### Year {year_num}")
210
+ col_fall, col_spring, col_summer = st.columns(3)
211
+
212
+ # Fall semester
213
+ with col_fall:
214
+ fall_courses = year_data.get("fall", [])
215
+ st.markdown("**πŸ‚ Fall Semester**")
216
+ if fall_courses:
217
+ for course_id in fall_courses:
218
+ if course_id in optimizer.courses:
219
+ course_data = optimizer.courses[course_id]
220
+ course_name = course_data.get("name", course_id)
221
+ st.write(f"β€’ **{course_id}**: {course_name}")
222
+ total_courses += 1
223
+ else:
224
+ st.write(f"β€’ {course_id}")
225
+ total_courses += 1
226
+ else:
227
+ st.write("*No courses scheduled*")
228
+
229
+ # Spring semester
230
+ with col_spring:
231
+ spring_courses = year_data.get("spring", [])
232
+ st.markdown("**🌸 Spring Semester**")
233
+ if spring_courses:
234
+ for course_id in spring_courses:
235
+ if course_id in optimizer.courses:
236
+ course_data = optimizer.courses[course_id]
237
+ course_name = course_data.get("name", course_id)
238
+ st.write(f"β€’ **{course_id}**: {course_name}")
239
+ total_courses += 1
240
+ else:
241
+ st.write(f"β€’ {course_id}")
242
+ total_courses += 1
243
+ else:
244
+ st.write("*No courses scheduled*")
245
+
246
+ # Summer
247
+ with col_summer:
248
+ summer = year_data.get("summer", [])
249
+ st.markdown("**β˜€οΈ Summer**")
250
+ if summer == "co-op":
251
+ st.write("🏒 *Co-op Experience*")
252
+ elif summer:
253
+ for course_id in summer:
254
+ if course_id in optimizer.courses:
255
+ course_data = optimizer.courses[course_id]
256
+ course_name = course_data.get("name", course_id)
257
+ st.write(f"β€’ **{course_id}**: {course_name}")
258
+ else:
259
+ st.write(f"β€’ {course_id}")
260
+ else:
261
+ st.write("*Break*")
262
+
263
+ # Summary and export
264
+ st.divider()
265
+ col_export1, col_export2 = st.columns(2)
266
+
267
+ with col_export1:
268
+ st.metric("Total Courses", total_courses)
269
+
270
+ with col_export2:
271
+ if st.button("πŸ“₯ Export Plan as JSON", use_container_width=True):
272
+ export_data = {
273
+ "student_profile": {
274
+ "name": name,
275
+ "gpa": gpa,
276
+ "career_goals": career_goal,
277
+ "interests": interests,
278
+ "learning_style": learning_style,
279
+ "time_commitment": time_commit,
280
+ "preferred_difficulty": difficulty,
281
+ "completed_courses": completed_courses_input
282
+ },
283
+ "plan": st.session_state.display_plan,
284
+ "metrics": st.session_state.metrics,
285
+ "generation_info": {
286
+ "plan_type": st.session_state.get('plan_type', 'Unknown'),
287
+ "generation_time": st.session_state.get('generation_time', 0)
288
+ }
289
+ }
290
+ plan_json = json.dumps(export_data, indent=2)
291
+ st.download_button(
292
+ label="Download Complete Plan Data",
293
+ data=plan_json,
294
+ file_name=f"curriculum_plan_{name.replace(' ', '_')}.json",
295
+ mime="application/json"
296
+ )
297
+
298
+ # TAB 2: CURRICULUM MAP
299
+ with tab2:
300
+ st.subheader("πŸ—ΊοΈ Interactive Curriculum Dependency Graph")
301
+
302
+ if not st.session_state.graph_data_loaded:
303
+ st.info("Please load curriculum data in the Plan Generator tab first.")
304
+ else:
305
+ # Controls
306
+ col1, col2 = st.columns([1, 3])
307
+
308
+ with col1:
309
+ show_critical = st.checkbox("Show Critical Path", True)
310
+ if st.session_state.display_plan:
311
+ highlight_plan = st.checkbox("Highlight My Courses", False)
312
+
313
+ # Create visualization
314
+ if st.session_state.visualizer:
315
+ critical_path = []
316
+ if show_critical:
317
+ critical_path = st.session_state.visualizer.find_critical_path()
318
+ if critical_path:
319
+ st.info(f"Critical Path ({len(critical_path)} courses): {' β†’ '.join(critical_path[:5])}...")
320
+
321
+ # Create the plot
322
+ fig = st.session_state.visualizer.create_interactive_plot(critical_path)
323
+ st.plotly_chart(fig, use_container_width=True)
324
+
325
+ # Legend
326
+ with st.expander("πŸ“– How to Read This Graph"):
327
+ st.markdown("""
328
+ **Node (Circle) Size**: Blocking factor - larger circles block more future courses
329
+ **Node Color**: Complexity score - darker = more complex
330
+ **Lines**: Prerequisite relationships
331
+ **Red Path**: Critical path (longest chain)
332
+ **Hover over nodes**: See detailed metrics for each course
333
+
334
+ **Metrics Explained:**
335
+ - **Blocking Factor**: How many courses this prerequisite blocks
336
+ - **Delay Factor**: Length of longest path through this course
337
+ - **Centrality**: How important this course is in the curriculum network
338
+ - **Complexity**: Combined score (research by Prof. Lionelle)
339
+ """)
340
+
341
+ # TAB 3: ANALYTICS
342
+ with tab3:
343
+ st.subheader("πŸ“Š Curriculum Analytics Dashboard")
344
+
345
+ if not st.session_state.graph_data_loaded:
346
+ st.info("Please load curriculum data in the Plan Generator tab first.")
347
+ else:
348
+ # Overall metrics
349
+ col1, col2, col3, col4 = st.columns(4)
350
+
351
+ graph = st.session_state.graph_data
352
+ total_courses = graph.number_of_nodes()
353
+ total_prereqs = graph.number_of_edges()
354
+
355
+ col1.metric("Total Courses", total_courses)
356
+ col2.metric("Total Prerequisites", total_prereqs)
357
+ col3.metric("Avg Prerequisites", f"{total_prereqs/total_courses:.1f}")
358
+
359
+ # Calculate total curriculum complexity
360
+ if st.session_state.visualizer:
361
+ total_complexity = sum(
362
+ st.session_state.visualizer.calculate_metrics(n)['complexity']
363
+ for n in graph.nodes()
364
+ )
365
+ col4.metric("Curriculum Complexity", f"{total_complexity:,.0f}")
366
+
367
+ st.divider()
368
+
369
+ # Most complex courses
370
+ col1, col2 = st.columns(2)
371
+
372
+ with col1:
373
+ st.subheader("Most Complex Courses")
374
+
375
+ if st.session_state.visualizer:
376
+ complexities = []
377
+ for node in graph.nodes():
378
+ metrics = st.session_state.visualizer.calculate_metrics(node)
379
+ complexities.append({
380
+ 'course': node,
381
+ 'name': graph.nodes[node].get('name', ''),
382
+ 'complexity': metrics['complexity'],
383
+ 'blocking': metrics['blocking']
384
+ })
385
+
386
+ complexities.sort(key=lambda x: x['complexity'], reverse=True)
387
+
388
+ for item in complexities[:10]:
389
+ st.write(f"**{item['course']}**: {item['name']}")
390
+ prog_col1, prog_col2 = st.columns([3, 1])
391
+ with prog_col1:
392
+ st.progress(min(item['complexity']/200, 1.0))
393
+ with prog_col2:
394
+ st.caption(f"Blocks: {item['blocking']}")
395
+
396
+ with col2:
397
+ st.subheader("Bottleneck Courses")
398
+ st.caption("(High blocking factor)")
399
+
400
+ if st.session_state.visualizer:
401
+ bottlenecks = sorted(complexities, key=lambda x: x['blocking'], reverse=True)
402
+
403
+ for item in bottlenecks[:10]:
404
+ st.write(f"**{item['course']}**: {item['name']}")
405
+ st.info(f"Blocks {item['blocking']} future courses")
406
+
407
+ # Export to CurricularAnalytics format
408
+ st.divider()
409
+ if st.button("πŸ“€ Export to CurricularAnalytics Format"):
410
+ if st.session_state.visualizer:
411
+ ca_format = st.session_state.visualizer.export_to_curricular_analytics_format({})
412
+ st.download_button(
413
+ "Download CA Format JSON",
414
+ json.dumps(ca_format, indent=2),
415
+ "curriculum_analytics.json",
416
+ "application/json"
417
+ )
418
+
419
+ # Footer
420
+ st.divider()
421
+ st.caption("πŸš€ Powered by Students, For Students")