Spaces:
Runtime error
Runtime error
| # ============================================================================ | |
| # IMPORTS AND DEPENDENCIES | |
| # ============================================================================ | |
| import gradio as gr | |
| from PIL import Image, ImageEnhance | |
| import torch | |
| from transformers import AutoProcessor, PaliGemmaForConditionalGeneration | |
| import base64 | |
| import io | |
| from datetime import datetime | |
| import random | |
| import hashlib | |
| from database import create_database, get_connection, insert_demo_data, check_database_status | |
| import re | |
| # ============================================================================ | |
| # DATABASE INITIALIZATION | |
| # ============================================================================ | |
| print("ποΈ Setting up database...") | |
| create_database() | |
| insert_demo_data() | |
| check_database_status() | |
| # ============================================================================ | |
| # DATABASE INITIALIZATION | |
| # ============================================================================ | |
| print("ποΈ Setting up database...") | |
| create_database() | |
| insert_demo_data() | |
| check_database_status() | |
| # ============================================================================ | |
| # EXPORT DATABASE FOR HUGGING FACE FILES TAB | |
| # ============================================================================ | |
| print("\n" + "="*70) | |
| print("π EXPORTING DATABASE TO FILES TAB") | |
| print("="*70) | |
| import shutil | |
| import os | |
| def export_database_to_files(): | |
| """Copy database to visible location for download""" | |
| try: | |
| source_db = "vqa_stem_education.db" | |
| # Check if source exists | |
| if not os.path.exists(source_db): | |
| print(f"β Source database not found: {source_db}") | |
| return False | |
| # Get file info | |
| file_size = os.path.getsize(source_db) | |
| file_path = os.path.abspath(source_db) | |
| print(f"π Source: {file_path}") | |
| print(f"π Size: {file_size:,} bytes ({file_size/1024:.2f} KB)") | |
| # Copy to root directory with visible name | |
| export_name = "DATABASE_EXPORT.db" | |
| shutil.copy(source_db, export_name) | |
| print(f"β Copied to: {export_name}") | |
| # Also create timestamped backup | |
| from datetime import datetime | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| backup_name = f"db_backup_{timestamp}.db" | |
| shutil.copy(source_db, backup_name) | |
| print(f"β Backup created: {backup_name}") | |
| # Verify copy | |
| if os.path.exists(export_name): | |
| print(f"β Export successful!") | |
| print(f"π File '{export_name}' should appear in HF Files tab") | |
| return True | |
| else: | |
| print(f"β Export failed - file not created") | |
| return False | |
| except Exception as e: | |
| print(f"β Export error: {e}") | |
| return False | |
| # Run export | |
| export_database_to_files() | |
| print("="*70 + "\n") | |
| # ============================================================================ | |
| # MODEL LOADING - VISION QUESTION ANSWERING (VQA) | |
| # ============================================================================ | |
| print("π€ Loading VQA model...") | |
| try: | |
| model_id = "google/paligemma-3b-pt-224" | |
| processor = AutoProcessor.from_pretrained(model_id) | |
| model = PaliGemmaForConditionalGeneration.from_pretrained(model_id) | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| model.to(device) | |
| model_name = "PaliGemma" | |
| print(f"β PaliGemma loaded on {device}") | |
| except Exception as e: | |
| print(f"β οΈ PaliGemma not available, loading BLIP...") | |
| from transformers import BlipProcessor, BlipForQuestionAnswering | |
| processor = BlipProcessor.from_pretrained("Salesforce/blip-vqa-base") | |
| model = BlipForQuestionAnswering.from_pretrained("Salesforce/blip-vqa-base") | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| model.to(device) | |
| model_name = "BLIP" | |
| print(f"β BLIP loaded on {device}") | |
| # ============================================================================ | |
| # GLOBAL STATE MANAGEMENT | |
| # ============================================================================ | |
| current_user = { | |
| "id": None, | |
| "name": None, | |
| "is_logged_in": False, | |
| "role": None | |
| } | |
| # ============================================================================ | |
| # AUTHENTICATION FUNCTIONS | |
| # ============================================================================ | |
| def hash_password(password): | |
| return hashlib.sha256(password.encode()).hexdigest() | |
| def register_student(name, dob, gender, class_name, guardian_phone, guardian_email, username, password): | |
| """Register new student with validation - UPDATED for new database schema""" | |
| # Validate all required fields | |
| if not all([name, dob, gender, class_name, guardian_phone, guardian_email, username, password]): | |
| return "β οΈ Sila isi SEMUA medan yang bertanda *" | |
| # Validate date format (DD/MM/YY) | |
| if not re.match(r'^\d{2}/\d{2}/\d{2}$', dob): | |
| return "β οΈ Format tarikh lahir salah! Sila gunakan DD/MM/YY (Contoh: 15/03/10)" | |
| # Validate phone number (must be digits only and 10-11 digits) | |
| if not guardian_phone.isdigit() or len(guardian_phone) < 10: | |
| return "β οΈ No. HP tidak sah! Sila masukkan nombor yang betul (10-11 digit)" | |
| # Validate email format | |
| if not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', guardian_email): | |
| return "β οΈ Format emel tidak sah! Sila masukkan emel yang betul" | |
| try: | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| # Check if username already exists | |
| cursor.execute('SELECT studentID FROM Student WHERE username = ?', (username,)) | |
| if cursor.fetchone(): | |
| conn.close() | |
| return "β Username sudah wujud! Sila guna username lain." | |
| # Hash the password | |
| hashed_pw = hash_password(password) | |
| # β CORRECTED: Use fullName, class, guardianCtcNo, guardianEmail | |
| cursor.execute(''' | |
| INSERT INTO Student (fullName, username, password, class, guardianCtcNo, guardianEmail, dateOfBirth, gender) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?) | |
| ''', (name, username, hashed_pw, class_name, guardian_phone, guardian_email, dob, gender)) | |
| conn.commit() | |
| conn.close() | |
| return f"β Pendaftaran Berjaya!\n\n**Selamat datang, {name}!**\n\nπ§ Emel pengesahan telah dihantar ke {guardian_email}\n\nSila log masuk untuk mula menggunakan sistem." | |
| except Exception as e: | |
| return f"β Error: {e}" | |
| def login_student(username, password): | |
| if not username or not password: | |
| return "β οΈ Sila masukkan username dan password", gr.update(), gr.update(), gr.update(), gr.update(), gr.update() | |
| try: | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| hashed_pw = hash_password(password) | |
| cursor.execute(''' | |
| SELECT studentID, fullName FROM Student | |
| WHERE username = ? AND password = ? | |
| ''', (username, hashed_pw)) | |
| result = cursor.fetchone() | |
| conn.close() | |
| if result: | |
| student_id, student_name = result | |
| current_user["id"] = student_id | |
| current_user["name"] = student_name | |
| current_user["is_logged_in"] = True | |
| current_user["role"] = "student" | |
| return ( | |
| f"β Log Masuk Berjaya!\n\n**Selamat kembali, {student_name}!** π", | |
| gr.update(visible=False), | |
| gr.update(visible=True), | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| gr.update(value=f"π Pelajar: {student_name}") | |
| ) | |
| else: | |
| return "β Username atau password salah!", gr.update(), gr.update(), gr.update(), gr.update(), gr.update() | |
| except Exception as e: | |
| return f"β Error: {e}", gr.update(), gr.update(), gr.update(), gr.update(), gr.update() | |
| def register_teacher(name, gender, phone, email, subject, username, password): | |
| """Register new teacher with validation - UPDATED for new database schema""" | |
| # Validate all required fields | |
| if not all([name, gender, phone, email, subject, username, password]): | |
| return "β οΈ Sila isi SEMUA medan yang bertanda *" | |
| # Validate phone number (must be digits only and 10-11 digits) | |
| if not phone.isdigit() or len(phone) < 10: | |
| return "β οΈ No. Telefon tidak sah! Sila masukkan nombor yang betul (10-11 digit)" | |
| # Validate email format | |
| if not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', email): | |
| return "β οΈ Format emel tidak sah! Sila masukkan emel yang betul" | |
| try: | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| # Check if username already exists | |
| cursor.execute('SELECT teacherID FROM Teacher WHERE username = ?', (username,)) | |
| if cursor.fetchone(): | |
| conn.close() | |
| return "β Username sudah wujud! Sila guna username lain." | |
| # Hash the password | |
| hashed_pw = hash_password(password) | |
| # β CORRECTED: Use fullName, gender, CtcNumber, email, AssignedSubject | |
| cursor.execute(''' | |
| INSERT INTO Teacher (fullName, gender, CtcNumber, email, AssignedSubject, username, password) | |
| VALUES (?, ?, ?, ?, ?, ?, ?) | |
| ''', (name, gender, phone, email, subject, username, hashed_pw)) | |
| conn.commit() | |
| conn.close() | |
| return f"β Pendaftaran Guru Berjaya!\n\n**Selamat datang, Cikgu {name}!**\n\nπ§ Emel pengesahan telah dihantar ke {email}\n\nSila log masuk untuk mula menggunakan sistem." | |
| except Exception as e: | |
| return f"β Error: {e}" | |
| def login_teacher(username, password): | |
| if not username or not password: | |
| return "β οΈ Sila masukkan username dan password", gr.update(), gr.update(), gr.update(), gr.update(), gr.update() | |
| try: | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| hashed_pw = hash_password(password) | |
| cursor.execute(''' | |
| SELECT teacherID, fullName FROM Teacher | |
| WHERE username = ? AND password = ? | |
| ''', (username, hashed_pw)) | |
| result = cursor.fetchone() | |
| conn.close() | |
| if result: | |
| teacher_id, teacher_name = result | |
| current_user["id"] = teacher_id | |
| current_user["name"] = teacher_name | |
| current_user["is_logged_in"] = True | |
| current_user["role"] = "teacher" | |
| return ( | |
| f"β Log Masuk Berjaya!\n\n**Selamat kembali, Cikgu {teacher_name}!** π¨βπ«", | |
| gr.update(visible=False), | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| gr.update(visible=True), | |
| gr.update(value=f"π¨βπ« Guru: {teacher_name}") | |
| ) | |
| else: | |
| return "β Username atau password salah!", gr.update(), gr.update(), gr.update(), gr.update(), gr.update() | |
| except Exception as e: | |
| return f"β Error: {e}", gr.update(), gr.update(), gr.update(), gr.update(), gr.update() | |
| def logout_user(): | |
| name = current_user["name"] | |
| role = current_user["role"] | |
| current_user["id"] = None | |
| current_user["name"] = None | |
| current_user["is_logged_in"] = False | |
| current_user["role"] = None | |
| role_emoji = "π" if role == "student" else "π¨βπ«" | |
| return ( | |
| f"π Anda telah log keluar. Terima kasih, {role_emoji} {name}!", | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| gr.update(visible=False), | |
| gr.update(visible=False), | |
| gr.update(value="") | |
| ) | |
| def check_auth(required_role=None): | |
| if not current_user["is_logged_in"]: | |
| return False | |
| if required_role and current_user["role"] != required_role: | |
| return False | |
| return True | |
| # ============================================================================ | |
| # IMAGE PROCESSING FUNCTIONS | |
| # ============================================================================ | |
| def preprocess_image(image): | |
| try: | |
| if image.mode != "RGB": | |
| image = image.convert("RGB") | |
| enhancer = ImageEnhance.Contrast(image) | |
| image = enhancer.enhance(1.3) | |
| enhancer = ImageEnhance.Sharpness(image) | |
| image = enhancer.enhance(1.2) | |
| max_size = 800 | |
| if max(image.size) > max_size: | |
| image.thumbnail((max_size, max_size), Image.Resampling.LANCZOS) | |
| return image | |
| except Exception as e: | |
| print(f"β οΈ Image preprocessing error: {e}") | |
| return image | |
| # ============================================================================ | |
| # VQA PROMPT ENGINEERING FUNCTIONS | |
| # ============================================================================ | |
| def improve_prompt(question): | |
| question_lower = question.lower() | |
| if any(word in question_lower for word in ["warna", "color", "colour"]): | |
| return f"Identify the main color in this image. Question: {question}. Answer:" | |
| elif any(word in question_lower for word in ["berapa", "how many", "count", "bilangan"]): | |
| return f"Count the objects carefully in this image. Question: {question}. Answer:" | |
| elif any(word in question_lower for word in ["apa", "what", "apakah"]): | |
| return f"Describe what you see in this image. Question: {question}. Answer:" | |
| elif any(word in question_lower for word in ["mana", "where", "di mana"]): | |
| return f"Locate the position in this image. Question: {question}. Answer:" | |
| elif any(word in question_lower for word in ["bentuk", "shape"]): | |
| return f"Identify the shape in this image. Question: {question}. Answer:" | |
| else: | |
| return f"answer: {question}" | |
| def postprocess_answer(answer, question): | |
| answer = answer.strip() | |
| answer = answer.replace("answer:", "").strip() | |
| answer = answer.replace("Answer:", "").strip() | |
| words = answer.split() | |
| if len(words) > 1: | |
| cleaned = [words[0]] | |
| for i in range(1, len(words)): | |
| if words[i] != words[i-1]: | |
| cleaned.append(words[i]) | |
| answer = " ".join(cleaned) | |
| if answer and len(answer) > 0 and not answer[0].isupper(): | |
| answer = answer.capitalize() | |
| if answer and answer[-1] not in '.!?': | |
| if any(word in question.lower() for word in ["how many", "berapa"]): | |
| answer = answer + "." | |
| else: | |
| answer = answer + "!" | |
| answer = answer.replace(" .", ".") | |
| answer = answer.replace(" ,", ",") | |
| answer = answer.replace(" ", " ") | |
| return answer | |
| # ============================================================================ | |
| # DATABASE UTILITY FUNCTIONS | |
| # ============================================================================ | |
| def image_to_base64(image): | |
| buffered = io.BytesIO() | |
| image.save(buffered, format="PNG") | |
| return base64.b64encode(buffered.getvalue()).decode() | |
| def save_to_database(user_id, image, question, answer, processing_time): | |
| try: | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| image_data = image_to_base64(image) | |
| timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| cursor.execute(''' | |
| INSERT INTO ImageQuestion (studentID, imagePath, questionText, submissionDate) | |
| VALUES (?, ?, ?, ?) | |
| ''', (user_id, image_data, question, timestamp)) | |
| question_id = cursor.lastrowid | |
| cursor.execute(''' | |
| INSERT INTO AIAnswer (questionID, answerText, generatedDate, processingTime) | |
| VALUES (?, ?, ?, ?) | |
| ''', (question_id, answer, timestamp, processing_time)) | |
| conn.commit() | |
| conn.close() | |
| return True | |
| except Exception as e: | |
| print(f"β Save error: {e}") | |
| return False | |
| # ============================================================================ | |
| # MAIN VQA FUNCTION | |
| # ============================================================================ | |
| def answer_question(image, question): | |
| if image is None: | |
| return "πΌοΈ Sila muat naik gambar / Please upload image" | |
| if not question or question.strip() == "": | |
| return "β Tanya soalan / Ask a question" | |
| try: | |
| start_time = datetime.now() | |
| image = preprocess_image(image) | |
| prompt = improve_prompt(question) | |
| if model_name == "PaliGemma": | |
| inputs = processor(text=prompt, images=image, return_tensors="pt").to(device) | |
| with torch.no_grad(): | |
| outputs = model.generate( | |
| **inputs, | |
| max_length=100, | |
| num_beams=5, | |
| temperature=0.7, | |
| top_p=0.9, | |
| repetition_penalty=1.2, | |
| do_sample=True, | |
| early_stopping=True | |
| ) | |
| answer = processor.decode(outputs[0], skip_special_tokens=True) | |
| answer = answer.replace(prompt, "").strip() | |
| else: | |
| inputs = processor(image, question, return_tensors="pt").to(device) | |
| with torch.no_grad(): | |
| outputs = model.generate( | |
| **inputs, | |
| max_length=50, | |
| num_beams=5, | |
| temperature=0.7, | |
| repetition_penalty=1.2 | |
| ) | |
| answer = processor.decode(outputs[0], skip_special_tokens=True) | |
| processing_time = (datetime.now() - start_time).total_seconds() | |
| answer = postprocess_answer(answer, question) | |
| user_id = current_user["id"] if current_user["is_logged_in"] else 0 | |
| saved = save_to_database(user_id, image, question, answer, processing_time) | |
| response = f"π {answer}" | |
| if saved: | |
| response += f"\n\nπΎ Disimpan! / Saved! | β±οΈ {processing_time:.2f}s | π€ {model_name}" | |
| return response | |
| except Exception as e: | |
| return f"π **Error:** {str(e)}" | |
| # ============================================================================ | |
| # CONTENT LIBRARY FUNCTIONS | |
| # ============================================================================ | |
| def get_all_content(): | |
| if not check_auth(required_role="student"): | |
| return "π Sila log masuk sebagai PELAJAR untuk akses perpustakaan" | |
| try: | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| cursor.execute(''' | |
| SELECT c.contentID, c.title, c.description, c.uploadDate, t.fullName | |
| FROM ContentLibrary c | |
| JOIN Teacher t ON c.teacherID = t.teacherID | |
| ORDER BY c.uploadDate DESC | |
| ''') | |
| records = cursor.fetchall() | |
| conn.close() | |
| if not records: | |
| return "π **Tiada kandungan lagi / No content yet**" | |
| output = f"## π Perpustakaan Kandungan\n\n**Jumlah: {len(records)}**\n\n" | |
| for rec in records: | |
| content_id, title, desc, date, teacher = rec | |
| output += f"### π {title}\n" | |
| output += f"- **ID:** {content_id}\n" | |
| output += f"- **Penerangan:** {desc}\n" | |
| output += f"- **Dimuat naik oleh:** {teacher}\n" | |
| output += f"- **Tarikh:** {date}\n\n---\n\n" | |
| return output | |
| except Exception as e: | |
| return f"β Error: {e}" | |
| # ============================================================================ | |
| # BOOKMARK FUNCTIONS | |
| # ============================================================================ | |
| def get_bookmarks(): | |
| if not check_auth(required_role="student"): | |
| return "π Sila log masuk sebagai PELAJAR untuk akses tanda buku" | |
| try: | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| cursor.execute(''' | |
| SELECT b.bookmarkID, b.contentType, b.contentTypeID, b.dateSaved | |
| FROM Bookmark b | |
| WHERE b.studentID = ? | |
| ORDER BY b.dateSaved DESC | |
| ''', (current_user["id"],)) | |
| records = cursor.fetchall() | |
| conn.close() | |
| if not records: | |
| return "π **Tiada tanda buku lagi**" | |
| output = f"## π Kandungan di Tanda\n\n**Jumlah: {len(records)}**\n\n" | |
| for rec in records: | |
| bookmark_id, content_type, content_id, date = rec | |
| output += f"### π Bookmark #{bookmark_id}\n" | |
| output += f"- **Jenis:** {content_type}\n" | |
| output += f"- **ID Kandungan:** {content_id}\n" | |
| output += f"- **Tarikh:** {date}\n\n---\n\n" | |
| return output | |
| except Exception as e: | |
| return f"β Error: {e}" | |
| def add_bookmark(content_type, content_id): | |
| if not check_auth(required_role="student"): | |
| return "π Sila log masuk sebagai PELAJAR terlebih dahulu" | |
| try: | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| cursor.execute(''' | |
| INSERT INTO Bookmark (studentID, contentType, contentTypeID, dateSaved) | |
| VALUES (?, ?, ?, ?) | |
| ''', (current_user["id"], content_type, content_id, timestamp)) | |
| conn.commit() | |
| conn.close() | |
| return f"β **Ditanda!**" | |
| except Exception as e: | |
| return f"β Error: {e}" | |
| # ============================================================================ | |
| # PROGRESS TRACKING FUNCTIONS | |
| # ============================================================================ | |
| def get_all_students_progress(): | |
| if not check_auth(required_role="teacher"): | |
| return "π Sila log masuk sebagai GURU untuk lihat kemajuan pelajar" | |
| try: | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| cursor.execute(''' | |
| SELECT s.studentID, s.fullName, s.class, | |
| COUNT(iq.questionID) as total_questions, | |
| AVG(aa.processingTime) as avg_time | |
| FROM Student s | |
| LEFT JOIN ImageQuestion iq ON s.studentID = iq.studentID | |
| LEFT JOIN AIAnswer aa ON iq.questionID = aa.questionID | |
| WHERE s.studentID > 0 | |
| GROUP BY s.studentID | |
| ORDER BY s.class, s.fullName | |
| ''') | |
| records = cursor.fetchall() | |
| conn.close() | |
| if not records: | |
| return "π **Tiada data pelajar lagi**" | |
| output = "## π Penjejak Kemajuan Semua Pelajar\n\n" | |
| output += f"**Jumlah Pelajar: {len(records)}**\n\n" | |
| current_class = None | |
| for rec in records: | |
| student_id, name, class_name, total_q, avg_time = rec | |
| if class_name != current_class: | |
| output += f"### π Kelas {class_name}\n\n" | |
| current_class = class_name | |
| output += f"**{name}** (ID: {student_id})\n" | |
| output += f"- Soalan dijawab: {total_q}\n" | |
| output += f"- Purata masa: {avg_time:.2f}s\n\n" | |
| return output | |
| except Exception as e: | |
| return f"β Error: {e}" | |
| # ============================================================================ | |
| # STUDENT PROGRESS REPORT WITH GRAPH AND PDF EXPORT | |
| # ============================================================================ | |
| import matplotlib | |
| matplotlib.use('Agg') # Use non-GUI backend | |
| import matplotlib.pyplot as plt | |
| import pandas as pd | |
| from io import BytesIO | |
| import base64 | |
| def generate_progress_graph(student_data): | |
| """Generate progress graph for student""" | |
| try: | |
| # Create figure with subplots | |
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) | |
| fig.suptitle('Graf Kemajuan Pelajar', fontsize=16, fontweight='bold') | |
| # Graph 1: Questions vs Challenges | |
| categories = ['Soalan\nDitanya', 'Cabaran\nDisertai'] | |
| values = [student_data['total_questions'], student_data['total_challenges']] | |
| colors = ['#FF6B6B', '#4ECDC4'] | |
| ax1.bar(categories, values, color=colors, alpha=0.7, edgecolor='black', linewidth=2) | |
| ax1.set_ylabel('Bilangan', fontsize=12, fontweight='bold') | |
| ax1.set_title('Aktiviti Pembelajaran', fontsize=14, fontweight='bold') | |
| ax1.grid(axis='y', alpha=0.3) | |
| # Add value labels on bars | |
| for i, v in enumerate(values): | |
| ax1.text(i, v + 0.5, str(v), ha='center', va='bottom', fontweight='bold') | |
| # Graph 2: Average Score (if available) | |
| if student_data['avg_score'] > 0: | |
| labels = ['Skor\nPurata', 'Target\n(100)'] | |
| sizes = [student_data['avg_score'], 100 - student_data['avg_score']] | |
| colors_pie = ['#95E1D3', '#F38181'] | |
| explode = (0.1, 0) | |
| ax2.pie(sizes, explode=explode, labels=labels, colors=colors_pie, | |
| autopct='%1.1f%%', shadow=True, startangle=90) | |
| ax2.set_title('Pencapaian Skor', fontsize=14, fontweight='bold') | |
| else: | |
| ax2.text(0.5, 0.5, 'Tiada skor lagi', ha='center', va='center', | |
| fontsize=14, transform=ax2.transAxes) | |
| ax2.set_title('Pencapaian Skor', fontsize=14, fontweight='bold') | |
| ax2.axis('off') | |
| plt.tight_layout() | |
| # Convert to base64 image | |
| buffer = BytesIO() | |
| plt.savefig(buffer, format='png', dpi=150, bbox_inches='tight') | |
| buffer.seek(0) | |
| image_base64 = base64.b64encode(buffer.read()).decode() | |
| plt.close() | |
| return f"data:image/png;base64,{image_base64}" | |
| except Exception as e: | |
| print(f"Error generating graph: {e}") | |
| return None | |
| def generate_pdf_report(student_data): | |
| """Generate PDF report for download""" | |
| try: | |
| from reportlab.lib.pagesizes import A4 | |
| from reportlab.lib import colors | |
| from reportlab.lib.units import cm | |
| from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image | |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |
| from reportlab.lib.enums import TA_CENTER, TA_LEFT | |
| from datetime import datetime | |
| # Create PDF file | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| filename = f"outputs/laporan_prestasi_{student_data['name'].replace(' ', '_')}_{timestamp}.pdf" | |
| import os | |
| os.makedirs("outputs", exist_ok=True) | |
| doc = SimpleDocTemplate(filename, pagesize=A4) | |
| elements = [] | |
| styles = getSampleStyleSheet() | |
| # Custom styles | |
| title_style = ParagraphStyle( | |
| 'CustomTitle', | |
| parent=styles['Heading1'], | |
| fontSize=24, | |
| textColor=colors.HexColor('#2C3E50'), | |
| spaceAfter=30, | |
| alignment=TA_CENTER | |
| ) | |
| heading_style = ParagraphStyle( | |
| 'CustomHeading', | |
| parent=styles['Heading2'], | |
| fontSize=16, | |
| textColor=colors.HexColor('#E74C3C'), | |
| spaceAfter=12, | |
| spaceBefore=12 | |
| ) | |
| # Title | |
| title = Paragraph("π LAPORAN PRESTASI PELAJAR", title_style) | |
| elements.append(title) | |
| elements.append(Spacer(1, 0.5*cm)) | |
| # Student Info Table | |
| elements.append(Paragraph("π Maklumat Pelajar", heading_style)) | |
| info_data = [ | |
| ['Nama Murid:', student_data['name']], | |
| ['Kelas:', student_data['class']], | |
| ['Tarikh Laporan:', student_data['date']], | |
| ] | |
| info_table = Table(info_data, colWidths=[5*cm, 10*cm]) | |
| info_table.setStyle(TableStyle([ | |
| ('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#ECF0F1')), | |
| ('TEXTCOLOR', (0, 0), (-1, -1), colors.black), | |
| ('ALIGN', (0, 0), (0, -1), 'LEFT'), | |
| ('ALIGN', (1, 0), (1, -1), 'LEFT'), | |
| ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'), | |
| ('FONTNAME', (1, 0), (1, -1), 'Helvetica'), | |
| ('FONTSIZE', (0, 0), (-1, -1), 12), | |
| ('BOTTOMPADDING', (0, 0), (-1, -1), 12), | |
| ('GRID', (0, 0), (-1, -1), 1, colors.grey) | |
| ])) | |
| elements.append(info_table) | |
| elements.append(Spacer(1, 1*cm)) | |
| # Performance Summary | |
| elements.append(Paragraph("π Ringkasan Prestasi", heading_style)) | |
| summary_data = [ | |
| ['Metrik', 'Nilai'], | |
| ['Jumlah Soalan Ditanya', str(student_data['total_questions'])], | |
| ['Bilangan Cabaran Disertai', str(student_data['total_challenges'])], | |
| ['Purata Skor Cabaran', f"{student_data['avg_score']:.1f}%"], | |
| ['Purata Masa Jawapan', f"{student_data['avg_time']:.2f}s"], | |
| ] | |
| summary_table = Table(summary_data, colWidths=[10*cm, 5*cm]) | |
| summary_table.setStyle(TableStyle([ | |
| ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#3498DB')), | |
| ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), | |
| ('ALIGN', (0, 0), (-1, -1), 'LEFT'), | |
| ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), | |
| ('FONTSIZE', (0, 0), (-1, 0), 14), | |
| ('BOTTOMPADDING', (0, 0), (-1, 0), 12), | |
| ('BACKGROUND', (0, 1), (-1, -1), colors.beige), | |
| ('GRID', (0, 0), (-1, -1), 1, colors.black) | |
| ])) | |
| elements.append(summary_table) | |
| elements.append(Spacer(1, 1*cm)) | |
| # Comments | |
| elements.append(Paragraph("π¬ Ulasan", heading_style)) | |
| comment_text = student_data.get('comment', 'Teruskan usaha yang baik! Kekalkan semangat belajar.') | |
| comment_para = Paragraph(comment_text, styles['Normal']) | |
| elements.append(comment_para) | |
| elements.append(Spacer(1, 1*cm)) | |
| # Recent Activities | |
| if student_data.get('recent_activities'): | |
| elements.append(Paragraph("π Aktiviti Terkini", heading_style)) | |
| activity_data = [['Tarikh', 'Soalan', 'Jawapan']] | |
| for activity in student_data['recent_activities'][:5]: | |
| activity_data.append([ | |
| activity[2][:10], # Date (first 10 chars) | |
| activity[0][:40] + '...' if len(activity[0]) > 40 else activity[0], # Question | |
| activity[1][:40] + '...' if len(activity[1]) > 40 else activity[1] # Answer | |
| ]) | |
| activity_table = Table(activity_data, colWidths=[3*cm, 6*cm, 6*cm]) | |
| activity_table.setStyle(TableStyle([ | |
| ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2ECC71')), | |
| ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), | |
| ('ALIGN', (0, 0), (-1, -1), 'LEFT'), | |
| ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), | |
| ('FONTSIZE', (0, 0), (-1, -1), 9), | |
| ('BOTTOMPADDING', (0, 0), (-1, -1), 8), | |
| ('GRID', (0, 0), (-1, -1), 1, colors.black) | |
| ])) | |
| elements.append(activity_table) | |
| # Build PDF | |
| doc.build(elements) | |
| return filename | |
| except Exception as e: | |
| print(f"Error generating PDF: {e}") | |
| return None | |
| def get_student_progress(): | |
| """Get comprehensive student progress report with graph and PDF""" | |
| if not check_auth(required_role="student"): | |
| return "π Sila log masuk sebagai PELAJAR untuk lihat kemajuan", None, None | |
| try: | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| # Get student info | |
| cursor.execute('SELECT fullName, class FROM Student WHERE studentID = ?', (current_user["id"],)) | |
| student_info = cursor.fetchone() | |
| student_name = student_info[0] if student_info else "Unknown" | |
| student_class = student_info[1] if student_info else "N/A" | |
| # Get total questions | |
| cursor.execute('SELECT COUNT(*) FROM ImageQuestion WHERE studentID = ?', (current_user["id"],)) | |
| total_questions = cursor.fetchone()[0] | |
| # Get total challenges participated | |
| cursor.execute('SELECT COUNT(*) FROM ChallengeSubmission WHERE studentID = ?', (current_user["id"],)) | |
| total_challenges = cursor.fetchone()[0] | |
| # Get average score from challenges | |
| cursor.execute(''' | |
| SELECT AVG(score) FROM ChallengeSubmission | |
| WHERE studentID = ? AND score IS NOT NULL AND score > 0 | |
| ''', (current_user["id"],)) | |
| avg_score_result = cursor.fetchone()[0] | |
| avg_score = avg_score_result if avg_score_result else 0 | |
| # Get average processing time | |
| cursor.execute(''' | |
| SELECT AVG(aa.processingTime) | |
| FROM AIAnswer aa | |
| JOIN ImageQuestion iq ON aa.questionID = iq.questionID | |
| WHERE iq.studentID = ? | |
| ''', (current_user["id"],)) | |
| avg_time = cursor.fetchone()[0] or 0 | |
| # Get recent activities | |
| cursor.execute(''' | |
| SELECT iq.questionText, aa.answerText, iq.submissionDate | |
| FROM ImageQuestion iq | |
| JOIN AIAnswer aa ON iq.questionID = aa.questionID | |
| WHERE iq.studentID = ? | |
| ORDER BY iq.submissionDate DESC | |
| LIMIT 5 | |
| ''', (current_user["id"],)) | |
| recent_activities = cursor.fetchall() | |
| # Get AI/Teacher comment (from latest progress report if exists) | |
| cursor.execute(''' | |
| SELECT comment FROM ProgressReport | |
| WHERE studentID = ? | |
| ORDER BY dateSubmitted DESC LIMIT 1 | |
| ''', (current_user["id"],)) | |
| comment_result = cursor.fetchone() | |
| comment = comment_result[0] if comment_result else "Teruskan usaha yang baik! πͺ" | |
| conn.close() | |
| # Prepare student data | |
| from datetime import datetime | |
| student_data = { | |
| 'name': student_name, | |
| 'class': student_class, | |
| 'total_questions': total_questions, | |
| 'total_challenges': total_challenges, | |
| 'avg_score': avg_score, | |
| 'avg_time': avg_time, | |
| 'comment': comment, | |
| 'date': datetime.now().strftime("%d/%m/%Y"), | |
| 'recent_activities': recent_activities | |
| } | |
| # Generate graph | |
| graph_image = generate_progress_graph(student_data) | |
| # Generate markdown report | |
| output = "## π Laporan Prestasi Pelajar\n\n" | |
| output += "### π Ringkasan\n\n" | |
| output += f"| Maklumat | Butiran |\n" | |
| output += f"|----------|----------|\n" | |
| output += f"| **π€ Nama Murid** | {student_name} |\n" | |
| output += f"| **π Kelas** | {student_class} |\n" | |
| output += f"| **β Jumlah Soalan Ditanya** | {total_questions} |\n" | |
| output += f"| **π― Bilangan Cabaran Disertai** | {total_challenges} |\n" | |
| output += f"| **β Purata Skor Cabaran** | {avg_score:.1f}% |\n" | |
| output += f"| **β±οΈ Purata Masa Jawapan** | {avg_time:.2f}s |\n" | |
| output += f"| **π Tarikh Laporan** | {student_data['date']} |\n\n" | |
| output += "### π¬ Ulasan AI/Guru\n\n" | |
| output += f"> {comment}\n\n" | |
| if recent_activities: | |
| output += "### π Aktiviti Terkini (5 Terbaru)\n\n" | |
| for i, (q, a, date) in enumerate(recent_activities, 1): | |
| output += f"**{i}. {date}**\n" | |
| output += f"- **Q:** {q}\n" | |
| output += f"- **A:** {a}\n\n" | |
| # Generate PDF | |
| pdf_file = generate_pdf_report(student_data) | |
| return output, graph_image, pdf_file | |
| except Exception as e: | |
| return f"β Error: {e}", None, None | |
| # ============================================================================ | |
| # DAILY CHALLENGE FUNCTIONS | |
| # ============================================================================ | |
| def get_daily_challenge(): | |
| if not check_auth(required_role="student"): | |
| return "π Sila log masuk sebagai PELAJAR untuk cabaran harian" | |
| try: | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| cursor.execute(''' | |
| SELECT challengeID, title, description, challengeQuestion, datePosted | |
| FROM Challenge | |
| ORDER BY datePosted DESC | |
| LIMIT 1 | |
| ''') | |
| challenge = cursor.fetchone() | |
| conn.close() | |
| if not challenge: | |
| return "π Tiada cabaran hari ini" | |
| ch_id, title, desc, question, date = challenge | |
| output = f"## π― Cabaran Harian STEM\n\n### {title}\n" | |
| output += f"Tarikh: {date}\n\n**Penerangan:**\n{desc}\n\n" | |
| output += f"Soalan:\n{question}\n\n*Hantar jawapan anda di bawah!*" | |
| return output | |
| except Exception as e: | |
| return f"β Error: {e}" | |
| def add_challenge(title, description, question): | |
| if not check_auth(required_role="teacher"): | |
| return "π Hanya GURU boleh menambah cabaran" | |
| if not title or not description or not question: | |
| return "β οΈ Sila isi semua medan" | |
| try: | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| cursor.execute(''' | |
| INSERT INTO Challenge (teacherID, title, challengePath, description, challengeQuestion, datePosted) | |
| VALUES (?, ?, ?, ?, ?, ?) | |
| ''', (current_user["id"], title, "challenge_image.png", description, question, timestamp)) | |
| conn.commit() | |
| conn.close() | |
| return f"β Cabaran berjaya ditambah!\n\nπ― **{title}**" | |
| except Exception as e: | |
| return f"β Error: {e}" | |
| def submit_challenge_answer(answer_text): | |
| if not check_auth(required_role="student"): | |
| return "π Sila log masuk sebagai PELAJAR terlebih dahulu" | |
| if not answer_text: | |
| return "β οΈ Sila masukkan jawapan" | |
| try: | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| cursor.execute('SELECT challengeID FROM Challenge ORDER BY datePosted DESC LIMIT 1') | |
| challenge = cursor.fetchone() | |
| if not challenge: | |
| return "β Tiada cabaran aktif" | |
| challenge_id = challenge[0] | |
| timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| cursor.execute(''' | |
| INSERT INTO ChallengeSubmission (challengeID, studentID, answerText, submissionDate, score) | |
| VALUES (?, ?, ?, ?, ?) | |
| ''', (challenge_id, current_user["id"], answer_text, timestamp, 0)) | |
| conn.commit() | |
| conn.close() | |
| return f"β Jawapan dihantar!\n\nGuru akan menilai jawapan anda." | |
| except Exception as e: | |
| return f"β Error: {e}" | |
| # ============================================================================ | |
| # TUTORIAL AND HELP FUNCTIONS | |
| # ============================================================================ | |
| def get_tutorials(): | |
| try: | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| cursor.execute(''' | |
| SELECT helpId, title, description, filePath, uploadDate | |
| FROM HelpTutorial | |
| ORDER BY uploadDate DESC | |
| ''') | |
| records = cursor.fetchall() | |
| conn.close() | |
| if not records: | |
| return "π **Tiada tutorial lagi / No tutorials yet**" | |
| output = '<div style="max-width: 100%;">' | |
| output += f'<h2 style="color: #FF6F00;">π Bantuan & Tutorial</h2>' | |
| output += f'<p style="font-size: 1.1rem; color: #666;"><strong>Jumlah Tutorial: {len(records)}</strong></p>' | |
| output += '<hr style="border: 2px solid #FFE0B2; margin: 20px 0;">' | |
| for rec in records: | |
| help_id, title, desc, file_path, date = rec | |
| output += '<div style="background: linear-gradient(135deg, #FFF3E0 0%, #FFE0B2 100%); border-radius: 15px; padding: 20px; margin-bottom: 25px; border: 2px solid #FF8F00; box-shadow: 0 5px 15px rgba(255, 143, 0, 0.2);">' | |
| output += f'<h3 style="color: #555555 ; margin-top: 0; font-size: 1.5rem;">π {title}</h3>' | |
| if desc: | |
| output += f'<p style="color: #555; font-size: 1rem; line-height: 1.6; margin: 10px 0;">{desc}</p>' | |
| if file_path: | |
| file_lower = file_path.lower() | |
| if 'youtube.com' in file_lower or 'youtu.be' in file_lower: | |
| video_id = "" | |
| if 'youtube.com/watch?v=' in file_lower: | |
| video_id = file_path.split('watch?v=')[1].split('&')[0] | |
| elif 'youtu.be/' in file_lower: | |
| video_id = file_path.split('youtu.be/')[1].split('?')[0] | |
| if video_id: | |
| output += f'<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; margin: 15px 0; border-radius: 10px; box-shadow: 0 4px 10px rgba(0,0,0,0.2);">' | |
| output += f'<iframe src="https://www.youtube.com/embed/{video_id}" ' | |
| output += 'style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0;" ' | |
| output += 'allowfullscreen></iframe></div>' | |
| else: | |
| output += f'<p style="color: #FF6F00;">π₯ <a href="{file_path}" target="_blank" style="color: #FF6F00; text-decoration: underline;">Tonton Video Tutorial</a></p>' | |
| elif file_lower.endswith('.pdf'): | |
| output += f'<div style="margin: 15px 0;">' | |
| output += f'<iframe src="{file_path}" style="width: 100%; height: 600px; border: 2px solid #FF8F00; border-radius: 10px; box-shadow: 0 4px 10px rgba(0,0,0,0.2);"></iframe>' | |
| output += f'</div>' | |
| output += f'<p style="text-align: center;"><a href="{file_path}" target="_blank" style="background: #FF8F00; color: white; padding: 10px 20px; border-radius: 8px; text-decoration: none; display: inline-block; font-weight: 600;">π₯ Muat Turun PDF</a></p>' | |
| else: | |
| output += f'<p style="color: #FF6F00;">π <a href="{file_path}" target="_blank" style="color: #FF6F00; text-decoration: underline;">Lihat Tutorial</a></p>' | |
| output += f'<p style="color: #888; font-size: 0.9rem; margin-top: 15px;">π Tarikh: {date}</p>' | |
| output += '</div>' | |
| output += '</div>' | |
| return output | |
| except Exception as e: | |
| return f"β Error: {e}" | |
| def add_tutorial(title, description, file_path): | |
| if not check_auth(required_role="teacher"): | |
| return "π Hanya GURU boleh menambah tutorial" | |
| if not title: | |
| return "β οΈ Sila masukkan tajuk" | |
| if not file_path: | |
| return "β οΈ Sila masukkan pautan YouTube atau laluan PDF" | |
| try: | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| date = datetime.now().strftime("%Y-%m-%d") | |
| cursor.execute(''' | |
| INSERT INTO HelpTutorial (teacherId, title, description, filePath, uploadDate) | |
| VALUES (?, ?, ?, ?, ?) | |
| ''', (current_user["id"], title, description or "", file_path, date)) | |
| conn.commit() | |
| conn.close() | |
| file_type = "Video" if 'youtube' in file_path.lower() or 'youtu.be' in file_path.lower() else "PDF" if file_path.lower().endswith('.pdf') else "Tutorial" | |
| return f"β {file_type} Tutorial berjaya ditambah!\n\nπ **{title}**\n\nSila refresh untuk melihat tutorial baru." | |
| except Exception as e: | |
| return f"β Error: {e}" | |
| # ============================================================================ | |
| # FEEDBACK FUNCTIONS | |
| # ============================================================================ | |
| def get_all_feedback(): | |
| """Display all feedback with category""" | |
| try: | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| cursor.execute(''' | |
| SELECT feedbackID, userType, category, feedbackText, rating, dateSubmitted | |
| FROM Feedback | |
| ORDER BY dateSubmitted DESC | |
| ''') | |
| records = cursor.fetchall() | |
| conn.close() | |
| if not records: | |
| return "π Tiada maklum balas lagi" | |
| output = f"## π¬ Maklum Balas & Penilaian\n\n**Jumlah: {len(records)}**\n\n" | |
| for rec in records: | |
| fb_id, user_type, category, text, rating, date = rec | |
| stars = "β" * rating | |
| output += f"### π¬ Feedback #{fb_id}\n" | |
| output += f"- **Pengguna:** {user_type}\n" | |
| output += f"- **Kategori:** {category or 'Umum'}\n" | |
| output += f"- **Penilaian:** {stars} ({rating}/5 bintang)\n" | |
| output += f"- **Maklum Balas:** {text}\n" | |
| output += f"- **Tarikh:** {date}\n\n---\n\n" | |
| return output | |
| except Exception as e: | |
| return f"β Error: {e}" | |
| def submit_feedback(user_type, category, feedback_text, rating): | |
| """Submit feedback with category and star rating""" | |
| # Validate all required fields | |
| if not category: | |
| return ( | |
| "β οΈ Sila pilih kategori maklum balas", | |
| gr.update(), # Don't clear user_type | |
| gr.update(), # Don't clear category | |
| gr.update(), # Don't clear text | |
| gr.update() # Don't clear rating | |
| ) | |
| if not feedback_text or not feedback_text.strip(): | |
| return ( | |
| "β οΈ Sila tulis maklum balas anda", | |
| gr.update(), | |
| gr.update(), | |
| gr.update(), | |
| gr.update() | |
| ) | |
| if not rating or rating == 0: | |
| return ( | |
| "β οΈ Sila berikan penilaian bintang", | |
| gr.update(), | |
| gr.update(), | |
| gr.update(), | |
| gr.update() | |
| ) | |
| try: | |
| conn = get_connection() | |
| cursor = conn.cursor() | |
| timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| user_id = current_user["id"] if current_user["is_logged_in"] else 0 | |
| cursor.execute(''' | |
| INSERT INTO Feedback (userType, userID, category, feedbackText, rating, dateSubmitted) | |
| VALUES (?, ?, ?, ?, ?, ?) | |
| ''', (user_type, user_id, category, feedback_text, int(rating), timestamp)) | |
| conn.commit() | |
| conn.close() | |
| # Display stars | |
| stars = "β" * int(rating) | |
| success_msg = f"β Terima kasih atas maklum balas anda!\n\nπ Kategori: {category}\n{stars} ({int(rating)}/5 bintang)" | |
| # Clear the form after successful submission | |
| return ( | |
| success_msg, # Status message | |
| gr.update(value="Pelajar"), # Reset user type to default | |
| gr.update(value="π Umum"), # Reset category to default | |
| gr.update(value=""), # Clear feedback text | |
| gr.update(value=5) # Reset rating to 5 stars | |
| ) | |
| except Exception as e: | |
| return ( | |
| f"β Error: {e}", | |
| gr.update(), | |
| gr.update(), | |
| gr.update(), | |
| gr.update() | |
| ) | |
| # ============================================================================ | |
| # CUSTOM CSS STYLING | |
| # ============================================================================ | |
| custom_css = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600&display=swap'); | |
| body { | |
| font-family: 'Fredoka', sans-serif !important; | |
| position: relative !important; | |
| } | |
| body::before { | |
| content: '' !important; | |
| position: fixed !important; | |
| top: 0 !important; | |
| left: 0 !important; | |
| width: 100% !important; | |
| height: 100% !important; | |
| background-image: url('https://static.vecteezy.com/system/resources/thumbnails/000/591/370/small_2x/dhhq_o7oo_180322.jpg') !important; | |
| background-size: cover !important; | |
| background-position: center !important; | |
| background-repeat: no-repeat !important; | |
| filter: blur(3px) !important; | |
| opacity: 0.5 !important; | |
| z-index: 0 !important; | |
| } | |
| .gradio-container { | |
| background: transparent !important; | |
| position: relative !important; | |
| z-index: 1 !important; | |
| } | |
| .main-title { | |
| color: #FFFFFF !important; | |
| text-align: center !important; | |
| text-shadow: 3px 3px 0px rgba(150, 120, 200, 0.6), 6px 6px 0px rgba(0, 0, 0, 0.2) !important; | |
| font-weight: 600 !important; | |
| margin-top: 20px !important; | |
| } | |
| .top-right-login { | |
| position: fixed !important; | |
| top: 20px !important; | |
| right: 20px !important; | |
| z-index: 100 !important; | |
| display: flex !important; | |
| gap: 10px !important; | |
| flex-direction: column !important; | |
| align-items: flex-end !important; | |
| } | |
| .sidebar-nav { | |
| background: rgba(50, 50, 50, 0.95) !important; | |
| border-radius: 25px !important; | |
| padding: 20px !important; | |
| box-shadow: 0 15px 40px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.1) !important; | |
| border: 3px solid rgba(100, 100, 100, 0.8) !important; | |
| backdrop-filter: blur(10px) !important; | |
| margin-bottom: 20px !important; | |
| } | |
| .nav-button { | |
| width: 100% !important; | |
| margin-bottom: 12px !important; | |
| text-align: center !important; | |
| font-size: 1rem !important; | |
| padding: 15px !important; | |
| border-radius: 15px !important; | |
| background: linear-gradient(135deg, #FFF3E0 0%, #FFE0B2 100%) !important; | |
| border: 2px solid #FF8F00 !important; | |
| color: #E65100 !important; | |
| font-weight: 600 !important; | |
| transition: all 0.3s ease !important; | |
| cursor: pointer !important; | |
| box-shadow: 0 5px 15px rgba(255, 143, 0, 0.3) !important; | |
| } | |
| .nav-button:hover { | |
| transform: translateY(-3px) !important; | |
| box-shadow: 0 8px 20px rgba(255, 143, 0, 0.4) !important; | |
| background: linear-gradient(135deg, #FFE0B2 0%, #FFCC80 100%) !important; | |
| } | |
| .gradio-group { | |
| background: rgba(255, 255, 255, 0.95) !important; | |
| border-radius: 20px !important; | |
| padding: 20px !important; | |
| box-shadow: 0 10px 30px rgba(150, 120, 200, 0.2) !important; | |
| width: 100% !important; | |
| } | |
| .gradio-image { | |
| width: 100% !important; | |
| max-width: 100% !important; | |
| } | |
| .gradio-textbox input, | |
| .gradio-textbox textarea, | |
| .gradio-image, | |
| .gradio-slider, | |
| .gradio-dropdown select { | |
| border: 2px solid #FFB6C1 !important; | |
| border-radius: 12px !important; | |
| background: rgba(255, 250, 240, 0.95) !important; | |
| } | |
| .gradio-button { | |
| border-radius: 15px !important; | |
| font-weight: 600 !important; | |
| border: 2px solid transparent !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .gradio-button.primary { | |
| background: linear-gradient(135deg, #FF69B4 0%, #FF1493 100%) !important; | |
| color: white !important; | |
| box-shadow: 0 5px 15px rgba(255, 105, 180, 0.3) !important; | |
| } | |
| .gradio-button.primary:hover { | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 8px 20px rgba(255, 105, 180, 0.4) !important; | |
| } | |
| .gradio-button.secondary { | |
| background: linear-gradient(135deg, #87CEEB 0%, #B0E0E6 100%) !important; | |
| color: #0047AB !important; | |
| box-shadow: 0 5px 15px rgba(135, 206, 235, 0.3) !important; | |
| } | |
| .gradio-button.secondary:hover { | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 8px 20px rgba(135, 206, 235, 0.4) !important; | |
| } | |
| .gradio-label { | |
| color: #FF1493 !important; | |
| font-weight: 600 !important; | |
| } | |
| .prose h1, .prose h2, .prose h3 { | |
| color: #FFFFFF !important; | |
| } | |
| div.markdown h2 { | |
| color: #FF1493 !important; | |
| border-bottom: 3px dashed #87CEEB !important; | |
| padding-bottom: 10px !important; | |
| } | |
| div.markdown h3 { | |
| color: #FF69B4 !important; | |
| } | |
| .gradio-radio, | |
| .gradio-checkbox { | |
| color: #FF1493 !important; | |
| } | |
| .hamburger-btn-open { | |
| position: fixed !important; | |
| top: 20px !important; | |
| left: 20px !important; | |
| z-index: 999 !important; | |
| background: rgba(50, 50, 50, 0.95) !important; | |
| color: white !important; | |
| border: 2px solid #FF69B4 !important; | |
| border-radius: 10px !important; | |
| padding: 10px 15px !important; | |
| font-size: 1.5rem !important; | |
| cursor: pointer !important; | |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3) !important; | |
| width: 60px !important; | |
| } | |
| .hamburger-btn-open:hover { | |
| background: rgba(80, 80, 80, 0.95) !important; | |
| transform: scale(1.1) !important; | |
| } | |
| .hamburger-btn-close { | |
| background: rgba(255, 105, 180, 0.3) !important; | |
| color: white !important; | |
| border: 2px solid #FF69B4 !important; | |
| border-radius: 10px !important; | |
| padding: 8px 12px !important; | |
| font-size: 1.3rem !important; | |
| cursor: pointer !important; | |
| margin-bottom: 15px !important; | |
| width: 100% !important; | |
| } | |
| .hamburger-btn-close:hover { | |
| background: rgba(255, 105, 180, 0.5) !important; | |
| transform: scale(1.05) !important; | |
| } | |
| """ | |
| # ============================================================================ | |
| # GRADIO UI - MAIN INTERFACE | |
| # ============================================================================ | |
| with gr.Blocks(css=custom_css, theme=gr.themes.Soft(primary_hue="amber")) as demo: | |
| # Top-right login area | |
| with gr.Row(): | |
| with gr.Column(scale=10): | |
| pass | |
| with gr.Column(scale=1, elem_classes="top-right-login"): | |
| login_button = gr.Button("π Log Masuk", variant="primary", size="lg", visible=True) | |
| user_display = gr.Markdown("") | |
| logout_btn = gr.Button("πͺ Log Keluar", variant="secondary", visible=False) | |
| # Sidebar at the top | |
| toggle_sidebar_open = gr.Button("β° Menu", elem_classes="hamburger-btn-open", size="sm", visible=False) | |
| with gr.Column(elem_classes="sidebar-nav", visible=True) as sidebar_nav: | |
| toggle_sidebar_close = gr.Button("β Tutup Menu", elem_classes="hamburger-btn-close", size="sm") | |
| with gr.Row(): | |
| gr.Markdown("### π Menu Utama") | |
| btn_vqa = gr.Button("π¨ Tanya Soalan", variant="primary", elem_classes="nav-button", scale=1) | |
| student_restricted_group = gr.Group(visible=False) | |
| with student_restricted_group: | |
| with gr.Row(): | |
| btn_library = gr.Button("π Perpustakaan", variant="secondary", elem_classes="nav-button", scale=1) | |
| btn_bookmark = gr.Button("π Kandungan Ditanda", variant="secondary", elem_classes="nav-button", scale=1) | |
| btn_student_progress = gr.Button("π Kemajuan Saya", variant="secondary", elem_classes="nav-button", scale=1) | |
| btn_challenge = gr.Button("π― Cabaran Harian STEM", variant="secondary", elem_classes="nav-button", scale=1) | |
| teacher_restricted_group = gr.Group(visible=False) | |
| with teacher_restricted_group: | |
| with gr.Row(): | |
| btn_teacher_progress = gr.Button("π Penjejak Kemajuan Pelajar", variant="secondary", elem_classes="nav-button", scale=1) | |
| btn_add_challenge = gr.Button("π― Tambah Cabaran", variant="secondary", elem_classes="nav-button", scale=1) | |
| btn_add_tutorial = gr.Button("β Tambah Tutorial", variant="secondary", elem_classes="nav-button", scale=1) | |
| btn_tutorial = gr.Button("β Bantuan & Tutorial", variant="secondary", elem_classes="nav-button", scale=1) | |
| btn_feedback = gr.Button("π¬ Maklum Balas & Penilaian", variant="secondary", elem_classes="nav-button", scale=1) | |
| btn_info = gr.Button("βΉοΈ Info", variant="secondary", elem_classes="nav-button", scale=1) | |
| # Title AFTER sidebar | |
| gr.HTML(""" | |
| <h1 class="main-title">π¨ Sistem VQA STEM Sekolah Rendah π€</h1> | |
| <p class="main-title" style="font-size: 3.5rem;"> | |
| Tanya, Muat Naik & Dapatkan Jawapan. Belajar Lebih Seronok ! | |
| </p> | |
| """) | |
| # Content column - FIXED TO scale=4 | |
| with gr.Column(scale=4) as content_column: | |
| # Login modal | |
| with gr.Group(visible=False) as section_login: | |
| gr.Markdown("### π Log Masuk / Daftar") | |
| with gr.Tab("π Pelajar"): | |
| student_login_group = gr.Group(visible=True) | |
| with student_login_group: | |
| gr.Markdown("#### π Log Masuk Pelajar") | |
| student_login_username = gr.Textbox(label="Username", placeholder="username") | |
| student_login_password = gr.Textbox(label="Password", type="password", placeholder="********") | |
| with gr.Row(): | |
| student_login_btn = gr.Button("π Log Masuk", variant="primary", scale=2) | |
| student_forget_pwd_btn = gr.Button("π Lupa Password", variant="secondary", scale=1) | |
| student_login_status = gr.Markdown() | |
| gr.Markdown("---") | |
| with gr.Row(): | |
| gr.Markdown("**Pengguna Baru?**") | |
| student_show_register_btn = gr.Button("π Daftar Sekarang", variant="secondary", size="sm") | |
| student_register_group = gr.Group(visible=False) | |
| with student_register_group: | |
| gr.Markdown("#### π Lengkapkan Maklumat Akaun Pelajar") | |
| student_reg_name = gr.Textbox(label="Nama Penuh *", placeholder="Contoh: Ali Bin Abu") | |
| student_reg_dob = gr.Textbox(label="Tarikh Lahir (DD/MM/YYYY) *", placeholder="Contoh: 15/03/2010") | |
| student_reg_gender = gr.Radio(["Lelaki", "Perempuan"], label="Jantina *", value="Lelaki") | |
| student_reg_class = gr.Textbox(label="Kelas *", placeholder="Contoh: 3A") | |
| student_reg_guardian_phone = gr.Textbox(label="No. HP Ibu Bapa/Penjaga *", placeholder="Contoh: 0123456789") | |
| student_reg_guardian_email = gr.Textbox(label="No. Emel Ibu Bapa/Penjaga *", placeholder="Contoh: ibu@email.com") | |
| student_reg_username = gr.Textbox(label="Username *", placeholder="Contoh: ali123") | |
| student_reg_password = gr.Textbox(label="Kata Laluan *", type="password", placeholder="Masukkan kata laluan") | |
| student_register_btn = gr.Button("π Daftar", variant="primary") | |
| student_register_status = gr.Markdown() | |
| gr.Markdown("---") | |
| student_back_to_login_btn = gr.Button("β Kembali ke Log Masuk", variant="secondary", size="sm") | |
| with gr.Tab("π¨βπ« Guru"): | |
| teacher_login_group = gr.Group(visible=True) | |
| with teacher_login_group: | |
| gr.Markdown("#### π Log Masuk Guru") | |
| teacher_login_username = gr.Textbox(label="Username", placeholder="username") | |
| teacher_login_password = gr.Textbox(label="Password", type="password", placeholder="********") | |
| with gr.Row(): | |
| teacher_login_btn = gr.Button("π Log Masuk", variant="primary", scale=2) | |
| teacher_forget_pwd_btn = gr.Button("π Lupa Password", variant="secondary", scale=1) | |
| teacher_login_status = gr.Markdown() | |
| gr.Markdown("---") | |
| with gr.Row(): | |
| gr.Markdown("**Pengguna Baru?**") | |
| teacher_show_register_btn = gr.Button("π Daftar Sekarang", variant="secondary", size="sm") | |
| teacher_register_group = gr.Group(visible=False) | |
| with teacher_register_group: | |
| gr.Markdown("#### π Lengkapkan Maklumat Akaun Guru") | |
| teacher_reg_name = gr.Textbox(label="Nama Penuh *", placeholder="Contoh: Encik Rahman Bin Ali") | |
| teacher_reg_gender = gr.Radio(["Lelaki", "Perempuan"], label="Jantina *", value="Lelaki") | |
| teacher_reg_phone = gr.Textbox(label="No. Telefon *", placeholder="Contoh: 0123456789") | |
| teacher_reg_email = gr.Textbox(label="Emel *", placeholder="Contoh: rahman@sekolah.edu.my") | |
| teacher_reg_subject = gr.Textbox(label="Subjek yang Diajar *", placeholder="Contoh: Sains") | |
| teacher_reg_username = gr.Textbox(label="Username *", placeholder="Contoh: rahman123") | |
| teacher_reg_password = gr.Textbox(label="Kata Laluan *", type="password", placeholder="Masukkan kata laluan") | |
| teacher_register_btn = gr.Button("π Daftar", variant="primary") | |
| teacher_register_status = gr.Markdown() | |
| gr.Markdown("---") | |
| teacher_back_to_login_btn = gr.Button("β Kembali ke Log Masuk", variant="secondary", size="sm") | |
| gr.Markdown("---") | |
| close_login_btn = gr.Button("β Tutup", variant="secondary") | |
| # VQA Section | |
| with gr.Group(visible=True) as section_vqa: | |
| gr.Markdown("## π¨ Muat Naik Imej & Tanya Soalan") | |
| image_input = gr.Image(type="pil", label="πΈ Gambar", height=300) | |
| with gr.Row(): | |
| question_input = gr.Textbox(label="Soalan",placeholder="Masukkan soalan di sini ....", lines=2, scale=7) | |
| submit_btn = gr.Button("π Tanya!", size="lg", variant="primary", scale=1) | |
| answer_output = gr.Textbox(label="π¬ Jawapan", lines=3, interactive=False) | |
| with gr.Row(): | |
| gr.Button("π¨ Warna?", size="sm").click(lambda: "Apa warna ini?", outputs=question_input) | |
| gr.Button("π’ Berapa?", size="sm").click(lambda: "Berapa banyak?", outputs=question_input) | |
| gr.Button("β Apa?", size="sm").click(lambda: "Apa ini?", outputs=question_input) | |
| # Content Library | |
| with gr.Group(visible=False) as section_library: | |
| gr.Markdown("## π Perpustakaan Kandungan") | |
| content_output = gr.Markdown() | |
| gr.Button("π Refresh", variant="secondary").click(fn=get_all_content, outputs=content_output) | |
| # Bookmarks | |
| with gr.Group(visible=False) as section_bookmark: | |
| gr.Markdown("## π Kandungan di Tanda") | |
| bookmark_output = gr.Markdown() | |
| gr.Button("π Refresh", variant="secondary").click(fn=get_bookmarks, outputs=bookmark_output) | |
| gr.Markdown("---\n### β Tambah Tanda Buku") | |
| with gr.Row(): | |
| bm_type = gr.Dropdown(["Content", "Challenge", "Question"], label="Jenis", value="Content") | |
| bm_id = gr.Number(label="ID Kandungan", value=1) | |
| add_bm_btn = gr.Button("π Tanda", variant="primary") | |
| add_bm_status = gr.Markdown() | |
| add_bm_btn.click(fn=add_bookmark, inputs=[bm_type, bm_id], outputs=add_bm_status) | |
| # Student Progress | |
| with gr.Group(visible=False) as section_student_progress: | |
| gr.Markdown("## π Laporan Prestasi Pelajar") | |
| gr.Markdown("Lihat prestasi pembelajaran anda dengan lengkap, termasuk graf kemajuan dan laporan boleh muat turun.") | |
| with gr.Row(): | |
| refresh_progress_btn = gr.Button("π Kemas Kini Laporan", variant="primary", size="lg") | |
| student_progress_output = gr.Markdown() | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| progress_graph = gr.Image(label="π Graf Kemajuan", type="filepath") | |
| with gr.Column(scale=1): | |
| progress_pdf = gr.File(label="π₯ Muat Turun Laporan PDF") | |
| # Connect refresh button | |
| refresh_progress_btn.click( | |
| fn=get_student_progress, | |
| outputs=[student_progress_output, progress_graph, progress_pdf] | |
| ) | |
| gr.Markdown(""" | |
| --- | |
| ### π Panduan: | |
| - **Kemas Kini Laporan**: Klik butang untuk dapatkan laporan terkini | |
| - **Graf Kemajuan**: Lihat visualisasi prestasi anda | |
| - **Muat Turun PDF**: Simpan atau cetak laporan untuk rekod | |
| """) | |
| # Teacher Progress | |
| with gr.Group(visible=False) as section_teacher_progress: | |
| gr.Markdown("## π Penjejak Kemajuan Semua Pelajar") | |
| teacher_progress_output = gr.Markdown() | |
| gr.Button("π Kemas Kini", variant="primary").click(fn=get_all_students_progress, outputs=teacher_progress_output) | |
| # Daily Challenge | |
| with gr.Group(visible=False) as section_challenge: | |
| gr.Markdown("## π― Cabaran Harian STEM") | |
| challenge_output = gr.Markdown() | |
| gr.Button("π Lihat Cabaran", variant="secondary").click(fn=get_daily_challenge, outputs=challenge_output) | |
| gr.Markdown("---\n### βοΈ Hantar Jawapan") | |
| challenge_answer = gr.Textbox(label="Jawapan Anda", lines=4) | |
| submit_challenge_btn = gr.Button("π€ Hantar", variant="primary") | |
| submit_challenge_status = gr.Markdown() | |
| submit_challenge_btn.click(fn=submit_challenge_answer, inputs=challenge_answer, outputs=submit_challenge_status) | |
| # Add Challenge (Teacher) | |
| with gr.Group(visible=False) as section_add_challenge: | |
| gr.Markdown("## π― Cipta Cabaran Baru") | |
| with gr.Row(): | |
| ch_title = gr.Textbox(label="Tajuk *", placeholder="Cabaran Matematik Minggu Ini") | |
| ch_desc = gr.Textbox(label="Penerangan *", lines=2, placeholder="Soalan matematik untuk pelajar...") | |
| ch_question = gr.Textbox(label="Soalan *", lines=3, placeholder="Berapa 5 + 3?") | |
| add_ch_btn = gr.Button("β Tambah Cabaran", variant="primary") | |
| add_ch_status = gr.Markdown() | |
| add_ch_btn.click(fn=add_challenge, inputs=[ch_title, ch_desc, ch_question], outputs=add_ch_status) | |
| # Tutorials | |
| with gr.Group(visible=False) as section_tutorial: | |
| gr.Markdown("## β Bantuan & Tutorial") | |
| tutorial_output = gr.HTML() | |
| gr.Button("π Refresh", variant="secondary").click(fn=get_tutorials, outputs=tutorial_output) | |
| # Add Tutorial (Teacher) | |
| with gr.Group(visible=False) as section_add_tutorial: | |
| gr.Markdown("## β Tambah Tutorial Baru") | |
| gr.Markdown("### π Maklumat Tutorial") | |
| tut_title = gr.Textbox( | |
| label="Tajuk Tutorial *", | |
| placeholder="Cara Guna Sistem VQA", | |
| info="Masukkan tajuk tutorial yang menarik" | |
| ) | |
| tut_desc = gr.Textbox( | |
| label="Penerangan *", | |
| lines=3, | |
| placeholder="Panduan lengkap untuk pelajar menggunakan sistem VQA dengan mudah...", | |
| info="Berikan penerangan ringkas tentang tutorial" | |
| ) | |
| gr.Markdown("### π₯ Pautan Video atau PDF") | |
| tut_file_path = gr.Textbox( | |
| label="YouTube URL atau Laluan PDF *", | |
| placeholder="https://www.youtube.com/watch?v=xxxxx atau https://example.com/tutorial.pdf", | |
| info="Masukkan pautan YouTube atau PDF" | |
| ) | |
| with gr.Row(): | |
| gr.Button("πΊ Contoh YouTube", size="sm").click( | |
| lambda: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", | |
| outputs=tut_file_path | |
| ) | |
| gr.Button("π Contoh PDF", size="sm").click( | |
| lambda: "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", | |
| outputs=tut_file_path | |
| ) | |
| add_tut_btn = gr.Button("β Tambah Tutorial", variant="primary", size="lg") | |
| add_tut_status = gr.Markdown() | |
| add_tut_btn.click( | |
| fn=add_tutorial, | |
| inputs=[tut_title, tut_desc, tut_file_path], | |
| outputs=add_tut_status | |
| ) | |
| # Feedback | |
| with gr.Group(visible=False) as section_feedback: | |
| gr.Markdown("## π¬ Maklum Balas & Penilaian") | |
| gr.Markdown("### π Hantar Maklum Balas") | |
| # User type selection | |
| fb_user_type = gr.Radio( | |
| ["Pelajar", "Guru", "Ibu Bapa"], | |
| label="Anda adalah *", | |
| value="Pelajar" | |
| ) | |
| # Category dropdown | |
| fb_category = gr.Dropdown( | |
| choices=[ | |
| "π¨ Sistem VQA (Tanya Soalan)", | |
| "π Perpustakaan Kandungan", | |
| "π Tanda Buku", | |
| "π Penjejak Kemajuan", | |
| "π― Cabaran Harian", | |
| "β Bantuan & Tutorial", | |
| "π Log Masuk & Pendaftaran", | |
| "π¨ Reka Bentuk & Antara Muka", | |
| "β‘ Prestasi & Kelajuan Sistem", | |
| "π Ralat/Bug", | |
| "π‘ Cadangan Penambahbaikan", | |
| "π± Kebolehgunaan (Mudah Guna)", | |
| "π Umum" | |
| ], | |
| label="Kategori Maklum Balas *", | |
| value="π Umum", | |
| info="Pilih bahagian sistem yang ingin anda komen" | |
| ) | |
| # Star rating with radio buttons for better UX | |
| fb_rating = gr.Radio( | |
| choices=[ | |
| ("β (1 bintang - Sangat Tidak Berpuas Hati)", 1), | |
| ("ββ (2 bintang - Tidak Berpuas Hati)", 2), | |
| ("βββ (3 bintang - Sederhana)", 3), | |
| ("ββββ (4 bintang - Berpuas Hati)", 4), | |
| ("βββββ (5 bintang - Sangat Berpuas Hati)", 5) | |
| ], | |
| label="Penilaian Bintang *", | |
| value=5, | |
| info="Klik untuk memilih rating" | |
| ) | |
| # Feedback text | |
| fb_text = gr.Textbox( | |
| label="Maklum Balas Anda *", | |
| lines=5, | |
| placeholder="Tulis maklum balas atau cadangan anda di sini...", | |
| info="Berikan maklum balas yang konstruktif untuk membantu kami menambah baik sistem" | |
| ) | |
| # Submit and Clear buttons | |
| with gr.Row(): | |
| submit_fb_btn = gr.Button("π€ Hantar Maklum Balas", variant="primary", size="lg", scale=2) | |
| clear_fb_btn = gr.Button("ποΈ Kosongkan Borang", variant="secondary", size="lg", scale=1) | |
| submit_fb_status = gr.Markdown() | |
| # Connect submit button - NOW WITH MULTIPLE OUTPUTS | |
| submit_fb_btn.click( | |
| fn=submit_feedback, | |
| inputs=[fb_user_type, fb_category, fb_text, fb_rating], | |
| outputs=[submit_fb_status, fb_user_type, fb_category, fb_text, fb_rating] | |
| ) | |
| # Connect clear button | |
| clear_fb_btn.click( | |
| fn=lambda: ( | |
| "", # Clear status message | |
| "Pelajar", # Reset user type | |
| "π Umum", # Reset category | |
| "", # Clear text | |
| 5 # Reset rating | |
| ), | |
| outputs=[submit_fb_status, fb_user_type, fb_category, fb_text, fb_rating] | |
| ) | |
| # Divider | |
| gr.Markdown("---\n### π¬ Semua Maklum Balas") | |
| # Display all feedback - FIXED: No scale parameter | |
| refresh_fb_btn = gr.Button("π Lihat Semua Maklum Balas", variant="secondary") | |
| all_feedback_output = gr.Markdown() | |
| # Connect refresh button | |
| refresh_fb_btn.click( | |
| fn=get_all_feedback, | |
| outputs=all_feedback_output | |
| ) | |
| # System Info | |
| with gr.Group(visible=False) as section_info: | |
| gr.Markdown(f""" | |
| ## βΉοΈ Maklumat Sistem | |
| **Model:** {model_name} | **Device:** {device} | |
| ### π Demo Accounts: | |
| - **Student:** ali / 123456 | |
| - **Teacher:** rahman / 123456 | |
| ### π Access Control: | |
| - π Public: VQA, Tutorial, Feedback | |
| - π Student: Library, Bookmarks, Progress, Challenges | |
| - π¨βπ« Teacher: All Progress, Add Challenge, Add Tutorial | |
| """) | |
| # Event Handlers | |
| def login_student_updated(username, password): | |
| msg, login_vis, logout_vis, student_vis, teacher_vis, user_info = login_student(username, password) | |
| return (msg, login_vis, logout_vis, student_vis, teacher_vis, user_info, gr.update(visible=False)) | |
| def login_teacher_updated(username, password): | |
| msg, login_vis, logout_vis, student_vis, teacher_vis, user_info = login_teacher(username, password) | |
| return (msg, login_vis, logout_vis, student_vis, teacher_vis, user_info, gr.update(visible=False)) | |
| def logout_user_updated(): | |
| msg, login_vis, logout_vis, student_vis, teacher_vis, user_info = logout_user() | |
| return (msg, login_vis, logout_vis, student_vis, teacher_vis, user_info, gr.update(visible=False)) | |
| # Login modal | |
| login_button.click(lambda: gr.update(visible=True), outputs=section_login) | |
| close_login_btn.click(lambda: gr.update(visible=False), outputs=section_login) | |
| # Sidebar toggle | |
| def toggle_sidebar_hide(): | |
| return ( | |
| gr.update(visible=False), | |
| gr.update(visible=True) | |
| ) | |
| def toggle_sidebar_show(): | |
| return ( | |
| gr.update(visible=True), | |
| gr.update(visible=False) | |
| ) | |
| toggle_sidebar_close.click( | |
| fn=toggle_sidebar_hide, | |
| outputs=[sidebar_nav, toggle_sidebar_open] | |
| ) | |
| toggle_sidebar_open.click( | |
| fn=toggle_sidebar_show, | |
| outputs=[sidebar_nav, toggle_sidebar_open] | |
| ) | |
| # Authentication | |
| #student_register_btn.click(fn=register_student, inputs=[student_reg_name, student_reg_username, student_reg_password, student_reg_class, student_reg_guardian, student_reg_dob, student_reg_gender], outputs=student_register_status) | |
| student_register_btn.click( | |
| fn=register_student, | |
| inputs=[ | |
| student_reg_name, # Nama Penuh | |
| student_reg_dob, # Tarikh Lahir (DD/MM/YY) | |
| student_reg_gender, # Jantina | |
| student_reg_class, # Kelas | |
| student_reg_guardian_phone, # No HP | |
| student_reg_guardian_email, # Email | |
| student_reg_username, # Username | |
| student_reg_password # Password | |
| ], | |
| outputs=student_register_status | |
| ) | |
| # Student Login | |
| student_login_btn.click( | |
| fn=login_student_updated, | |
| inputs=[student_login_username, student_login_password], | |
| outputs=[student_login_status, login_button, logout_btn, student_restricted_group, teacher_restricted_group, user_display, section_login] | |
| ) | |
| # Teacher Registration | |
| teacher_register_btn.click( | |
| fn=register_teacher, | |
| inputs=[ | |
| teacher_reg_name, # Nama Penuh | |
| teacher_reg_gender, # Jantina | |
| teacher_reg_phone, # No. Telefon | |
| teacher_reg_email, # Emel | |
| teacher_reg_subject, # Subjek | |
| teacher_reg_username, # Username | |
| teacher_reg_password # Kata Laluan | |
| ], | |
| outputs=teacher_register_status | |
| ) | |
| # Teacher Login | |
| teacher_login_btn.click( | |
| fn=login_teacher_updated, | |
| inputs=[teacher_login_username, teacher_login_password], | |
| outputs=[teacher_login_status, login_button, logout_btn, student_restricted_group, teacher_restricted_group, user_display, section_login] | |
| ) | |
| # Logout | |
| logout_btn.click( | |
| fn=logout_user_updated, | |
| outputs=[logout_btn, login_button, logout_btn, student_restricted_group, teacher_restricted_group, user_display, section_login] | |
| ) | |
| # Toggle login/register forms | |
| student_show_register_btn.click( | |
| lambda: (gr.update(visible=False), gr.update(visible=True)), | |
| outputs=[student_login_group, student_register_group] | |
| ) | |
| student_back_to_login_btn.click( | |
| lambda: (gr.update(visible=True), gr.update(visible=False)), | |
| outputs=[student_login_group, student_register_group] | |
| ) | |
| teacher_show_register_btn.click( | |
| lambda: (gr.update(visible=False), gr.update(visible=True)), | |
| outputs=[teacher_login_group, teacher_register_group] | |
| ) | |
| teacher_back_to_login_btn.click( | |
| lambda: (gr.update(visible=True), gr.update(visible=False)), | |
| outputs=[teacher_login_group, teacher_register_group] | |
| ) | |
| # Forget password | |
| student_forget_pwd_btn.click( | |
| lambda: "π§ Sila hubungi pentadbir sekolah untuk reset password anda.", | |
| outputs=student_login_status | |
| ) | |
| teacher_forget_pwd_btn.click( | |
| lambda: "π§ Sila hubungi pentadbir sekolah untuk reset password anda.", | |
| outputs=teacher_login_status | |
| ) | |
| # Section visibility | |
| def show_section(vqa, lib, bm, stud_prog, teach_prog, ch, add_ch, tut, add_tut, fb, info): | |
| return (gr.update(visible=vqa), gr.update(visible=lib), gr.update(visible=bm), gr.update(visible=stud_prog), gr.update(visible=teach_prog), gr.update(visible=ch), gr.update(visible=add_ch), gr.update(visible=tut), gr.update(visible=add_tut), gr.update(visible=fb), gr.update(visible=info)) | |
| btn_vqa.click(lambda: show_section(True, False, False, False, False, False, False, False, False, False, False), outputs=[section_vqa, section_library, section_bookmark, section_student_progress, section_teacher_progress, section_challenge, section_add_challenge, section_tutorial, section_add_tutorial, section_feedback, section_info]) | |
| btn_library.click(lambda: show_section(False, True, False, False, False, False, False, False, False, False, False), outputs=[section_vqa, section_library, section_bookmark, section_student_progress, section_teacher_progress, section_challenge, section_add_challenge, section_tutorial, section_add_tutorial, section_feedback, section_info]) | |
| btn_bookmark.click(lambda: show_section(False, False, True, False, False, False, False, False, False, False, False), outputs=[section_vqa, section_library, section_bookmark, section_student_progress, section_teacher_progress, section_challenge, section_add_challenge, section_tutorial, section_add_tutorial, section_feedback, section_info]) | |
| btn_student_progress.click(lambda: show_section(False, False, False, True, False, False, False, False, False, False, False), outputs=[section_vqa, section_library, section_bookmark, section_student_progress, section_teacher_progress, section_challenge, section_add_challenge, section_tutorial, section_add_tutorial, section_feedback, section_info]) | |
| btn_teacher_progress.click(lambda: show_section(False, False, False, False, True, False, False, False, False, False, False), outputs=[section_vqa, section_library, section_bookmark, section_student_progress, section_teacher_progress, section_challenge, section_add_challenge, section_tutorial, section_add_tutorial, section_feedback, section_info]) | |
| btn_challenge.click(lambda: show_section(False, False, False, False, False, True, False, False, False, False, False), outputs=[section_vqa, section_library, section_bookmark, section_student_progress, section_teacher_progress, section_challenge, section_add_challenge, section_tutorial, section_add_tutorial, section_feedback, section_info]) | |
| btn_add_challenge.click(lambda: show_section(False, False, False, False, False, False, True, False, False, False, False), outputs=[section_vqa, section_library, section_bookmark, section_student_progress, section_teacher_progress, section_challenge, section_add_challenge, section_tutorial, section_add_tutorial, section_feedback, section_info]) | |
| btn_tutorial.click(lambda: show_section(False, False, False, False, False, False, False, True, False, False, False), outputs=[section_vqa, section_library, section_bookmark, section_student_progress, section_teacher_progress, section_challenge, section_add_challenge, section_tutorial, section_add_tutorial, section_feedback, section_info]) | |
| btn_add_tutorial.click(lambda: show_section(False, False, False, False, False, False, False, False, True, False, False), outputs=[section_vqa, section_library, section_bookmark, section_student_progress, section_teacher_progress, section_challenge, section_add_challenge, section_tutorial, section_add_tutorial, section_feedback, section_info]) | |
| btn_feedback.click(lambda: show_section(False, False, False, False, False, False, False, False, False, True, False), outputs=[section_vqa, section_library, section_bookmark, section_student_progress, section_teacher_progress, section_challenge, section_add_challenge, section_tutorial, section_add_tutorial, section_feedback, section_info]) | |
| btn_info.click(lambda: show_section(False, False, False, False, False, False, False, False, False, False, True), outputs=[section_vqa, section_library, section_bookmark, section_student_progress, section_teacher_progress, section_challenge, section_add_challenge, section_tutorial, section_add_tutorial, section_feedback, section_info]) | |
| # VQA submit | |
| submit_btn.click(fn=answer_question, inputs=[image_input, question_input], outputs=answer_output) | |
| # Load tutorials on startup | |
| demo.load(fn=get_tutorials, outputs=tutorial_output) | |
| if __name__ == "__main__": | |
| demo.launch() |