Spaces:
Running
Running
import gradio as gr | |
import pandas as pd | |
import json | |
import os | |
import re | |
from PyPDF2 import PdfReader | |
from collections import defaultdict | |
from typing import Dict, List, Optional, Tuple, Union | |
import html | |
from pathlib import Path | |
import fitz # PyMuPDF for better PDF text extraction | |
import pytesseract | |
from PIL import Image | |
import io | |
import secrets | |
import string | |
from huggingface_hub import HfApi, HfFolder | |
# ========== CONFIGURATION ========== | |
PROFILES_DIR = "student_profiles" | |
ALLOWED_FILE_TYPES = [".pdf", ".png", ".jpg", ".jpeg"] # Added image support | |
MAX_FILE_SIZE_MB = 5 | |
MIN_AGE = 5 | |
MAX_AGE = 120 | |
SESSION_TOKEN_LENGTH = 32 | |
HF_TOKEN = os.getenv("HF_TOKEN") | |
# Initialize Hugging Face API | |
if HF_TOKEN: | |
hf_api = HfApi(token=HF_TOKEN) | |
HfFolder.save_token(HF_TOKEN) | |
# ========== UTILITY FUNCTIONS ========== | |
def generate_session_token() -> str: | |
"""Generate a random session token for user identification.""" | |
alphabet = string.ascii_letters + string.digits | |
return ''.join(secrets.choice(alphabet) for _ in range(SESSION_TOKEN_LENGTH)) | |
def sanitize_input(text: str) -> str: | |
"""Sanitize user input to prevent XSS and injection attacks.""" | |
return html.escape(text.strip()) | |
def validate_name(name: str) -> str: | |
"""Validate name input.""" | |
name = name.strip() | |
if not name: | |
raise gr.Error("Name cannot be empty") | |
if len(name) > 100: | |
raise gr.Error("Name is too long (max 100 characters)") | |
if any(c.isdigit() for c in name): | |
raise gr.Error("Name cannot contain numbers") | |
return name | |
def validate_age(age: Union[int, float, str]) -> int: | |
"""Validate and convert age input.""" | |
try: | |
age_int = int(age) | |
if not MIN_AGE <= age_int <= MAX_AGE: | |
raise gr.Error(f"Age must be between {MIN_AGE} and {MAX_AGE}") | |
return age_int | |
except (ValueError, TypeError): | |
raise gr.Error("Please enter a valid age number") | |
def validate_file(file_obj) -> None: | |
"""Validate uploaded file.""" | |
if not file_obj: | |
raise gr.Error("No file uploaded") | |
file_ext = os.path.splitext(file_obj.name)[1].lower() | |
if file_ext not in ALLOWED_FILE_TYPES: | |
raise gr.Error(f"Invalid file type. Allowed: {', '.join(ALLOWED_FILE_TYPES)}") | |
file_size = os.path.getsize(file_obj.name) / (1024 * 1024) # MB | |
if file_size > MAX_FILE_SIZE_MB: | |
raise gr.Error(f"File too large. Max size: {MAX_FILE_SIZE_MB}MB") | |
def extract_text_with_ocr(file_path: str) -> str: | |
"""Extract text from image files using OCR.""" | |
try: | |
image = Image.open(file_path) | |
text = pytesseract.image_to_string(image) | |
return text | |
except Exception as e: | |
raise gr.Error(f"OCR processing failed: {str(e)}") | |
# ========== TRANSCRIPT PARSING ========== | |
def extract_gpa(text: str, gpa_type: str) -> str: | |
"""Extract GPA information from text with validation.""" | |
patterns = [ | |
rf'{gpa_type}\s*GPA\s*:\s*([\d\.]+)', # "Weighted GPA: 3.5" | |
rf'{gpa_type}\s*GPA\s*([\d\.]+)', # "Weighted GPA 3.5" | |
rf'{gpa_type}\s*:\s*([\d\.]+)', # "Weighted: 3.5" | |
rf'{gpa_type}\s*([\d\.]+)' # "Weighted 3.5" | |
] | |
for pattern in patterns: | |
match = re.search(pattern, text, re.IGNORECASE) | |
if match: | |
gpa_value = match.group(1) | |
try: | |
gpa_float = float(gpa_value) | |
if not 0.0 <= gpa_float <= 5.0: # Assuming 5.0 is max for weighted GPA | |
return "Invalid GPA" | |
return gpa_value | |
except ValueError: | |
continue | |
return "N/A" | |
def extract_courses_from_table(text: str) -> Dict[str, List[Dict]]: | |
"""Extract course information with multiple pattern fallbacks.""" | |
# Enhanced patterns to handle more transcript formats | |
patterns = [ | |
# Pattern 1: Structured table format | |
re.compile( | |
r'(\d{4}-\d{4})\s*' # School year | |
r'\|?\s*(\d+)\s*' # Grade level | |
r'\|?\s*([A-Z0-9]+)\s*' # Course code | |
r'\|?\s*([^\|]+?)\s*' # Course name | |
r'(?:\|\s*[^\|]*){2}' # Skip Term and DstNumber | |
r'\|\s*([A-FW][+-]?)\s*' # Grade (FG column) | |
r'(?:\|\s*[^\|]*)' # Skip Incl column | |
r'\|\s*([\d\.]+|inProgress)' # Credits | |
), | |
# Pattern 2: Less structured format | |
re.compile( | |
r'(\d{4}-\d{4})\s+' # School year | |
r'(\d+)\s+' # Grade level | |
r'([A-Z0-9]+)\s+' # Course code | |
r'(.+?)\s+' # Course name | |
r'([A-FW][+-]?)\s*' # Grade | |
r'([\d\.]+|inProgress)' # Credits | |
), | |
# Pattern 3: Semester-based format | |
re.compile( | |
r'(Fall|Spring|Summer)\s+(\d{4})\s+' # Term and year | |
r'(\d+)\s+' # Grade level | |
r'([A-Z0-9]+)\s+' # Course code | |
r'(.+?)\s+' # Course name | |
r'([A-FW][+-]?)\s*' # Grade | |
r'([\d\.]+)' # Credits | |
) | |
] | |
courses_by_grade = defaultdict(list) | |
for pattern in patterns: | |
for match in re.finditer(pattern, text): | |
if len(match.groups()) == 6: | |
year_range, grade_level, course_code, course_name, grade, credits = match.groups() | |
term = None | |
else: | |
term, year, grade_level, course_code, course_name, grade, credits = match.groups() | |
year_range = f"{term} {year}" | |
# Clean and format course information | |
course_name = course_name.strip() | |
if 'DE:' in course_name: | |
course_name = course_name.replace('DE:', 'Dual Enrollment:') | |
if 'AP' in course_name and 'AP ' not in course_name: | |
course_name = course_name.replace('AP', 'AP ') | |
course_info = { | |
'name': f"{course_code} {course_name}", | |
'year': year_range, | |
'credits': credits if credits != 'inProgress' else 'In Progress' | |
} | |
if grade and grade.strip(): | |
course_info['grade'] = grade.strip() | |
courses_by_grade[grade_level].append(course_info) | |
if courses_by_grade: # If we found matches with this pattern, stop | |
break | |
return courses_by_grade | |
def parse_transcript(file_obj) -> Tuple[str, Optional[Dict]]: | |
"""Parse transcript file with robust error handling and OCR support.""" | |
try: | |
if not file_obj: | |
raise gr.Error("Please upload a file first") | |
validate_file(file_obj) | |
text = '' | |
file_ext = os.path.splitext(file_obj.name)[1].lower() | |
try: | |
if file_ext == '.pdf': | |
# Try PyMuPDF first for better text extraction | |
try: | |
doc = fitz.open(file_obj.name) | |
for page in doc: | |
text += page.get_text() + '\n' | |
except: | |
# Fallback to PyPDF2 | |
reader = PdfReader(file_obj.name) | |
for page in reader.pages: | |
page_text = page.extract_text() | |
if page_text: | |
text += page_text + '\n' | |
elif file_ext in ['.png', '.jpg', '.jpeg']: | |
text = extract_text_with_ocr(file_obj.name) | |
except Exception as e: | |
raise gr.Error(f"Error processing file: {str(e)}") | |
if not text.strip(): | |
raise gr.Error("No text could be extracted from the file") | |
# Enhanced GPA extraction | |
gpa_data = { | |
'weighted': extract_gpa(text, 'Weighted'), | |
'unweighted': extract_gpa(text, 'Unweighted') | |
} | |
# Extract grade level with multiple fallback patterns | |
grade_match = ( | |
re.search(r'Current Grade:\s*(\d+)', text) or | |
re.search(r'Grade\s*:\s*(\d+)', text) or | |
re.search(r'Grade\s+(\d+)', text) or | |
re.search(r'Grade\s+Level:\s*(\d+)', text) or | |
re.search(r'Grade\s*\(?\s*(\d+)\s*\)?', text) | |
) | |
grade_level = grade_match.group(1) if grade_match else "Unknown" | |
courses_by_grade = extract_courses_from_table(text) | |
# Format output text | |
output_text = f"Student Transcript Summary\n{'='*40}\n" | |
output_text += f"Current Grade Level: {grade_level}\n" | |
output_text += f"Weighted GPA: {gpa_data['weighted']}\n" | |
output_text += f"Unweighted GPA: {gpa_data['unweighted']}\n\n" | |
output_text += "Course History:\n{'='*40}\n" | |
for grade in sorted(courses_by_grade.keys(), key=lambda x: int(x) if x.isdigit() else x): | |
output_text += f"\nGrade {grade}:\n{'-'*30}\n" | |
for course in courses_by_grade[grade]: | |
output_text += f"- {course['name']}" | |
if 'grade' in course and course['grade']: | |
output_text += f" (Grade: {course['grade']})" | |
if 'credits' in course: | |
output_text += f" | Credits: {course['credits']}" | |
output_text += f" | Year: {course['year']}\n" | |
return output_text, { | |
"gpa": gpa_data, | |
"grade_level": grade_level, | |
"courses": dict(courses_by_grade) | |
} | |
except Exception as e: | |
return f"Error processing transcript: {str(e)}", None | |
# ========== LEARNING STYLE QUIZ ========== | |
class LearningStyleQuiz: | |
def __init__(self): | |
self.questions = [ | |
"When you study for a test, you prefer to:", | |
"When you need directions to a new place, you prefer:", | |
"When you learn a new skill, you prefer to:", | |
"When you're trying to concentrate, you:", | |
"When you meet new people, you remember them by:", | |
"When you're assembling furniture or a gadget, you:", | |
"When choosing a restaurant, you rely most on:", | |
"When you're in a waiting room, you typically:", | |
"When giving someone instructions, you tend to:", | |
"When you're trying to recall information, you:", | |
"When you're at a museum or exhibit, you:", | |
"When you're learning a new language, you prefer:", | |
"When you're taking notes in class, you:", | |
"When you're explaining something complex, you:", | |
"When you're at a party, you enjoy:", | |
"When you're trying to remember a phone number, you:", | |
"When you're relaxing, you prefer to:", | |
"When you're learning to use new software, you:", | |
"When you're giving a presentation, you rely on:", | |
"When you're solving a difficult problem, you:" | |
] | |
self.options = [ | |
["Read the textbook (Reading/Writing)", "Listen to lectures (Auditory)", "Use diagrams/charts (Visual)", "Practice problems (Kinesthetic)"], | |
["Look at a map (Visual)", "Have someone tell you (Auditory)", "Write down directions (Reading/Writing)", "Try walking/driving there (Kinesthetic)"], | |
["Read instructions (Reading/Writing)", "Have someone show you (Visual)", "Listen to explanations (Auditory)", "Try it yourself (Kinesthetic)"], | |
["Need quiet (Reading/Writing)", "Need background noise (Auditory)", "Need to move around (Kinesthetic)", "Need visual stimulation (Visual)"], | |
["Their face (Visual)", "Their name (Auditory)", "What you talked about (Reading/Writing)", "What you did together (Kinesthetic)"], | |
["Read the instructions carefully (Reading/Writing)", "Look at the diagrams (Visual)", "Ask someone to explain (Auditory)", "Start putting pieces together (Kinesthetic)"], | |
["Online photos of the food (Visual)", "Recommendations from friends (Auditory)", "Reading the menu online (Reading/Writing)", "Remembering how it felt to eat there (Kinesthetic)"], | |
["Read magazines (Reading/Writing)", "Listen to music (Auditory)", "Watch TV (Visual)", "Fidget or move around (Kinesthetic)"], | |
["Write them down (Reading/Writing)", "Explain verbally (Auditory)", "Demonstrate (Visual)", "Guide them physically (Kinesthetic)"], | |
["See written words in your mind (Visual)", "Hear the information in your head (Auditory)", "Write it down to remember (Reading/Writing)", "Associate it with physical actions (Kinesthetic)"], | |
["Read all the descriptions (Reading/Writing)", "Listen to audio guides (Auditory)", "Look at the displays (Visual)", "Touch interactive exhibits (Kinesthetic)"], | |
["Study grammar rules (Reading/Writing)", "Listen to native speakers (Auditory)", "Use flashcards with images (Visual)", "Practice conversations (Kinesthetic)"], | |
["Write detailed paragraphs (Reading/Writing)", "Record the lecture (Auditory)", "Draw diagrams and charts (Visual)", "Doodle while listening (Kinesthetic)"], | |
["Write detailed steps (Reading/Writing)", "Explain verbally with examples (Auditory)", "Draw diagrams (Visual)", "Use physical objects to demonstrate (Kinesthetic)"], | |
["Conversations with people (Auditory)", "Watching others or the environment (Visual)", "Writing notes or texting (Reading/Writing)", "Dancing or physical activities (Kinesthetic)"], | |
["See the numbers in your head (Visual)", "Say them aloud (Auditory)", "Write them down (Reading/Writing)", "Dial them on a keypad (Kinesthetic)"], | |
["Read a book (Reading/Writing)", "Listen to music (Auditory)", "Watch TV/movies (Visual)", "Do something physical (Kinesthetic)"], | |
["Read the manual (Reading/Writing)", "Ask someone to show you (Visual)", "Call tech support (Auditory)", "Experiment with the software (Kinesthetic)"], | |
["Detailed notes (Reading/Writing)", "Verbal explanations (Auditory)", "Visual slides (Visual)", "Physical demonstrations (Kinesthetic)"], | |
["Write out possible solutions (Reading/Writing)", "Talk through it with someone (Auditory)", "Draw diagrams (Visual)", "Build a model or prototype (Kinesthetic)"] | |
] | |
self.learning_styles = { | |
"Visual": { | |
"description": "Visual learners prefer using images, diagrams, and spatial understanding.", | |
"tips": [ | |
"Use color coding in your notes", | |
"Create mind maps and diagrams", | |
"Watch educational videos", | |
"Use flashcards with images", | |
"Highlight important information in different colors" | |
], | |
"careers": [ | |
"Graphic Designer", "Architect", "Photographer", | |
"Engineer", "Surgeon", "Pilot" | |
] | |
}, | |
"Auditory": { | |
"description": "Auditory learners learn best through listening and speaking.", | |
"tips": [ | |
"Record lectures and listen to them", | |
"Participate in study groups", | |
"Explain concepts out loud to yourself", | |
"Use rhymes or songs to remember information", | |
"Listen to educational podcasts" | |
], | |
"careers": [ | |
"Musician", "Journalist", "Lawyer", | |
"Psychologist", "Teacher", "Customer Service" | |
] | |
}, | |
"Reading/Writing": { | |
"description": "These learners prefer information displayed as words.", | |
"tips": [ | |
"Write detailed notes", | |
"Create summaries in your own words", | |
"Read textbooks and articles", | |
"Make lists to organize information", | |
"Rewrite your notes to reinforce learning" | |
], | |
"careers": [ | |
"Writer", "Researcher", "Editor", | |
"Accountant", "Programmer", "Historian" | |
] | |
}, | |
"Kinesthetic": { | |
"description": "Kinesthetic learners learn through movement and hands-on activities.", | |
"tips": [ | |
"Use hands-on activities", | |
"Take frequent movement breaks", | |
"Create physical models", | |
"Associate information with physical actions", | |
"Study while walking or pacing" | |
], | |
"careers": [ | |
"Athlete", "Chef", "Mechanic", | |
"Dancer", "Physical Therapist", "Carpenter" | |
] | |
} | |
} | |
def evaluate_quiz(self, *answers) -> str: | |
"""Evaluate quiz answers and generate enhanced results.""" | |
answers = list(answers) # Convert tuple to list | |
if len(answers) != len(self.questions): | |
raise gr.Error("Not all questions were answered") | |
scores = {style: 0 for style in self.learning_styles} | |
for i, answer in enumerate(answers): | |
if not answer: | |
continue # Skip unanswered questions | |
for j, style in enumerate(self.learning_styles): | |
if answer == self.options[i][j]: | |
scores[style] += 1 | |
break | |
total_answered = sum(1 for ans in answers if ans) | |
if total_answered == 0: | |
raise gr.Error("No answers provided") | |
percentages = {style: (score/total_answered)*100 for style, score in scores.items()} | |
sorted_styles = sorted(scores.items(), key=lambda x: x[1], reverse=True) | |
# Generate enhanced results report | |
result = "## Your Learning Style Results\n\n" | |
result += "### Scores:\n" | |
for style, score in sorted_styles: | |
result += f"- **{style}**: {score}/{total_answered} ({percentages[style]:.1f}%)\n" | |
max_score = max(scores.values()) | |
primary_styles = [style for style, score in scores.items() if score == max_score] | |
result += "\n### Analysis:\n" | |
if len(primary_styles) == 1: | |
primary_style = primary_styles[0] | |
style_info = self.learning_styles[primary_style] | |
result += f"Your primary learning style is **{primary_style}**\n\n" | |
result += f"**{primary_style} Characteristics**:\n" | |
result += f"{style_info['description']}\n\n" | |
result += "**Recommended Study Strategies**:\n" | |
for tip in style_info['tips']: | |
result += f"- {tip}\n" | |
result += "\n**Potential Career Paths**:\n" | |
for career in style_info['careers'][:6]: | |
result += f"- {career}\n" | |
# Add complementary strategies | |
complementary = [s for s in sorted_styles if s[0] != primary_style][0][0] | |
result += f"\nYou might also benefit from some **{complementary}** strategies:\n" | |
for tip in self.learning_styles[complementary]['tips'][:3]: | |
result += f"- {tip}\n" | |
else: | |
result += "You have multiple strong learning styles:\n" | |
for style in primary_styles: | |
result += f"- **{style}**\n" | |
result += "\n**Combined Learning Strategies**:\n" | |
result += "You may benefit from combining different learning approaches:\n" | |
for style in primary_styles: | |
result += f"\n**{style}** techniques:\n" | |
for tip in self.learning_styles[style]['tips'][:2]: | |
result += f"- {tip}\n" | |
result += f"\n**{style}** career suggestions:\n" | |
for career in self.learning_styles[style]['careers'][:3]: | |
result += f"- {career}\n" | |
return result | |
# Initialize quiz instance | |
learning_style_quiz = LearningStyleQuiz() | |
# ========== PROFILE MANAGEMENT ========== | |
class ProfileManager: | |
def __init__(self): | |
self.profiles_dir = Path(PROFILES_DIR) | |
self.profiles_dir.mkdir(exist_ok=True, parents=True) | |
self.current_session = None | |
def set_session(self, session_token: str) -> None: | |
"""Set the current session token.""" | |
self.current_session = session_token | |
def get_profile_path(self, name: str) -> Path: | |
"""Get profile path with session token if available.""" | |
if self.current_session: | |
return self.profiles_dir / f"{name.replace(' ', '_')}_{self.current_session}_profile.json" | |
return self.profiles_dir / f"{name.replace(' ', '_')}_profile.json" | |
def save_profile(self, name: str, age: Union[int, str], interests: str, | |
transcript: Dict, learning_style: str, | |
movie: str, movie_reason: str, show: str, show_reason: str, | |
book: str, book_reason: str, character: str, character_reason: str, | |
blog: str) -> str: | |
"""Save student profile with validation.""" | |
try: | |
# Validate required fields | |
name = validate_name(name) | |
age = validate_age(age) | |
interests = sanitize_input(interests) | |
# Prepare favorites data | |
favorites = { | |
"movie": sanitize_input(movie), | |
"movie_reason": sanitize_input(movie_reason), | |
"show": sanitize_input(show), | |
"show_reason": sanitize_input(show_reason), | |
"book": sanitize_input(book), | |
"book_reason": sanitize_input(book_reason), | |
"character": sanitize_input(character), | |
"character_reason": sanitize_input(character_reason) | |
} | |
# Prepare full profile data | |
data = { | |
"name": name, | |
"age": age, | |
"interests": interests, | |
"transcript": transcript if transcript else {}, | |
"learning_style": learning_style if learning_style else "Not assessed", | |
"favorites": favorites, | |
"blog": sanitize_input(blog) if blog else "", | |
"session_token": self.current_session | |
} | |
# Save to JSON file | |
filepath = self.get_profile_path(name) | |
with open(filepath, "w", encoding='utf-8') as f: | |
json.dump(data, f, indent=2, ensure_ascii=False) | |
# Upload to HF Hub if token is available | |
if HF_TOKEN: | |
try: | |
hf_api.upload_file( | |
path_or_fileobj=filepath, | |
path_in_repo=f"profiles/{filepath.name}", | |
repo_id="your-username/student-learning-assistant", | |
repo_type="dataset" | |
) | |
except Exception as e: | |
print(f"Failed to upload to HF Hub: {str(e)}") | |
return self._generate_profile_summary(data) | |
except Exception as e: | |
raise gr.Error(f"Error saving profile: {str(e)}") | |
def load_profile(self, name: str = None, session_token: str = None) -> Dict: | |
"""Load profile by name or return the first one found.""" | |
try: | |
if session_token: | |
profile_pattern = f"*{session_token}_profile.json" | |
else: | |
profile_pattern = "*.json" | |
profiles = list(self.profiles_dir.glob(profile_pattern)) | |
if not profiles: | |
return {} | |
if name: | |
# Find profile by name | |
name = name.replace(" ", "_") | |
if session_token: | |
profile_file = self.profiles_dir / f"{name}_{session_token}_profile.json" | |
else: | |
profile_file = self.profiles_dir / f"{name}_profile.json" | |
if not profile_file.exists(): | |
# Try loading from HF Hub | |
if HF_TOKEN: | |
try: | |
hf_api.download_file( | |
path_in_repo=f"profiles/{profile_file.name}", | |
repo_id="your-username/student-learning-assistant", | |
repo_type="dataset", | |
local_dir=self.profiles_dir | |
) | |
except: | |
raise gr.Error(f"No profile found for {name}") | |
else: | |
raise gr.Error(f"No profile found for {name}") | |
else: | |
# Load the first profile found | |
profile_file = profiles[0] | |
with open(profile_file, "r", encoding='utf-8') as f: | |
return json.load(f) | |
except Exception as e: | |
print(f"Error loading profile: {str(e)}") | |
return {} | |
def list_profiles(self, session_token: str = None) -> List[str]: | |
"""List all available profile names for the current session.""" | |
if session_token: | |
profiles = list(self.profiles_dir.glob(f"*{session_token}_profile.json")) | |
else: | |
profiles = list(self.profiles_dir.glob("*.json")) | |
# Extract just the name part (without session token) | |
profile_names = [] | |
for p in profiles: | |
name_part = p.stem.replace("_profile", "") | |
if session_token: | |
name_part = name_part.replace(f"_{session_token}", "") | |
profile_names.append(name_part.replace("_", " ")) | |
return profile_names | |
def _generate_profile_summary(self, data: Dict) -> str: | |
"""Generate markdown summary of the profile.""" | |
transcript = data.get("transcript", {}) | |
favorites = data.get("favorites", {}) | |
learning_style = data.get("learning_style", "Not assessed") | |
markdown = f"""## Student Profile: {data['name']} | |
### Basic Information | |
- **Age:** {data['age']} | |
- **Interests:** {data['interests']} | |
- **Learning Style:** {learning_style.split('##')[0].strip()} | |
### Academic Information | |
{self._format_transcript(transcript)} | |
### Favorites | |
- **Movie:** {favorites.get('movie', 'Not specified')} | |
*Reason:* {favorites.get('movie_reason', 'Not specified')} | |
- **TV Show:** {favorites.get('show', 'Not specified')} | |
*Reason:* {favorites.get('show_reason', 'Not specified')} | |
- **Book:** {favorites.get('book', 'Not specified')} | |
*Reason:* {favorites.get('book_reason', 'Not specified')} | |
- **Character:** {favorites.get('character', 'Not specified')} | |
*Reason:* {favorites.get('character_reason', 'Not specified')} | |
### Personal Blog | |
{data.get('blog', '_No blog provided_')} | |
""" | |
return markdown | |
def _format_transcript(self, transcript: Dict) -> str: | |
"""Format transcript data for display.""" | |
if not transcript or "courses" not in transcript: | |
return "_No transcript information available_" | |
display = "#### Course History\n" | |
courses_by_grade = transcript["courses"] | |
if isinstance(courses_by_grade, dict): | |
for grade in sorted(courses_by_grade.keys(), key=lambda x: int(x) if x.isdigit() else x): | |
display += f"\n**Grade {grade}**\n" | |
for course in courses_by_grade[grade]: | |
display += f"- {course.get('name', 'Unnamed course')}" | |
if 'grade' in course and course['grade']: | |
display += f" (Grade: {course['grade']})" | |
if 'credits' in course: | |
display += f" | Credits: {course['credits']}" | |
display += f" | Year: {course.get('year', 'N/A')}\n" | |
if 'gpa' in transcript: | |
gpa = transcript['gpa'] | |
display += "\n**GPA**\n" | |
display += f"- Unweighted: {gpa.get('unweighted', 'N/A')}\n" | |
display += f"- Weighted: {gpa.get('weighted', 'N/A')}\n" | |
return display | |
# Initialize profile manager | |
profile_manager = ProfileManager() | |
# ========== AI TEACHING ASSISTANT ========== | |
class TeachingAssistant: | |
def __init__(self): | |
self.context_history = [] | |
self.max_context_length = 5 # Keep last 5 exchanges for context | |
def generate_response(self, message: str, history: List[List[Union[str, None]]], session_token: str) -> str: | |
"""Generate personalized response based on student profile and context.""" | |
try: | |
# Load profile with session token | |
profile = profile_manager.load_profile(session_token=session_token) | |
if not profile: | |
return "Please complete and save your profile first using the previous tabs." | |
# Update context history | |
self._update_context(message, history) | |
# Extract profile information | |
name = profile.get("name", "there") | |
learning_style = profile.get("learning_style", "") | |
grade_level = profile.get("transcript", {}).get("grade_level", "unknown") | |
gpa = profile.get("transcript", {}).get("gpa", {}) | |
interests = profile.get("interests", "") | |
courses = profile.get("transcript", {}).get("courses", {}) | |
favorites = profile.get("favorites", {}) | |
# Process message with context | |
response = self._process_message(message, profile) | |
# Add follow-up suggestions | |
if "study" in message.lower() or "learn" in message.lower(): | |
response += "\n\nWould you like me to suggest a study schedule based on your courses?" | |
elif "course" in message.lower() or "class" in message.lower(): | |
response += "\n\nWould you like help finding resources for any of these courses?" | |
return response | |
except Exception as e: | |
print(f"Error generating response: {str(e)}") | |
return "I encountered an error processing your request. Please try again." | |
def _update_context(self, message: str, history: List[List[Union[str, None]]]) -> None: | |
"""Maintain conversation context.""" | |
self.context_history.append({"role": "user", "content": message}) | |
if history: | |
for h in history[-self.max_context_length:]: | |
if h[0]: # User message | |
self.context_history.append({"role": "user", "content": h[0]}) | |
if h[1]: # Assistant message | |
self.context_history.append({"role": "assistant", "content": h[1]}) | |
# Trim to maintain max context length | |
self.context_history = self.context_history[-(self.max_context_length*2):] | |
def _process_message(self, message: str, profile: Dict) -> str: | |
"""Process user message with profile context.""" | |
message_lower = message.lower() | |
# Greetings | |
if any(greet in message_lower for greet in ["hi", "hello", "hey", "greetings"]): | |
return f"Hello {profile.get('name', 'there')}! How can I help you with your learning today?" | |
# Study help | |
study_words = ["study", "learn", "prepare", "exam", "test", "homework"] | |
if any(word in message_lower for word in study_words): | |
return self._generate_study_advice(profile) | |
# Grade help | |
grade_words = ["grade", "gpa", "score", "marks", "results"] | |
if any(word in message_lower for word in grade_words): | |
return self._generate_grade_advice(profile) | |
# Interest help | |
interest_words = ["interest", "hobby", "passion", "extracurricular"] | |
if any(word in message_lower for word in interest_words): | |
return self._generate_interest_advice(profile) | |
# Course help | |
course_words = ["courses", "classes", "transcript", "schedule", "subject"] | |
if any(word in message_lower for word in course_words): | |
return self._generate_course_advice(profile) | |
# Favorites | |
favorite_words = ["movie", "show", "book", "character", "favorite"] | |
if any(word in message_lower for word in favorite_words): | |
return self._generate_favorites_response(profile) | |
# General help | |
if "help" in message_lower: | |
return self._generate_help_response() | |
# Default response | |
return ("I'm your personalized teaching assistant. I can help with study tips, " | |
"grade information, course advice, and more. Try asking about how to " | |
"study effectively or about your course history.") | |
def _generate_study_advice(self, profile: Dict) -> str: | |
"""Generate study advice based on learning style.""" | |
learning_style = profile.get("learning_style", "") | |
response = "" | |
if "Visual" in learning_style: | |
response = ("Based on your visual learning style, I recommend:\n" | |
"- Creating colorful mind maps or diagrams\n" | |
"- Using highlighters to color-code your notes\n" | |
"- Watching educational videos on the topics\n" | |
"- Creating flashcards with images\n\n") | |
elif "Auditory" in learning_style: | |
response = ("Based on your auditory learning style, I recommend:\n" | |
"- Recording your notes and listening to them\n" | |
"- Participating in study groups to discuss concepts\n" | |
"- Explaining the material out loud to yourself\n" | |
"- Finding podcasts or audio lectures on the topics\n\n") | |
elif "Reading/Writing" in learning_style: | |
response = ("Based on your reading/writing learning style, I recommend:\n" | |
"- Writing detailed summaries in your own words\n" | |
"- Creating organized outlines of the material\n" | |
"- Reading additional textbooks or articles\n" | |
"- Rewriting your notes to reinforce learning\n\n") | |
elif "Kinesthetic" in learning_style: | |
response = ("Based on your kinesthetic learning style, I recommend:\n" | |
"- Creating physical models or demonstrations\n" | |
"- Using hands-on activities to learn concepts\n" | |
"- Taking frequent movement breaks while studying\n" | |
"- Associating information with physical actions\n\n") | |
else: | |
response = ("Here are some general study tips:\n" | |
"- Use the Pomodoro technique (25 min study, 5 min break)\n" | |
"- Space out your study sessions over time\n" | |
"- Test yourself with practice questions\n" | |
"- Teach the material to someone else\n\n") | |
# Add time management advice | |
response += ("**Time Management Tips**:\n" | |
"- Create a study schedule and stick to it\n" | |
"- Prioritize difficult subjects when you're most alert\n" | |
"- Break large tasks into smaller, manageable chunks\n" | |
"- Set specific goals for each study session") | |
return response | |
def _generate_grade_advice(self, profile: Dict) -> str: | |
"""Generate response about grades and GPA.""" | |
gpa = profile.get("transcript", {}).get("gpa", {}) | |
courses = profile.get("transcript", {}).get("courses", {}) | |
response = (f"Your GPA information:\n" | |
f"- Unweighted: {gpa.get('unweighted', 'N/A')}\n" | |
f"- Weighted: {gpa.get('weighted', 'N/A')}\n\n") | |
# Identify any failing grades | |
weak_subjects = [] | |
for grade_level, course_list in courses.items(): | |
for course in course_list: | |
if course.get('grade', '').upper() in ['D', 'F']: | |
weak_subjects.append(course.get('name', 'Unknown course')) | |
if weak_subjects: | |
response += ("**Areas for Improvement**:\n" | |
f"You might want to focus on these subjects: {', '.join(weak_subjects)}\n\n") | |
response += ("**Grade Improvement Strategies**:\n" | |
"- Meet with your teachers to discuss your performance\n" | |
"- Identify specific areas where you lost points\n" | |
"- Create a targeted study plan for weak areas\n" | |
"- Practice with past exams or sample questions") | |
return response | |
def _generate_interest_advice(self, profile: Dict) -> str: | |
"""Generate response based on student interests.""" | |
interests = profile.get("interests", "") | |
response = f"I see you're interested in: {interests}\n\n" | |
response += ("**Suggestions**:\n" | |
"- Look for clubs or extracurricular activities related to these interests\n" | |
"- Explore career paths that align with these interests\n" | |
"- Find online communities or forums about these topics\n" | |
"- Consider projects or independent study in these areas") | |
return response | |
def _generate_course_advice(self, profile: Dict) -> str: | |
"""Generate response about courses.""" | |
courses = profile.get("transcript", {}).get("courses", {}) | |
grade_level = profile.get("transcript", {}).get("grade_level", "unknown") | |
response = "Here's a summary of your courses:\n" | |
for grade in sorted(courses.keys(), key=lambda x: int(x) if x.isdigit() else x): | |
response += f"\n**Grade {grade}**:\n" | |
for course in courses[grade]: | |
response += f"- {course.get('name', 'Unnamed course')}" | |
if 'grade' in course: | |
response += f" (Grade: {course['grade']})" | |
response += "\n" | |
response += f"\nAs a grade {grade_level} student, you might want to:\n" | |
if grade_level in ["9", "10"]: | |
response += ("- Focus on building strong foundational skills\n" | |
"- Explore different subjects to find your interests\n" | |
"- Start thinking about college/career requirements") | |
elif grade_level in ["11", "12"]: | |
response += ("- Focus on courses relevant to your college/career goals\n" | |
"- Consider taking AP or advanced courses if available\n" | |
"- Ensure you're meeting graduation requirements") | |
return response | |
def _generate_favorites_response(self, profile: Dict) -> str: | |
"""Generate response about favorite items.""" | |
favorites = profile.get("favorites", {}) | |
response = "I see you enjoy:\n" | |
if favorites.get('movie'): | |
response += f"- Movie: {favorites['movie']} ({favorites.get('movie_reason', 'no reason provided')})\n" | |
if favorites.get('show'): | |
response += f"- TV Show: {favorites['show']} ({favorites.get('show_reason', 'no reason provided')})\n" | |
if favorites.get('book'): | |
response += f"- Book: {favorites['book']} ({favorites.get('book_reason', 'no reason provided')})\n" | |
if favorites.get('character'): | |
response += f"- Character: {favorites['character']} ({favorites.get('character_reason', 'no reason provided')})\n" | |
response += "\nThese preferences suggest you might enjoy:\n" | |
response += "- Similar books/movies in the same genre\n" | |
response += "- Creative projects related to these stories\n" | |
response += "- Analyzing themes or characters in your schoolwork" | |
return response | |
def _generate_help_response(self) -> str: | |
"""Generate help response with available commands.""" | |
return ("""I can help with: | |
- **Study tips**: "How should I study for math?" | |
- **Grade information**: "What's my GPA?" | |
- **Course advice**: "Show me my course history" | |
- **Interest suggestions**: "What clubs match my interests?" | |
- **General advice**: "How can I improve my grades?" | |
Try asking about any of these topics!""") | |
# Initialize teaching assistant | |
teaching_assistant = TeachingAssistant() | |
# ========== GRADIO INTERFACE ========== | |
def create_interface(): | |
with gr.Blocks(theme=gr.themes.Soft(), title="Student Learning Assistant") as app: | |
# Session state | |
session_token = gr.State(value=generate_session_token()) | |
profile_manager.set_session(session_token.value) | |
# Custom CSS for better styling | |
app.css = """ | |
.gradio-container { | |
max-width: 1200px !important; | |
margin: 0 auto; | |
} | |
.tab { | |
padding: 20px; | |
border-radius: 8px; | |
background: white; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
} | |
.progress-bar { | |
height: 5px; | |
background: linear-gradient(to right, #4CAF50, #8BC34A); | |
margin-bottom: 15px; | |
border-radius: 3px; | |
} | |
.quiz-question { | |
margin-bottom: 15px; | |
padding: 15px; | |
background: #f5f5f5; | |
border-radius: 5px; | |
} | |
.profile-card { | |
border: 1px solid #e0e0e0; | |
border-radius: 8px; | |
padding: 15px; | |
margin-bottom: 15px; | |
background: white; | |
} | |
.chatbot { | |
min-height: 500px; | |
} | |
""" | |
gr.Markdown(""" | |
# Student Learning Assistant | |
**Your personalized education companion** | |
Complete each step to get customized learning recommendations. | |
""") | |
# Progress tracker | |
with gr.Row(): | |
with gr.Column(scale=1): | |
step1 = gr.Button("1. Upload Transcript", variant="primary") | |
with gr.Column(scale=1): | |
step2 = gr.Button("2. Learning Style Quiz") | |
with gr.Column(scale=1): | |
step3 = gr.Button("3. Personal Questions") | |
with gr.Column(scale=1): | |
step4 = gr.Button("4. Save & Review") | |
with gr.Column(scale=1): | |
step5 = gr.Button("5. AI Assistant") | |
# Main tabs | |
with gr.Tabs() as tabs: | |
# ===== TAB 1: Transcript Upload ===== | |
with gr.Tab("Transcript Upload", id=0) as tab1: | |
with gr.Row(): | |
with gr.Column(scale=1): | |
gr.Markdown("### Step 1: Upload Your Transcript") | |
gr.Markdown("Upload a PDF or image of your academic transcript to analyze your courses and GPA.") | |
with gr.Group(): | |
transcript_file = gr.File( | |
label="Transcript (PDF or Image)", | |
file_types=ALLOWED_FILE_TYPES, | |
type="filepath" | |
) | |
upload_btn = gr.Button("Upload & Analyze", variant="primary") | |
gr.Markdown(""" | |
**Supported Formats**: PDF, PNG, JPG | |
**Note**: Your file is processed locally and not stored permanently. | |
""") | |
with gr.Column(scale=2): | |
transcript_output = gr.Textbox( | |
label="Transcript Analysis", | |
lines=20, | |
interactive=False | |
) | |
transcript_data = gr.State() | |
upload_btn.click( | |
fn=parse_transcript, | |
inputs=transcript_file, | |
outputs=[transcript_output, transcript_data] | |
) | |
# ===== TAB 2: Learning Style Quiz ===== | |
with gr.Tab("Learning Style Quiz", id=1) as tab2: | |
with gr.Row(): | |
with gr.Column(scale=1): | |
gr.Markdown("### Step 2: Discover Your Learning Style") | |
gr.Markdown("Complete this 20-question quiz to identify whether you're a visual, auditory, reading/writing, or kinesthetic learner.") | |
progress = gr.HTML("<div class='progress-bar' style='width: 0%'></div>") | |
quiz_submit = gr.Button("Submit Quiz", variant="primary") | |
with gr.Column(scale=2): | |
quiz_components = [] | |
with gr.Accordion("Quiz Questions", open=True): | |
for i, (question, options) in enumerate(zip(learning_style_quiz.questions, learning_style_quiz.options)): | |
with gr.Group(elem_classes="quiz-question"): | |
q = gr.Radio( | |
options, | |
label=f"{i+1}. {question}", | |
show_label=True | |
) | |
quiz_components.append(q) | |
learning_output = gr.Markdown( | |
label="Your Learning Style Results", | |
visible=False | |
) | |
# Update progress bar as questions are answered | |
for component in quiz_components: | |
component.change( | |
fn=lambda *answers: { | |
progress: gr.HTML( | |
f"<div class='progress-bar' style='width: {sum(1 for a in answers if a)/len(answers)*100}%'></div>" | |
) | |
}, | |
inputs=quiz_components, | |
outputs=progress | |
) | |
quiz_submit.click( | |
fn=lambda *answers: learning_style_quiz.evaluate_quiz(*answers), | |
inputs=quiz_components, | |
outputs=learning_output | |
).then( | |
fn=lambda: gr.Markdown(visible=True), | |
outputs=learning_output | |
) | |
# ===== TAB 3: Personal Questions ===== | |
with gr.Tab("Personal Profile", id=2) as tab3: | |
with gr.Row(): | |
with gr.Column(scale=1): | |
gr.Markdown("### Step 3: Tell Us About Yourself") | |
gr.Markdown("This information helps us provide personalized recommendations.") | |
with gr.Group(): | |
name = gr.Textbox(label="Full Name", placeholder="Your name") | |
age = gr.Number(label="Age", minimum=MIN_AGE, maximum=MAX_AGE, precision=0) | |
interests = gr.Textbox( | |
label="Your Interests/Hobbies", | |
placeholder="e.g., Science, Music, Sports, Art..." | |
) | |
gr.Markdown("### Favorites") | |
with gr.Group(): | |
movie = gr.Textbox(label="Favorite Movie") | |
movie_reason = gr.Textbox(label="Why do you like it?", lines=2) | |
show = gr.Textbox(label="Favorite TV Show") | |
show_reason = gr.Textbox(label="Why do you like it?", lines=2) | |
book = gr.Textbox(label="Favorite Book") | |
book_reason = gr.Textbox(label="Why do you like it?", lines=2) | |
character = gr.Textbox(label="Favorite Character (from any story)") | |
character_reason = gr.Textbox(label="Why do you like them?", lines=2) | |
with gr.Column(scale=1): | |
gr.Markdown("### Additional Information") | |
blog_checkbox = gr.Checkbox( | |
label="Would you like to write a short blog about your learning experiences?", | |
value=False | |
) | |
blog_text = gr.Textbox( | |
label="Your Learning Blog", | |
placeholder="Write about your learning journey, challenges, goals...", | |
lines=8, | |
visible=False | |
) | |
blog_checkbox.change( | |
lambda x: gr.update(visible=x), | |
inputs=blog_checkbox, | |
outputs=blog_text | |
) | |
# ===== TAB 4: Save & Review ===== | |
with gr.Tab("Save Profile", id=3) as tab4: | |
with gr.Row(): | |
with gr.Column(scale=1): | |
gr.Markdown("### Step 4: Review & Save Your Profile") | |
gr.Markdown("Verify your information before saving. You can return to previous steps to make changes.") | |
save_btn = gr.Button("Save Profile", variant="primary") | |
# Profile management section | |
with gr.Group(): | |
load_profile_dropdown = gr.Dropdown( | |
label="Load Existing Profile", | |
choices=profile_manager.list_profiles(session_token.value), | |
visible=bool(profile_manager.list_profiles(session_token.value)) | |
) | |
with gr.Row(): | |
load_btn = gr.Button("Load", visible=bool(profile_manager.list_profiles(session_token.value))) | |
delete_btn = gr.Button("Delete", variant="stop", visible=bool(profile_manager.list_profiles(session_token.value))) | |
clear_btn = gr.Button("Clear Form") | |
with gr.Column(scale=2): | |
output_summary = gr.Markdown( | |
"Your profile summary will appear here after saving.", | |
label="Profile Summary" | |
) | |
# Save profile | |
save_btn.click( | |
fn=profile_manager.save_profile, | |
inputs=[ | |
name, age, interests, transcript_data, learning_output, | |
movie, movie_reason, show, show_reason, | |
book, book_reason, character, character_reason, blog_text | |
], | |
outputs=output_summary | |
).then( | |
fn=lambda: profile_manager.list_profiles(session_token.value), | |
outputs=load_profile_dropdown | |
).then( | |
fn=lambda: gr.update(visible=True), | |
outputs=load_btn | |
).then( | |
fn=lambda: gr.update(visible=True), | |
outputs=delete_btn | |
) | |
# Load profile | |
load_btn.click( | |
fn=lambda name: profile_manager.load_profile(name, session_token.value), | |
inputs=load_profile_dropdown, | |
outputs=output_summary | |
) | |
# Delete profile | |
def delete_profile(name, session_token): | |
if not name: | |
raise gr.Error("Please select a profile to delete") | |
try: | |
profile_path = profile_manager.get_profile_path(name) | |
if profile_path.exists(): | |
profile_path.unlink() | |
return "Profile deleted successfully", "" | |
except Exception as e: | |
raise gr.Error(f"Error deleting profile: {str(e)}") | |
delete_btn.click( | |
fn=delete_profile, | |
inputs=[load_profile_dropdown, session_token], | |
outputs=[output_summary, load_profile_dropdown] | |
).then( | |
fn=lambda: gr.update( | |
choices=profile_manager.list_profiles(session_token.value), | |
visible=bool(profile_manager.list_profiles(session_token.value)) | |
), | |
outputs=load_profile_dropdown | |
).then( | |
fn=lambda: gr.update(visible=bool(profile_manager.list_profiles(session_token.value))), | |
outputs=load_btn | |
).then( | |
fn=lambda: gr.update(visible=bool(profile_manager.list_profiles(session_token.value))), | |
outputs=delete_btn | |
) | |
# Clear form | |
clear_btn.click( | |
fn=lambda: [gr.update(value="") for _ in range(12)], | |
outputs=[ | |
name, age, interests, | |
movie, movie_reason, show, show_reason, | |
book, book_reason, character, character_reason, | |
blog_text | |
] | |
) | |
# ===== TAB 5: AI Teaching Assistant ===== | |
with gr.Tab("AI Assistant", id=4) as tab5: | |
gr.Markdown("## Your Personalized Learning Assistant") | |
gr.Markdown("Ask me anything about studying, your courses, grades, or learning strategies.") | |
# Chat interface with session token | |
chatbot = gr.ChatInterface( | |
fn=lambda msg, hist: teaching_assistant.generate_response(msg, hist, session_token.value), | |
examples=[ | |
"How should I study for my next math test?", | |
"What's my current GPA?", | |
"Show me my course history", | |
"How can I improve my grades in science?", | |
"What study methods match my learning style?" | |
], | |
title="" | |
) | |
# Tab navigation logic | |
def navigate_to_tab(tab_index: int): | |
return gr.Tabs(selected=tab_index) | |
step1.click( | |
fn=lambda: navigate_to_tab(0), | |
outputs=tabs | |
) | |
step2.click( | |
fn=lambda: navigate_to_tab(1), | |
outputs=tabs | |
) | |
step3.click( | |
fn=lambda: navigate_to_tab(2), | |
outputs=tabs | |
) | |
step4.click( | |
fn=lambda: navigate_to_tab(3), | |
outputs=tabs | |
) | |
step5.click( | |
fn=lambda: navigate_to_tab(4), | |
outputs=tabs | |
) | |
return app | |
# Create the interface | |
app = create_interface() | |
# For Hugging Face Spaces deployment | |
if __name__ == "__main__": | |
app.launch() | |