my-vqa-system / app.py
szaharah's picture
Update app.py
f8ce216 verified
# ============================================================================
# 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()