8674-Project / src /streamlit_app.py
ckharche's picture
Rename src/ui.py to src/streamlit_app.py
be3f7ec verified
import streamlit as st
import pickle
import os
import time
import json
# Import the optimizer and visualizer
from curriculum_optimizer import HybridOptimizer, StudentProfile
from interactive_visualizer import CurriculumVisualizer
# --- Page Configuration ---
st.set_page_config(page_title="Curriculum Optimizer", layout="wide", initial_sidebar_state="expanded")
# Initialize session state
if "display_plan" not in st.session_state:
st.session_state.display_plan = None
if "metrics" not in st.session_state:
st.session_state.metrics = None
if "reasoning" not in st.session_state:
st.session_state.reasoning = ""
if "graph_data_loaded" not in st.session_state:
st.session_state.graph_data_loaded = False
if "last_profile" not in st.session_state:
st.session_state.last_profile = None
if "visualizer" not in st.session_state:
st.session_state.visualizer = None
# Title
st.title("πŸ§‘β€πŸŽ“ Next-Gen Curriculum Optimizer")
# --- Caching and Initialization ---
@st.cache_resource
def get_optimizer():
"""Loads and caches the main optimizer class and its models."""
try:
optimizer = HybridOptimizer()
optimizer.load_models()
return optimizer
except Exception as e:
st.error(f"Fatal error during model loading: {e}")
st.info("Please ensure you have the required libraries installed.")
st.stop()
return None
optimizer = get_optimizer()
# Create tabs
tab1, tab2, tab3 = st.tabs(["πŸ“ Plan Generator", "πŸ—ΊοΈ Curriculum Map", "πŸ“Š Analytics"])
# TAB 1: PLAN GENERATOR (Your existing code)
with tab1:
# --- SIDEBAR FOR STUDENT PROFILE ---
with st.sidebar:
st.header("Student Profile")
name = st.text_input("Name", "Chaitanya Kharche")
gpa = st.slider("GPA", 0.0, 4.0, 3.5, 0.1)
career_goal = st.text_area("Career Goal", "AI Engineer specializing in Large Language Models")
interests = st.text_input("Interests (comma-separated)", "AI, Machine Learning, LLMs, Agentic AI")
learning_style = st.selectbox("Learning Style", ["Visual", "Hands-on", "Auditory"])
time_commit = st.number_input("Weekly Study Hours", 10, 60, 40, 5)
difficulty = st.selectbox("Preferred Difficulty", ["easy", "moderate", "challenging"])
completed_courses_input = st.text_area("Completed Courses (comma-separated)", "CS1800, CS2500")
# Show profile impact
st.markdown("---")
st.markdown("**Profile Impact:**")
if time_commit < 20:
st.info("πŸ•’ Part-time load (3 courses/semester)")
elif time_commit >= 40:
st.info("πŸ”₯ Intensive load (up to 5 courses/semester)")
else:
st.info("πŸ“š Standard load (4 courses/semester)")
if difficulty == "easy":
st.info("😌 Focuses on foundational courses")
elif difficulty == "challenging":
st.info("πŸš€ Includes advanced/specialized courses")
else:
st.info("βš–οΈ Balanced difficulty progression")
# LOAD DATA
st.subheader("1. Load Curriculum Data")
uploaded_file = st.file_uploader("Upload `neu_graph_analyzed_clean.pkl`", type=["pkl"])
if uploaded_file and not st.session_state.graph_data_loaded:
with st.spinner("Loading curriculum data and preparing embeddings..."):
try:
graph_data = pickle.load(uploaded_file)
optimizer.load_data(graph_data)
# Also create visualizer
st.session_state.visualizer = CurriculumVisualizer(graph_data)
st.session_state.graph_data = graph_data
st.session_state.graph_data_loaded = True
st.success(f"Successfully loaded and processed '{uploaded_file.name}'!")
time.sleep(1)
st.rerun()
except Exception as e:
st.error(f"Error processing .pkl file: {e}")
st.session_state.graph_data_loaded = False
elif st.session_state.graph_data_loaded:
st.success("Curriculum data is loaded and ready.")
# GENERATE PLAN
st.subheader("2. Generate a Plan")
if not st.session_state.graph_data_loaded:
st.info("Please load a curriculum file above to enable plan generation.")
else:
# Create student profile
profile = StudentProfile(
completed_courses=[c.strip().upper() for c in completed_courses_input.split(',') if c.strip()],
current_gpa=gpa,
interests=[i.strip() for i in interests.split(',') if i.strip()],
career_goals=career_goal,
learning_style=learning_style,
time_commitment=time_commit,
preferred_difficulty=difficulty
)
# Check if profile changed
profile_changed = st.session_state.last_profile != profile
if profile_changed:
st.session_state.last_profile = profile
col1, col2, col3 = st.columns(3)
if col1.button("🧠 AI-Optimized Plan", use_container_width=True, type="primary"):
with st.spinner("πŸš€ Using LLM for intelligent course selection..."):
start_time = time.time()
result = optimizer.generate_llm_plan(profile)
generation_time = time.time() - start_time
plan_raw = result.get('pathway', {})
st.session_state.reasoning = plan_raw.get("reasoning", "")
st.session_state.metrics = plan_raw.get("complexity_analysis", {})
st.session_state.display_plan = plan_raw
st.session_state.plan_type = "AI-Optimized"
st.session_state.generation_time = generation_time
st.success(f"πŸŽ‰ AI-optimized plan generated in {generation_time:.1f}s!")
if col2.button("⚑ Smart Rule-Based Plan", use_container_width=True):
with st.spinner("Generating personalized rule-based plan..."):
start_time = time.time()
result = optimizer.generate_simple_plan(profile)
generation_time = time.time() - start_time
plan_raw = result.get('pathway', {})
st.session_state.reasoning = plan_raw.get("reasoning", "")
st.session_state.metrics = plan_raw.get("complexity_analysis", {})
st.session_state.display_plan = plan_raw
st.session_state.plan_type = "Smart Rule-Based"
st.session_state.generation_time = generation_time
st.success(f"πŸŽ‰ Smart rule-based plan generated in {generation_time:.1f}s!")
if col3.button("πŸ”„ Clear Plan", use_container_width=True):
st.session_state.display_plan = None
st.session_state.metrics = None
st.session_state.reasoning = ""
st.rerun()
# Show profile change notification
if st.session_state.display_plan and profile_changed:
st.warning("⚠️ Student profile changed! Generate a new plan to see updated recommendations.")
# DISPLAY RESULTS
if st.session_state.display_plan:
st.subheader(f"πŸ“š {st.session_state.get('plan_type', 'Optimized')} Degree Plan")
# Display generation info
col_info1, col_info2, col_info3 = st.columns(3)
with col_info1:
st.metric("Generation Time", f"{st.session_state.get('generation_time', 0):.1f}s")
with col_info2:
st.metric("Plan Type", st.session_state.get('plan_type', 'Unknown'))
with col_info3:
if time_commit < 20:
load_type = "Part-time"
elif time_commit >= 40:
load_type = "Intensive"
else:
load_type = "Standard"
st.metric("Course Load", load_type)
# Display reasoning and metrics
if st.session_state.reasoning or st.session_state.metrics:
st.markdown("##### πŸ“Š Plan Analysis")
if st.session_state.reasoning:
st.info(f"**Strategy:** {st.session_state.reasoning}")
if st.session_state.metrics:
m = st.session_state.metrics
c1, c2, c3, c4 = st.columns(4)
c1.metric("Avg Complexity", f"{m.get('average_semester_complexity', 0):.1f}")
c2.metric("Peak Complexity", f"{m.get('peak_semester_complexity', 0):.1f}")
c3.metric("Total Complexity", f"{m.get('total_complexity', 0):.0f}")
c4.metric("Balance Score", f"{m.get('balance_score (std_dev)', 0):.2f}")
st.divider()
# Display the actual plan
plan = st.session_state.display_plan
total_courses = 0
for year_num in range(1, 5):
year_key = f"year_{year_num}"
year_data = plan.get(year_key, {})
st.markdown(f"### Year {year_num}")
col_fall, col_spring, col_summer = st.columns(3)
# Fall semester
with col_fall:
fall_courses = year_data.get("fall", [])
st.markdown("**πŸ‚ Fall Semester**")
if fall_courses:
for course_id in fall_courses:
if course_id in optimizer.courses:
course_data = optimizer.courses[course_id]
course_name = course_data.get("name", course_id)
st.write(f"β€’ **{course_id}**: {course_name}")
total_courses += 1
else:
st.write(f"β€’ {course_id}")
total_courses += 1
else:
st.write("*No courses scheduled*")
# Spring semester
with col_spring:
spring_courses = year_data.get("spring", [])
st.markdown("**🌸 Spring Semester**")
if spring_courses:
for course_id in spring_courses:
if course_id in optimizer.courses:
course_data = optimizer.courses[course_id]
course_name = course_data.get("name", course_id)
st.write(f"β€’ **{course_id}**: {course_name}")
total_courses += 1
else:
st.write(f"β€’ {course_id}")
total_courses += 1
else:
st.write("*No courses scheduled*")
# Summer
with col_summer:
summer = year_data.get("summer", [])
st.markdown("**β˜€οΈ Summer**")
if summer == "co-op":
st.write("🏒 *Co-op Experience*")
elif summer:
for course_id in summer:
if course_id in optimizer.courses:
course_data = optimizer.courses[course_id]
course_name = course_data.get("name", course_id)
st.write(f"β€’ **{course_id}**: {course_name}")
else:
st.write(f"β€’ {course_id}")
else:
st.write("*Break*")
# Summary and export
st.divider()
col_export1, col_export2 = st.columns(2)
with col_export1:
st.metric("Total Courses", total_courses)
with col_export2:
if st.button("πŸ“₯ Export Plan as JSON", use_container_width=True):
export_data = {
"student_profile": {
"name": name,
"gpa": gpa,
"career_goals": career_goal,
"interests": interests,
"learning_style": learning_style,
"time_commitment": time_commit,
"preferred_difficulty": difficulty,
"completed_courses": completed_courses_input
},
"plan": st.session_state.display_plan,
"metrics": st.session_state.metrics,
"generation_info": {
"plan_type": st.session_state.get('plan_type', 'Unknown'),
"generation_time": st.session_state.get('generation_time', 0)
}
}
plan_json = json.dumps(export_data, indent=2)
st.download_button(
label="Download Complete Plan Data",
data=plan_json,
file_name=f"curriculum_plan_{name.replace(' ', '_')}.json",
mime="application/json"
)
# TAB 2: CURRICULUM MAP
with tab2:
st.subheader("πŸ—ΊοΈ Interactive Curriculum Dependency Graph")
if not st.session_state.graph_data_loaded:
st.info("Please load curriculum data in the Plan Generator tab first.")
else:
# Controls
col1, col2 = st.columns([1, 3])
with col1:
show_critical = st.checkbox("Show Critical Path", True)
if st.session_state.display_plan:
highlight_plan = st.checkbox("Highlight My Courses", False)
# Create visualization
if st.session_state.visualizer:
critical_path = []
if show_critical:
critical_path = st.session_state.visualizer.find_critical_path()
if critical_path:
st.info(f"Critical Path ({len(critical_path)} courses): {' β†’ '.join(critical_path[:5])}...")
# Create the plot
fig = st.session_state.visualizer.create_interactive_plot(critical_path)
st.plotly_chart(fig, use_container_width=True)
# Legend
with st.expander("πŸ“– How to Read This Graph"):
st.markdown("""
**Node (Circle) Size**: Blocking factor - larger circles block more future courses
**Node Color**: Complexity score - darker = more complex
**Lines**: Prerequisite relationships
**Red Path**: Critical path (longest chain)
**Hover over nodes**: See detailed metrics for each course
**Metrics Explained:**
- **Blocking Factor**: How many courses this prerequisite blocks
- **Delay Factor**: Length of longest path through this course
- **Centrality**: How important this course is in the curriculum network
- **Complexity**: Combined score (research by Prof. Lionelle)
""")
# TAB 3: ANALYTICS
with tab3:
st.subheader("πŸ“Š Curriculum Analytics Dashboard")
if not st.session_state.graph_data_loaded:
st.info("Please load curriculum data in the Plan Generator tab first.")
else:
# Overall metrics
col1, col2, col3, col4 = st.columns(4)
graph = st.session_state.graph_data
total_courses = graph.number_of_nodes()
total_prereqs = graph.number_of_edges()
col1.metric("Total Courses", total_courses)
col2.metric("Total Prerequisites", total_prereqs)
col3.metric("Avg Prerequisites", f"{total_prereqs/total_courses:.1f}")
# Calculate total curriculum complexity
if st.session_state.visualizer:
total_complexity = sum(
st.session_state.visualizer.calculate_metrics(n)['complexity']
for n in graph.nodes()
)
col4.metric("Curriculum Complexity", f"{total_complexity:,.0f}")
st.divider()
# Most complex courses
col1, col2 = st.columns(2)
with col1:
st.subheader("Most Complex Courses")
if st.session_state.visualizer:
complexities = []
for node in graph.nodes():
metrics = st.session_state.visualizer.calculate_metrics(node)
complexities.append({
'course': node,
'name': graph.nodes[node].get('name', ''),
'complexity': metrics['complexity'],
'blocking': metrics['blocking']
})
complexities.sort(key=lambda x: x['complexity'], reverse=True)
for item in complexities[:10]:
st.write(f"**{item['course']}**: {item['name']}")
prog_col1, prog_col2 = st.columns([3, 1])
with prog_col1:
st.progress(min(item['complexity']/200, 1.0))
with prog_col2:
st.caption(f"Blocks: {item['blocking']}")
with col2:
st.subheader("Bottleneck Courses")
st.caption("(High blocking factor)")
if st.session_state.visualizer:
bottlenecks = sorted(complexities, key=lambda x: x['blocking'], reverse=True)
for item in bottlenecks[:10]:
st.write(f"**{item['course']}**: {item['name']}")
st.info(f"Blocks {item['blocking']} future courses")
# Export to CurricularAnalytics format
st.divider()
if st.button("πŸ“€ Export to CurricularAnalytics Format"):
if st.session_state.visualizer:
ca_format = st.session_state.visualizer.export_to_curricular_analytics_format({})
st.download_button(
"Download CA Format JSON",
json.dumps(ca_format, indent=2),
"curriculum_analytics.json",
"application/json"
)
# Footer
st.divider()
st.caption("πŸš€ Powered by Students, For Students")