| import gradio as gr |
| from transformers import pipeline |
| import PyPDF2 |
| import re |
| import os |
| import io |
| import random |
| import time |
| import hashlib |
| import secrets |
| import sqlite3 |
| import tempfile |
| from datetime import datetime, timedelta |
| from pathlib import Path |
| from groq import Groq |
|
|
| |
| try: |
| import speech_recognition as sr |
| SPEECH_AVAILABLE = True |
| except ImportError: |
| SPEECH_AVAILABLE = False |
|
|
| try: |
| from gtts import gTTS |
| TTS_AVAILABLE = True |
| except ImportError: |
| TTS_AVAILABLE = False |
|
|
| |
| try: |
| from google import genai |
| GENAI_AVAILABLE = True |
| except ImportError: |
| GENAI_AVAILABLE = False |
| genai = None |
|
|
| |
| hf_token = os.environ.get("HF_TOKEN") |
| gemini_key = os.environ.get("GEMINI_API_KEY") |
| groq_key = os.environ.get("GROQ_API_KEY") |
| dev_password = os.environ.get("DEV_PASSWORD", "dev123") |
|
|
| |
| DATA_DIR = Path("/tmp/data") if os.path.exists("/tmp") else Path("./data") |
| DATA_DIR.mkdir(exist_ok=True) |
| DB_PATH = DATA_DIR / "student_facilitator.db" |
|
|
| |
| gemini_client = None |
| groq_client = None |
|
|
| if gemini_key and GENAI_AVAILABLE: |
| try: |
| gemini_client = genai.Client(api_key=gemini_key) |
| except: |
| try: |
| import google.generativeai as old_genai |
| old_genai.configure(api_key=gemini_key) |
| gemini_client = old_genai |
| except: |
| pass |
|
|
| if groq_key: |
| try: |
| groq_client = Groq(api_key=groq_key) |
| except: |
| pass |
|
|
| |
| summarizer = None |
|
|
| def load_summarizer(): |
| global summarizer |
| if summarizer is None: |
| try: |
| summarizer = pipeline("summarization", model="sshleifer/distilbart-cnn-12-6", device=-1) |
| except: |
| pass |
| return summarizer |
|
|
| |
| class DatabaseManager: |
| def __init__(self, db_path): |
| self.db_path = db_path |
| self.init_database() |
| |
| def get_connection(self): |
| conn = sqlite3.connect(self.db_path) |
| conn.row_factory = sqlite3.Row |
| return conn |
| |
| def init_database(self): |
| conn = self.get_connection() |
| cursor = conn.cursor() |
| cursor.execute("PRAGMA foreign_keys = ON") |
| |
| cursor.execute(""" |
| CREATE TABLE IF NOT EXISTS users ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| username TEXT UNIQUE NOT NULL, |
| password_hash TEXT NOT NULL, |
| name TEXT NOT NULL, |
| email TEXT UNIQUE NOT NULL, |
| role TEXT DEFAULT 'student', |
| created_at TEXT DEFAULT (datetime('now')), |
| last_login TEXT, |
| is_active INTEGER DEFAULT 1, |
| is_dev INTEGER DEFAULT 0 |
| ) |
| """) |
| |
| cursor.execute(""" |
| CREATE TABLE IF NOT EXISTS sessions ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user_id INTEGER NOT NULL, |
| session_token TEXT UNIQUE NOT NULL, |
| created_at TEXT DEFAULT (datetime('now')), |
| expires_at TEXT NOT NULL, |
| is_active INTEGER DEFAULT 1, |
| FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE |
| ) |
| """) |
| |
| cursor.execute(""" |
| CREATE TABLE IF NOT EXISTS quiz_records ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user_id INTEGER NOT NULL, |
| score INTEGER NOT NULL, |
| total_questions INTEGER NOT NULL, |
| percentage REAL NOT NULL, |
| material_summary TEXT, |
| timestamp TEXT DEFAULT (datetime('now')), |
| FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE |
| ) |
| """) |
| |
| cursor.execute(""" |
| CREATE TABLE IF NOT EXISTS activity_logs ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user_id INTEGER, |
| action TEXT NOT NULL, |
| tool_used TEXT, |
| timestamp TEXT DEFAULT (datetime('now')), |
| FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL |
| ) |
| """) |
| |
| conn.commit() |
| conn.close() |
| print(f"Database initialized at {self.db_path}") |
| |
| def create_user(self, username, password_hash, name, email, is_dev=False): |
| try: |
| conn = self.get_connection() |
| cursor = conn.cursor() |
| cursor.execute(""" |
| INSERT INTO users (username, password_hash, name, email, is_dev) |
| VALUES (?, ?, ?, ?, ?) |
| """, (username, password_hash, name, email, is_dev)) |
| user_id = cursor.lastrowid |
| conn.commit() |
| conn.close() |
| return True, user_id |
| except sqlite3.IntegrityError as e: |
| if "username" in str(e).lower(): |
| return False, "Username already exists" |
| elif "email" in str(e).lower(): |
| return False, "Email already registered" |
| return False, str(e) |
| except Exception as e: |
| return False, str(e) |
| |
| def get_user_by_username(self, username): |
| conn = self.get_connection() |
| cursor = conn.cursor() |
| cursor.execute("SELECT * FROM users WHERE username = ? AND is_active = 1", (username,)) |
| user = cursor.fetchone() |
| conn.close() |
| return dict(user) if user else None |
| |
| def update_last_login(self, user_id): |
| conn = self.get_connection() |
| cursor = conn.cursor() |
| cursor.execute("UPDATE users SET last_login = datetime('now') WHERE id = ?", (user_id,)) |
| conn.commit() |
| conn.close() |
| |
| def create_session(self, user_id, session_token, expires_at): |
| conn = self.get_connection() |
| cursor = conn.cursor() |
| cursor.execute("UPDATE sessions SET is_active = 0 WHERE user_id = ?", (user_id,)) |
| cursor.execute(""" |
| INSERT INTO sessions (user_id, session_token, expires_at) |
| VALUES (?, ?, ?) |
| """, (user_id, session_token, expires_at)) |
| conn.commit() |
| conn.close() |
| return True |
| |
| def get_session(self, session_token): |
| conn = self.get_connection() |
| cursor = conn.cursor() |
| cursor.execute(""" |
| SELECT s.*, u.username, u.name, u.role, u.is_dev, u.id as user_id |
| FROM sessions s |
| JOIN users u ON s.user_id = u.id |
| WHERE s.session_token = ? AND s.is_active = 1 |
| AND datetime(s.expires_at) > datetime('now') |
| AND u.is_active = 1 |
| """, (session_token,)) |
| session = cursor.fetchone() |
| conn.close() |
| return dict(session) if session else None |
| |
| def invalidate_session(self, session_token): |
| conn = self.get_connection() |
| cursor = conn.cursor() |
| cursor.execute("UPDATE sessions SET is_active = 0 WHERE session_token = ?", (session_token,)) |
| conn.commit() |
| conn.close() |
| return True |
| |
| def save_quiz_record(self, user_id, score, total, percentage, material_summary=None): |
| conn = self.get_connection() |
| cursor = conn.cursor() |
| cursor.execute(""" |
| INSERT INTO quiz_records (user_id, score, total_questions, percentage, material_summary) |
| VALUES (?, ?, ?, ?, ?) |
| """, (user_id, score, total, percentage, material_summary)) |
| conn.commit() |
| conn.close() |
| return True |
| |
| def get_user_quiz_stats(self, user_id): |
| conn = self.get_connection() |
| cursor = conn.cursor() |
| cursor.execute(""" |
| SELECT COUNT(*) as total_quizzes, |
| AVG(percentage) as avg_score, |
| MAX(percentage) as best_score, |
| MIN(percentage) as worst_score |
| FROM quiz_records WHERE user_id = ? |
| """, (user_id,)) |
| stats = cursor.fetchone() |
| |
| cursor.execute(""" |
| SELECT score, total_questions, percentage, timestamp |
| FROM quiz_records WHERE user_id = ? ORDER BY timestamp DESC LIMIT 10 |
| """, (user_id,)) |
| history = [dict(row) for row in cursor.fetchall()] |
| conn.close() |
| |
| return dict(stats) if stats else None, history |
| |
| def get_database_stats(self): |
| conn = self.get_connection() |
| cursor = conn.cursor() |
| stats = {} |
| cursor.execute("SELECT COUNT(*) FROM users WHERE is_dev = 0") |
| stats['total_users'] = cursor.fetchone()[0] |
| cursor.execute("SELECT COUNT(*) FROM sessions WHERE is_active = 1") |
| stats['active_sessions'] = cursor.fetchone()[0] |
| cursor.execute("SELECT COUNT(*) FROM quiz_records") |
| stats['total_quizzes'] = cursor.fetchone()[0] |
| conn.close() |
| return stats |
|
|
| db = DatabaseManager(DB_PATH) |
|
|
| |
| class UserAuth: |
| def __init__(self, database): |
| self.db = database |
| self.failed_attempts = {} |
| self._init_dev_account() |
| |
| def _hash(self, password): |
| return hashlib.sha256(password.encode()).hexdigest() |
| |
| def _init_dev_account(self): |
| existing = self.db.get_user_by_username("admin") |
| if not existing and dev_password: |
| self.db.create_user( |
| username="admin", |
| password_hash=self._hash(dev_password), |
| name="Developer", |
| email="dev@studentfacilitator.local", |
| is_dev=True |
| ) |
| print("Developer account created") |
| |
| def validate_email(self, email): |
| if not email or len(email) < 12: |
| return False, "Email must be at least 12 characters" |
| if "@" not in email: |
| return False, "Email must contain @" |
| if "." not in email.split("@")[-1]: |
| return False, "Email must have valid domain" |
| if email.count("@") != 1: |
| return False, "Email must contain exactly one @" |
| local, domain = email.split("@") |
| if len(local) < 1 or len(domain) < 3: |
| return False, "Invalid email format" |
| return True, "Valid" |
| |
| def validate_username(self, username): |
| if not username or len(username) < 3: |
| return False, "Username must be at least 3 characters" |
| if not username.isalnum(): |
| return False, "Username must be alphanumeric only" |
| return True, "Valid" |
| |
| def register(self, username, password, name, email): |
| valid, msg = self.validate_username(username) |
| if not valid: |
| return False, msg |
| |
| valid, msg = self.validate_email(email) |
| if not valid: |
| return False, msg |
| |
| if not password or len(password) < 6: |
| return False, "Password must be at least 6 characters" |
| |
| if not name or len(name.strip()) < 2: |
| return False, "Please enter your full name" |
| |
| password_hash = self._hash(password) |
| success, result = self.db.create_user(username, password_hash, name, email) |
| |
| if success: |
| return True, "Account created successfully!" |
| else: |
| return False, result |
| |
| def login(self, username, password): |
| if not username or not password: |
| return None, "Please enter both username and password" |
| |
| if username in self.failed_attempts: |
| if self.failed_attempts[username]["count"] >= 5: |
| last = self.failed_attempts[username]["last_attempt"] |
| if datetime.now() - last < timedelta(minutes=15): |
| return None, "Too many failed attempts. Try again in 15 minutes." |
| else: |
| del self.failed_attempts[username] |
| |
| user = self.db.get_user_by_username(username) |
| |
| if not user: |
| self._record_failed_attempt(username) |
| return None, "Invalid username or password" |
| |
| if user['password_hash'] != self._hash(password): |
| self._record_failed_attempt(username) |
| return None, "Invalid username or password" |
| |
| self.db.update_last_login(user['id']) |
| |
| if username in self.failed_attempts: |
| del self.failed_attempts[username] |
| |
| session_token = secrets.token_urlsafe(32) |
| expires_at = datetime.now() + timedelta(hours=24) |
| |
| self.db.create_session(user['id'], session_token, expires_at) |
| |
| return session_token, { |
| 'user_id': user['id'], |
| 'username': user['username'], |
| 'name': user['name'], |
| 'role': user['role'], |
| 'is_dev': user['is_dev'] |
| } |
| |
| def _record_failed_attempt(self, username): |
| if username not in self.failed_attempts: |
| self.failed_attempts[username] = {"count": 0, "last_attempt": datetime.now()} |
| self.failed_attempts[username]["count"] += 1 |
| self.failed_attempts[username]["last_attempt"] = datetime.now() |
| |
| def validate_session(self, session_token): |
| if not session_token: |
| return None |
| return self.db.get_session(session_token) |
| |
| def logout(self, session_token): |
| if session_token: |
| self.db.invalidate_session(session_token) |
| return True |
|
|
| auth_system = UserAuth(db) |
|
|
| |
| def speech_to_text(audio_file): |
| if not SPEECH_AVAILABLE: |
| return "Speech recognition not available" |
| if not audio_file: |
| return "No audio recorded" |
| try: |
| recognizer = sr.Recognizer() |
| with sr.AudioFile(audio_file) as source: |
| audio = recognizer.record(source) |
| text = recognizer.recognize_google(audio) |
| return text |
| except sr.UnknownValueError: |
| return "Could not understand audio" |
| except sr.RequestError: |
| return "Speech recognition service unavailable" |
| except Exception as e: |
| return f"Error: {str(e)}" |
|
|
| def process_audio_input(audio_file, current_text): |
| if not audio_file: |
| return current_text |
| text = speech_to_text(audio_file) |
| if text.startswith("Error:") or text.startswith("Speech") or text.startswith("No audio") or text.startswith("Could not"): |
| return current_text |
| return current_text + " " + text if current_text else text |
|
|
| def text_to_speech(text, lang='en'): |
| if not TTS_AVAILABLE: |
| return None, "Text-to-speech not available" |
| if not text: |
| return None, "No text to speak" |
| try: |
| tts = gTTS(text=text, lang=lang, slow=False) |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as f: |
| tts.save(f.name) |
| return f.name, "Audio generated" |
| except Exception as e: |
| return None, f"TTS error: {str(e)}" |
|
|
| def speak_text(text, lang='en'): |
| if not text: |
| return None |
| audio_file, error = text_to_speech(text, lang) |
| return audio_file |
|
|
| |
| def extract_text_from_pdf(pdf_file): |
| if pdf_file is None: |
| return None, "Please upload a PDF file." |
| try: |
| if isinstance(pdf_file, str): |
| with open(pdf_file, 'rb') as f: |
| pdf_reader = PyPDF2.PdfReader(f) |
| text = "" |
| for page in pdf_reader.pages: |
| page_text = page.extract_text() |
| if page_text: |
| text += page_text + "\n" |
| else: |
| if hasattr(pdf_file, 'read'): |
| pdf_bytes = pdf_file.read() |
| if hasattr(pdf_file, 'seek'): |
| pdf_file.seek(0) |
| else: |
| pdf_bytes = pdf_file |
| |
| if isinstance(pdf_bytes, bytes): |
| pdf_stream = io.BytesIO(pdf_bytes) |
| else: |
| pdf_stream = io.BytesIO(pdf_bytes.encode() if isinstance(pdf_bytes, str) else pdf_bytes) |
| |
| pdf_reader = PyPDF2.PdfReader(pdf_stream) |
| text = "" |
| for page in pdf_reader.pages: |
| page_text = page.extract_text() |
| if page_text: |
| text += page_text + "\n" |
| |
| text = re.sub(r'\s+', ' ', text).strip() |
| |
| if len(text) < 50: |
| return None, "Could not extract text. PDF may be image-based or scanned." |
| |
| return text, None |
| |
| except Exception as e: |
| return None, f"Error reading PDF: {str(e)}" |
|
|
| def summarize_with_gemini(text, max_length, min_length): |
| if not gemini_client: |
| return None |
| try: |
| if hasattr(gemini_client, 'models'): |
| prompt = f"Summarize the following text in {min_length}-{max_length} words:\n\n{text[:15000]}" |
| try: |
| response = gemini_client.models.generate_content( |
| model="gemini-2.5-flash", |
| contents=prompt |
| ) |
| return response.text |
| except: |
| if hasattr(gemini_client, 'GenerativeModel'): |
| model = gemini_client.GenerativeModel('gemini-2.5-flash') |
| response = model.generate_content(prompt) |
| return response.text |
| elif hasattr(gemini_client, 'GenerativeModel'): |
| model = gemini_client.GenerativeModel('gemini-2.5-flash') |
| prompt = f"Summarize the following text in {min_length}-{max_length} words:\n\n{text[:15000]}" |
| response = model.generate_content(prompt) |
| return response.text |
| except Exception as e: |
| print(f"Gemini error: {e}") |
| return None |
|
|
| def summarize_pdf(pdf_file, max_length, min_length): |
| text, error = extract_text_from_pdf(pdf_file) |
| if error: |
| return error |
| gemini_result = summarize_with_gemini(text, max_length, min_length) |
| if gemini_result: |
| return gemini_result |
| summ = load_summarizer() |
| if summ: |
| try: |
| result = summ(text[:3500], max_length=max_length, min_length=min_length, do_sample=False) |
| return result[0]['summary_text'] |
| except Exception as e: |
| return f"Error: {str(e)}" |
| return "Error: No summarization available" |
|
|
| def generate_essay_with_gemini(prompt, essay_type, word_count, tone): |
| if not gemini_client: |
| return None |
| try: |
| full_prompt = f"""Write a {essay_type} essay in {tone} tone (~{word_count} words). |
| Topic: {prompt} |
| Requirements: Engaging intro, structured body, strong conclusion.""" |
| if hasattr(gemini_client, 'models'): |
| response = gemini_client.models.generate_content( |
| model="gemini-2.5-flash", |
| contents=full_prompt |
| ) |
| essay = response.text.strip() |
| else: |
| model = gemini_client.GenerativeModel('gemini-2.5-flash') |
| response = model.generate_content(full_prompt) |
| essay = response.text.strip() |
| word_count_actual = len(essay.split()) |
| return f"""# {essay_type} Essay: {prompt[:50]}{'...' if len(prompt) > 50 else ''} |
| |
| {essay} |
| |
| --- |
| *~{word_count_actual} words | {tone} tone*""" |
| except Exception as e: |
| print(f"Essay error: {e}") |
| return None |
|
|
| def generate_essay(prompt, essay_type, word_count, tone): |
| if not prompt or len(prompt.strip()) < 10: |
| return "Please provide a detailed prompt (at least 10 characters)." |
| result = generate_essay_with_gemini(prompt, essay_type, word_count, tone) |
| if result: |
| return result |
| return "❌ Essay generation failed. Please check Gemini API configuration." |
|
|
| def summarize_text(text, max_length, min_length): |
| if len(text.strip()) < 100: |
| return "Please provide at least 100 characters." |
| gemini_result = summarize_with_gemini(text, max_length, min_length) |
| if gemini_result: |
| return gemini_result |
| summ = load_summarizer() |
| if summ: |
| try: |
| result = summ(text[:3500], max_length=max_length, min_length=min_length, do_sample=False) |
| return result[0]['summary_text'] |
| except Exception as e: |
| return f"Error: {str(e)}" |
| return "Error: No summarization available" |
|
|
| |
| def extract_sentences(text): |
| if not text or len(text.strip()) < 50: |
| return [] |
| text = re.sub(r'\s+', ' ', text.strip()) |
| sentences = re.split(r'[.!?]+', text) |
| valid_sentences = [] |
| for s in sentences: |
| s = s.strip() |
| words = s.split() |
| if 8 <= len(words) <= 20: |
| if s and s[0].isupper(): |
| valid_sentences.append(s) |
| return valid_sentences[:20] |
|
|
| def create_quiz(text, num_questions): |
| sentences = extract_sentences(text) |
| if len(sentences) < num_questions: |
| num_questions = max(1, len(sentences)) |
| if num_questions == 0: |
| return [] |
| selected = random.sample(sentences, num_questions) |
| quiz_data = [] |
| for sentence in selected: |
| words = sentence.split() |
| if len(words) < 5: |
| continue |
| candidates = [w for w in words[2:-2] if len(w) > 3 and w.isalpha()] |
| if not candidates: |
| candidates = [w for w in words[2:-2] if len(w) > 2] |
| if not candidates: |
| continue |
| keyword = random.choice(candidates) |
| question = sentence.replace(keyword, "________", 1) |
| all_words = list(set([w for w in text.split() if len(w) > 3 and w.isalpha() and w.lower() != keyword.lower()])) |
| if len(all_words) < 3: |
| wrong = ["alternative", "option", "choice", "selection"][:3] |
| else: |
| wrong = random.sample(all_words, min(3, len(all_words))) |
| wrong = [w for w in wrong if w.lower() != keyword.lower()] |
| while len(wrong) < 3: |
| wrong.append(f"option_{len(wrong)+1}") |
| options = wrong[:3] + [keyword] |
| random.shuffle(options) |
| quiz_data.append({ |
| "question": question, |
| "options": options, |
| "answer": keyword, |
| "full_sentence": sentence |
| }) |
| return quiz_data |
|
|
| def start_quiz(text, num_questions, timer_minutes): |
| if not text or not text.strip(): |
| return ( |
| "⚠️ Please enter study material first!", |
| gr.update(choices=[], visible=False), |
| "", |
| None, 0, 0, None, "⏳ --:--", gr.update(visible=False), "" |
| ) |
| if len(text.strip()) < 100: |
| return ( |
| "⚠️ Please provide at least 100 characters!", |
| gr.update(choices=[], visible=False), |
| "", |
| None, 0, 0, None, "⏳ --:--", gr.update(visible=False), "" |
| ) |
| quiz = create_quiz(text, num_questions) |
| if not quiz or len(quiz) == 0: |
| return ( |
| "⚠️ Could not generate quiz. Add more detailed material!", |
| gr.update(choices=[], visible=False), |
| "", |
| None, 0, 0, None, "⏳ --:--", gr.update(visible=False), "" |
| ) |
| actual_questions = len(quiz) |
| end_time = time.time() + (timer_minutes * 60) |
| material_summary = text[:300] + "..." if len(text) > 300 else text |
| return show_question(quiz, 0, 0, end_time, actual_questions, material_summary) |
|
|
| def show_question(quiz, index, score, end_time, total_questions, material_summary): |
| remaining = int(end_time - time.time()) |
| if remaining <= 0: |
| return finish_quiz(quiz, score, index, total_questions, material_summary, time_up=True) |
| if index >= len(quiz): |
| return finish_quiz(quiz, score, len(quiz), total_questions, material_summary) |
| q = quiz[index] |
| mins = remaining // 60 |
| secs = remaining % 60 |
| timer_text = f"⏳ {mins:02d}:{secs:02d}" |
| progress = (index / total_questions) * 20 |
| bar = "█" * int(progress) + "░" * (20 - int(progress)) |
| question_html = f""" |
| <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| color: white; padding: 2rem; border-radius: 20px; text-align: center;"> |
| <div style="font-size: 3rem; font-weight: bold; margin-bottom: 1rem;">{timer_text}</div> |
| <div style="background: rgba(255,255,255,0.2); padding: 0.5rem; border-radius: 10px; margin-bottom: 1rem;"> |
| Question {index + 1} of {total_questions} | Score: {score}/{index if index > 0 else 0} |
| </div> |
| <div style="font-family: monospace; margin-bottom: 1.5rem;">{bar}</div> |
| <h3 style="font-size: 1.3rem; line-height: 1.6; margin-bottom: 2rem;">{q['question']}</h3> |
| </div> |
| """ |
| return ( |
| question_html, |
| gr.update(choices=q['options'], value=None, visible=True), |
| f"Score: {score}/{index if index > 0 else 0}", |
| quiz, index, score, end_time, timer_text, |
| gr.update(visible=True), material_summary |
| ) |
|
|
| def submit_answer(selected, quiz, index, score, end_time, total_questions, material_summary, session_token): |
| if not selected: |
| return show_question(quiz, index, score, end_time, total_questions, material_summary) |
| correct_answer = quiz[index]['answer'] |
| is_correct = selected == correct_answer |
| new_score = score + (1 if is_correct else 0) |
| new_index = index + 1 |
| if new_index >= len(quiz): |
| percentage = (new_score / total_questions * 100) if total_questions > 0 else 0 |
| if session_token: |
| session = auth_system.validate_session(session_token) |
| if session: |
| db.save_quiz_record(session['user_id'], new_score, total_questions, percentage, material_summary) |
| return finish_quiz(quiz, new_score, len(quiz), total_questions, material_summary) |
| return show_question(quiz, new_index, new_score, end_time, total_questions, material_summary) |
|
|
| def finish_quiz(quiz, score, answered, total_questions, material_summary, time_up=False): |
| percentage = (score / total_questions * 100) if total_questions > 0 else 0 |
| if percentage >= 90: |
| grade, emoji, message = "A", "🏆", "Outstanding! Excellent mastery!" |
| elif percentage >= 80: |
| grade, emoji, message = "B", "🌟", "Great job! Very good understanding!" |
| elif percentage >= 70: |
| grade, emoji, message = "C", "👍", "Good work! Keep practicing!" |
| elif percentage >= 60: |
| grade, emoji, message = "D", "📚", "Passing, but more study needed!" |
| else: |
| grade, emoji, message = "F", "💪", "Keep trying! Review the material!" |
| time_msg = "⏰ Time's up! " if time_up else "" |
| result_html = f""" |
| <div style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); |
| color: white; padding: 2.5rem; border-radius: 25px; text-align: center; |
| box-shadow: 0 20px 60px rgba(0,0,0,0.3);"> |
| <div style="font-size: 4rem; margin-bottom: 1rem;">{emoji}</div> |
| <h2 style="font-size: 2rem; margin-bottom: 1rem;">{time_msg}Quiz Complete!</h2> |
| <div style="background: rgba(255,255,255,0.2); padding: 1.5rem; border-radius: 15px; margin: 1.5rem 0;"> |
| <div style="font-size: 3rem; font-weight: bold;">{score}/{total_questions}</div> |
| <div style="font-size: 1.5rem;">{percentage:.1f}%</div> |
| <div style="font-size: 2rem; margin-top: 0.5rem;">Grade: {grade}</div> |
| </div> |
| <p style="font-size: 1.2rem; margin-bottom: 1rem;">{message}</p> |
| <div style="font-size: 0.9rem; opacity: 0.9;">Questions answered: {answered}/{total_questions}</div> |
| </div> |
| """ |
| return ( |
| result_html, gr.update(choices=[], visible=False), |
| f"Final Score: {score}/{total_questions}", quiz, total_questions, score, None, |
| "⏰ Finished", gr.update(visible=False), material_summary |
| ) |
|
|
| def translate_to_urdu(text): |
| if not text or not text.strip(): |
| return "Please enter some text to translate." |
| if not groq_client: |
| return "❌ Groq API not configured." |
| try: |
| chat_completion = groq_client.chat.completions.create( |
| messages=[ |
| { |
| "role": "system", |
| "content": "You are a professional English to Urdu translator. Translate accurately to Urdu (اردو) using natural language. Respond ONLY with the translation." |
| }, |
| { |
| "role": "user", |
| "content": f"Translate to Urdu:\n\n{text}" |
| } |
| ], |
| model="llama-3.3-70b-versatile", |
| temperature=0.3, |
| max_completion_tokens=2048, |
| ) |
| return chat_completion.choices[0].message.content |
| except Exception as e: |
| return f"Error: {str(e)}" |
|
|
| |
| def generate_ascii_chart(data, title, width=40): |
| if not data: |
| return "No data available" |
| max_val = max(data.values()) if data else 1 |
| lines = [f"\n {title}", " " + "=" * width] |
| for label, value in data.items(): |
| bar_len = int((value / max_val) * (width - 10)) |
| bar = "█" * bar_len |
| lines.append(f" {label:8} │{bar:<30} {value}") |
| lines.append(" " + "=" * width) |
| return "\n".join(lines) |
|
|
| def get_user_dashboard_data(session_token): |
| session = auth_system.validate_session(session_token) |
| if not session: |
| return "Please login to view dashboard", "", "" |
| user_id = session['user_id'] |
| name = session['name'] |
| stats, history = db.get_user_quiz_stats(user_id) |
| if not stats or stats['total_quizzes'] == 0: |
| return f"## 📊 {name}'s Dashboard\n\nNo quiz records yet. Take a quiz to see your progress!", "", "" |
| progress_data = {} |
| for i, record in enumerate(reversed(history[-7:])): |
| date = record['timestamp'][:10] |
| progress_data[date] = record['percentage'] |
| progress_chart = generate_ascii_chart(progress_data, "Quiz Performance Over Time") |
| ranges = {"0-40%": 0, "41-60%": 0, "61-80%": 0, "81-100%": 0} |
| for record in history: |
| p = record['percentage'] |
| if p <= 40: ranges["0-40%"] += 1 |
| elif p <= 60: ranges["41-60%"] += 1 |
| elif p <= 80: ranges["61-80%"] += 1 |
| else: ranges["81-100%"] += 1 |
| distribution_chart = generate_ascii_chart(ranges, "Score Distribution") |
| summary = f""" |
| ## 📊 {name}'s Learning Dashboard |
| |
| ### 🎯 Overall Statistics |
| • **Total Quizzes Taken:** {stats['total_quizzes']} |
| • **Average Score:** {stats['avg_score']:.1f}% |
| • **Best Score:** {stats['best_score']:.1f}% |
| • **Improvement Needed:** {stats['worst_score']:.1f}% |
| |
| ### 📈 Recent Activity |
| """ |
| for record in history[:5]: |
| date = record['timestamp'][:10] |
| score_emoji = "🌟" if record['percentage'] >= 80 else "👍" if record['percentage'] >= 60 else "📚" |
| summary += f"\n{score_emoji} **{date}:** {record['score']}/{record['total_questions']} ({record['percentage']:.1f}%)" |
| return summary, progress_chart, distribution_chart |
|
|
| |
| custom_css = """ |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Noto+Nastaliq+Urdu&display=swap'); |
| |
| :root { |
| --primary: #6366f1; |
| --primary-dark: #4f46e5; |
| --secondary: #8b5cf6; |
| --success: #10b981; |
| --warning: #f59e0b; |
| --danger: #ef4444; |
| --dark: #0f172a; |
| --light: #f8fafc; |
| } |
| |
| body { |
| font-family: 'Inter', sans-serif !important; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; |
| min-height: 100vh; |
| } |
| |
| .auth-container { |
| max-width: 450px; |
| margin: 3rem auto; |
| padding: 2.5rem; |
| background: rgba(255, 255, 255, 0.95); |
| border-radius: 24px; |
| box-shadow: 0 25px 80px rgba(0,0,0,0.15); |
| text-align: center; |
| } |
| |
| .auth-logo { |
| width: 80px; |
| height: 80px; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| border-radius: 20px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| margin: 0 auto 1.5rem; |
| font-size: 2.5rem; |
| color: white; |
| } |
| |
| .auth-title { |
| font-size: 1.875rem; |
| font-weight: 800; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| margin-bottom: 0.5rem; |
| } |
| |
| .app-header { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| color: white; |
| padding: 2rem; |
| border-radius: 0 0 30px 30px; |
| margin: -20px -20px 2rem -20px; |
| } |
| |
| .app-title { |
| font-size: 2rem; |
| font-weight: 700; |
| margin: 0; |
| } |
| |
| .user-widget { |
| position: absolute; |
| top: 1.5rem; |
| right: 2rem; |
| background: rgba(255,255,255,0.2); |
| padding: 0.5rem 1rem; |
| border-radius: 50px; |
| display: flex; |
| align-items: center; |
| gap: 0.75rem; |
| font-size: 0.875rem; |
| } |
| |
| .status-pill { |
| padding: 0.5rem 1rem; |
| border-radius: 50px; |
| font-size: 0.8rem; |
| font-weight: 600; |
| } |
| |
| .status-ok { background: #d1fae5; color: #065f46; } |
| .status-error { background: #fee2e2; color: #991b1b; } |
| |
| .tool-card { |
| background: white; |
| border-radius: 20px; |
| padding: 1.5rem; |
| box-shadow: 0 4px 20px rgba(0,0,0,0.08); |
| border: 1px solid #f1f5f9; |
| } |
| |
| .quiz-container { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| color: white; |
| padding: 2rem; |
| border-radius: 20px; |
| text-align: center; |
| box-shadow: 0 20px 60px rgba(102, 126, 234, 0.3); |
| } |
| |
| .quiz-timer { |
| font-size: 3rem; |
| font-weight: bold; |
| font-family: 'Courier New', monospace; |
| background: rgba(255,255,255,0.2); |
| padding: 1rem; |
| border-radius: 16px; |
| margin-bottom: 1.5rem; |
| } |
| |
| .urdu-text { |
| font-family: 'Noto Nastaliq Urdu', serif !important; |
| font-size: 1.5em !important; |
| line-height: 2 !important; |
| direction: rtl !important; |
| text-align: right !important; |
| background: #f8fafc; |
| padding: 1.5rem; |
| border-radius: 16px; |
| border: 2px solid #e2e8f0; |
| } |
| |
| .btn-primary { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; |
| color: white !important; |
| border: none !important; |
| border-radius: 12px !important; |
| padding: 1rem !important; |
| font-weight: 600 !important; |
| } |
| |
| .app-footer { |
| text-align: center; |
| padding: 2rem; |
| color: #64748b; |
| font-size: 0.875rem; |
| margin-top: 2rem; |
| border-top: 1px solid #e2e8f0; |
| } |
| """ |
|
|
| |
| def build_auth_screen(): |
| with gr.Column(visible=True, elem_classes="auth-container") as auth_screen: |
| gr.Markdown(""" |
| <div class="auth-logo">🎓</div> |
| <h1 class="auth-title">Student Facilitator</h1> |
| <p style="color: #64748b; margin-bottom: 2rem;">Your AI-powered academic companion</p> |
| """) |
| |
| with gr.Row(): |
| login_tab = gr.Button("Sign In", variant="primary") |
| signup_tab = gr.Button("Create Account") |
| |
| with gr.Column(visible=True) as login_form: |
| login_username = gr.Textbox(label="Username", placeholder="Enter username") |
| login_password = gr.Textbox(label="Password", type="password", placeholder="Enter password") |
| login_btn = gr.Button("Sign In", variant="primary") |
| login_message = gr.Markdown(visible=False) |
| |
| with gr.Column(visible=False) as signup_form: |
| signup_name = gr.Textbox(label="Full Name", placeholder="Your name") |
| signup_email = gr.Textbox(label="Email", placeholder="your@email.com") |
| signup_username = gr.Textbox(label="Username", placeholder="Choose username (min 3 chars)") |
| signup_password = gr.Textbox(label="Password", type="password", placeholder="Min 6 characters") |
| signup_btn = gr.Button("Create Account", variant="primary") |
| signup_message = gr.Markdown(visible=False) |
| |
| gr.Markdown(""" |
| <div style="margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid #f1f5f9; color: #94a3b8; font-size: 0.875rem;"> |
| 🔐 Secure • 📊 Analytics • 🎓 Free for Students |
| </div> |
| """) |
| |
| return (auth_screen, login_tab, signup_tab, login_form, signup_form, |
| login_username, login_password, login_btn, login_message, |
| signup_name, signup_email, signup_username, signup_password, signup_btn, signup_message) |
|
|
| def build_main_app(): |
| with gr.Column(visible=False) as main_app: |
| with gr.Row(elem_classes="app-header"): |
| gr.Markdown(""" |
| <h1 class="app-title">🎓 Student Facilitator</h1> |
| <div style="opacity: 0.9; font-size: 1rem; margin-top: 0.5rem;"> |
| Essay • PDF • Quiz • Translate • Dashboard |
| </div> |
| """) |
| |
| with gr.Row(elem_classes="user-widget"): |
| user_display = gr.Markdown() |
| logout_btn = gr.Button("Logout", size="sm") |
| |
| with gr.Row(): |
| gemini_status = "✅ Gemini" if gemini_client else "❌ Gemini" |
| groq_status = "✅ Groq" if groq_client else "❌ Groq" |
| gr.Markdown(f""" |
| <div style="padding: 1rem;"> |
| <span class="status-pill {'status-ok' if gemini_client else 'status-error'}">🤖 {gemini_status}</span> |
| <span class="status-pill {'status-ok' if groq_client else 'status-error'}">🌐 {groq_status}</span> |
| </div> |
| """) |
| |
| with gr.Tabs(): |
| |
| with gr.TabItem("📊 Dashboard"): |
| with gr.Row(): |
| with gr.Column(): |
| dashboard_stats = gr.Markdown() |
| dashboard_chart1 = gr.Markdown() |
| with gr.Column(): |
| dashboard_chart2 = gr.Markdown() |
| refresh_dashboard_btn = gr.Button("🔄 Refresh Dashboard", variant="primary") |
| |
| |
| with gr.TabItem("📝 Essay & PDF"): |
| with gr.Tabs(): |
| with gr.Tab("✍️ Essay Generator"): |
| with gr.Row(): |
| with gr.Column(): |
| essay_prompt = gr.Textbox(label="Essay Topic", placeholder="e.g., 'Impact of AI on Education'", lines=3) |
| essay_mic = gr.Audio(label="🎤 Voice Input", sources=["microphone"], type="filepath") |
| with gr.Row(): |
| essay_type = gr.Dropdown(["Argumentative", "Expository", "Descriptive", "Persuasive"], value="Argumentative", label="Type") |
| essay_tone = gr.Dropdown(["Academic", "Formal", "Neutral"], value="Academic", label="Tone") |
| essay_words = gr.Slider(200, 1000, 500, step=50, label="Word Count") |
| with gr.Row(): |
| essay_btn = gr.Button("✨ Generate Essay", variant="primary") |
| essay_speak_btn = gr.Button("🔊 Read Essay") |
| essay_output = gr.Markdown() |
| essay_audio = gr.Audio(label="Essay Audio") |
| |
| with gr.Tab("📄 PDF Summarizer"): |
| with gr.Row(): |
| with gr.Column(): |
| pdf_file = gr.File(label="Upload PDF", file_types=[".pdf"], type="binary") |
| with gr.Row(): |
| pdf_max = gr.Slider(50, 500, 200, step=10, label="Max Words") |
| pdf_min = gr.Slider(20, 200, 50, step=10, label="Min Words") |
| pdf_btn = gr.Button("📝 Summarize PDF", variant="primary") |
| pdf_text = gr.Textbox(label="Or paste text", lines=4) |
| pdf_mic = gr.Audio(label="🎤 Voice", sources=["microphone"], type="filepath") |
| text_btn = gr.Button("Summarize Text") |
| with gr.Column(): |
| pdf_output = gr.Textbox(label="Summary", lines=12) |
| pdf_speak_btn = gr.Button("🔊 Read Summary") |
| pdf_audio = gr.Audio(label="Summary Audio") |
| |
| |
| with gr.TabItem("🎯 Smart Quiz"): |
| with gr.Row(): |
| with gr.Column(scale=1): |
| gr.Markdown("### 📖 Study Material") |
| quiz_text = gr.Textbox(label="Paste your notes here", placeholder="Example: Photosynthesis is the process by which plants convert light energy...", lines=10) |
| gr.Markdown("### ⚙️ Settings") |
| quiz_num = gr.Slider(3, 15, 5, step=1, label="Questions") |
| quiz_time = gr.Slider(1, 10, 3, step=1, label="Minutes") |
| quiz_start = gr.Button("🚀 Start Quiz", variant="primary") |
| |
| with gr.Column(scale=2): |
| quiz_timer = gr.Markdown("⏳ 03:00", elem_classes="quiz-timer") |
| quiz_progress = gr.Markdown("Ready to start?") |
| quiz_question = gr.Markdown("### 🎯 Enter material and click Start!") |
| quiz_options = gr.Radio(choices=[], label="Select answer:", visible=False) |
| quiz_submit = gr.Button("✅ Submit Answer", visible=False) |
| |
| quiz_state = gr.State() |
| quiz_idx = gr.State(0) |
| quiz_scr = gr.State(0) |
| quiz_end = gr.State() |
| quiz_material = gr.State("") |
| |
| |
| with gr.TabItem("🌍 Urdu Translator"): |
| with gr.Row(): |
| with gr.Column(): |
| trans_input = gr.Textbox(label="English Text", placeholder="Enter text to translate...", lines=6) |
| trans_mic = gr.Audio(label="🎤 Voice Input", sources=["microphone"], type="filepath") |
| trans_btn = gr.Button("🔄 Translate", variant="primary") |
| trans_speak_btn = gr.Button("🔊 Read Urdu") |
| gr.Examples(examples=["Hello, how are you?", "Pakistan is beautiful", "I love learning"], inputs=trans_input) |
| with gr.Column(): |
| trans_output = gr.Textbox(label="اردو ترجمہ", lines=6, elem_classes="urdu-text", interactive=False) |
| trans_audio = gr.Audio(label="Urdu Audio") |
| |
| gr.Markdown(""" |
| <div class="app-footer"> |
| <p>🎓 Student Facilitator © 2024 | Made with ❤️ for students worldwide</p> |
| </div> |
| """) |
| |
| return (main_app, user_display, logout_btn, |
| dashboard_stats, dashboard_chart1, dashboard_chart2, refresh_dashboard_btn, |
| essay_prompt, essay_mic, essay_type, essay_tone, essay_words, essay_btn, essay_output, essay_speak_btn, essay_audio, |
| pdf_file, pdf_max, pdf_min, pdf_btn, pdf_text, pdf_mic, text_btn, pdf_output, pdf_speak_btn, pdf_audio, |
| quiz_text, quiz_num, quiz_time, quiz_start, quiz_timer, quiz_progress, quiz_question, quiz_options, quiz_submit, |
| quiz_state, quiz_idx, quiz_scr, quiz_end, quiz_material, |
| trans_input, trans_mic, trans_btn, trans_output, trans_speak_btn, trans_audio) |
|
|
| |
| with gr.Blocks(title="Student Facilitator", css=custom_css) as demo: |
| session_token_state = gr.State("") |
| |
| (auth_screen, login_tab, signup_tab, login_form, signup_form, |
| login_username, login_password, login_btn, login_message, |
| signup_name, signup_email, signup_username, signup_password, signup_btn, signup_message) = build_auth_screen() |
| |
| (main_app, user_display, logout_btn, |
| dashboard_stats, dashboard_chart1, dashboard_chart2, refresh_dashboard_btn, |
| essay_prompt, essay_mic, essay_type, essay_tone, essay_words, essay_btn, essay_output, essay_speak_btn, essay_audio, |
| pdf_file, pdf_max, pdf_min, pdf_btn, pdf_text, pdf_mic, text_btn, pdf_output, pdf_speak_btn, pdf_audio, |
| quiz_text, quiz_num, quiz_time, quiz_start, quiz_timer, quiz_progress, quiz_question, quiz_options, quiz_submit, |
| quiz_state, quiz_idx, quiz_scr, quiz_end, quiz_material, |
| trans_input, trans_mic, trans_btn, trans_output, trans_speak_btn, trans_audio) = build_main_app() |
| |
| |
| def toggle_auth_mode(mode): |
| if mode == "login": |
| return {login_form: gr.update(visible=True), signup_form: gr.update(visible=False)} |
| else: |
| return {login_form: gr.update(visible=False), signup_form: gr.update(visible=True)} |
| |
| login_tab.click(lambda: toggle_auth_mode("login"), outputs=[login_form, signup_form]) |
| signup_tab.click(lambda: toggle_auth_mode("signup"), outputs=[login_form, signup_form]) |
| |
| def handle_login(username, password): |
| session_token, user_data = auth_system.login(username, password) |
| if session_token: |
| return { |
| session_token_state: gr.update(value=session_token), |
| auth_screen: gr.update(visible=False), |
| main_app: gr.update(visible=True), |
| user_display: gr.update(value=f"👤 {user_data['name']}"), |
| login_message: gr.update(visible=False) |
| } |
| else: |
| return { |
| session_token_state: gr.update(value=""), |
| auth_screen: gr.update(visible=True), |
| main_app: gr.update(visible=False), |
| user_display: gr.update(value=""), |
| login_message: gr.update(value=f"❌ {user_data}", visible=True) |
| } |
| |
| def handle_signup(username, password, name, email): |
| success, message = auth_system.register(username, password, name, email) |
| if success: |
| session_token, user_data = auth_system.login(username, password) |
| if session_token: |
| return { |
| session_token_state: gr.update(value=session_token), |
| auth_screen: gr.update(visible=False), |
| main_app: gr.update(visible=True), |
| user_display: gr.update(value=f"👤 {user_data['name']}"), |
| signup_message: gr.update(value=f"✅ {message}", visible=True) |
| } |
| return { |
| session_token_state: gr.update(value=""), |
| auth_screen: gr.update(visible=True), |
| main_app: gr.update(visible=False), |
| user_display: gr.update(value=""), |
| signup_message: gr.update(value=f"❌ {message}", visible=True) |
| } |
| |
| def handle_logout(): |
| return { |
| session_token_state: gr.update(value=""), |
| auth_screen: gr.update(visible=True), |
| main_app: gr.update(visible=False), |
| user_display: gr.update(value=""), |
| login_username: gr.update(value=""), |
| login_password: gr.update(value="") |
| } |
| |
| login_btn.click(handle_login, inputs=[login_username, login_password], |
| outputs=[session_token_state, auth_screen, main_app, user_display, login_message]) |
| |
| signup_btn.click(handle_signup, inputs=[signup_username, signup_password, signup_name, signup_email], |
| outputs=[session_token_state, auth_screen, main_app, user_display, signup_message]) |
| |
| logout_btn.click(handle_logout, outputs=[session_token_state, auth_screen, main_app, user_display, login_username, login_password]) |
| |
| |
| def update_dashboard(token): |
| stats, chart1, chart2 = get_user_dashboard_data(token) |
| return stats, chart1, chart2 |
| |
| refresh_dashboard_btn.click(update_dashboard, inputs=[session_token_state], |
| outputs=[dashboard_stats, dashboard_chart1, dashboard_chart2]) |
| |
| |
| essay_mic.change(process_audio_input, inputs=[essay_mic, essay_prompt], outputs=essay_prompt) |
| essay_btn.click(generate_essay, inputs=[essay_prompt, essay_type, essay_words, essay_tone], outputs=essay_output) |
| essay_speak_btn.click(lambda x: speak_text(x, 'en'), inputs=essay_output, outputs=essay_audio) |
| |
| |
| pdf_mic.change(process_audio_input, inputs=[pdf_mic, pdf_text], outputs=pdf_text) |
| pdf_btn.click(summarize_pdf, inputs=[pdf_file, pdf_max, pdf_min], outputs=pdf_output) |
| text_btn.click(summarize_text, inputs=[pdf_text, pdf_max, pdf_min], outputs=pdf_output) |
| pdf_speak_btn.click(lambda x: speak_text(x, 'en'), inputs=pdf_output, outputs=pdf_audio) |
| |
| |
| quiz_start.click(start_quiz, |
| inputs=[quiz_text, quiz_num, quiz_time], |
| outputs=[quiz_question, quiz_options, quiz_progress, quiz_state, quiz_idx, quiz_scr, quiz_end, quiz_timer, quiz_submit, quiz_material]) |
| |
| quiz_submit.click(submit_answer, |
| inputs=[quiz_options, quiz_state, quiz_idx, quiz_scr, quiz_end, quiz_num, quiz_material, session_token_state], |
| outputs=[quiz_question, quiz_options, quiz_progress, quiz_state, quiz_idx, quiz_scr, quiz_end, quiz_timer, quiz_submit, quiz_material]) |
| |
| |
| trans_mic.change(process_audio_input, inputs=[trans_mic, trans_input], outputs=trans_input) |
| trans_btn.click(translate_to_urdu, inputs=trans_input, outputs=trans_output) |
| trans_speak_btn.click(lambda x: speak_text(x, 'ur'), inputs=trans_output, outputs=trans_audio) |
|
|
| if __name__ == "__main__": |
| demo.launch(server_name="0.0.0.0", server_port=7860) |