Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -640,4 +640,896 @@ class SchoolService:
|
|
| 640 |
assignments = json.loads(result[0]) if result[0] else []
|
| 641 |
assignments.append(new_assignment)
|
| 642 |
cursor.execute('''
|
| 643 |
-
UPDATE student_sessions SET assignments = ? WHERE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 640 |
assignments = json.loads(result[0]) if result[0] else []
|
| 641 |
assignments.append(new_assignment)
|
| 642 |
cursor.execute('''
|
| 643 |
+
UPDATE student_sessions SET assignments = ? WHERE username = ?
|
| 644 |
+
''', (json.dumps(assignments), username))
|
| 645 |
+
|
| 646 |
+
self.conn.commit()
|
| 647 |
+
return assignment_id
|
| 648 |
+
except Exception as e:
|
| 649 |
+
print(f"Create assignment error: {e}")
|
| 650 |
+
return -1
|
| 651 |
+
|
| 652 |
+
# Initialize school service
|
| 653 |
+
school_service = SchoolService()
|
| 654 |
+
|
| 655 |
+
# ==================== ADMIN SERVICE WITH SQLITE ====================
|
| 656 |
+
|
| 657 |
+
class AdminService:
|
| 658 |
+
def __init__(self):
|
| 659 |
+
self.conn = db_manager.get_connection()
|
| 660 |
+
|
| 661 |
+
def __del__(self):
|
| 662 |
+
if hasattr(self, 'conn'):
|
| 663 |
+
self.conn.close()
|
| 664 |
+
|
| 665 |
+
def authenticate(self, username: str, password: str) -> Optional[Dict]:
|
| 666 |
+
try:
|
| 667 |
+
cursor = self.conn.cursor()
|
| 668 |
+
cursor.execute('''
|
| 669 |
+
SELECT name, role FROM teachers
|
| 670 |
+
WHERE username = ? AND password = ?
|
| 671 |
+
''', (username, password))
|
| 672 |
+
result = cursor.fetchone()
|
| 673 |
+
if result:
|
| 674 |
+
return {"name": result[0], "role": result[1]}
|
| 675 |
+
return None
|
| 676 |
+
except Exception as e:
|
| 677 |
+
print(f"Admin auth error: {e}")
|
| 678 |
+
return None
|
| 679 |
+
|
| 680 |
+
# Initialize admin service
|
| 681 |
+
admin_service = AdminService()
|
| 682 |
+
|
| 683 |
+
# ==================== OPENAI SETUP ====================
|
| 684 |
+
|
| 685 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
| 686 |
+
USE_OPENAI = bool(OPENAI_API_KEY)
|
| 687 |
+
|
| 688 |
+
if USE_OPENAI:
|
| 689 |
+
try:
|
| 690 |
+
from openai import OpenAI
|
| 691 |
+
client = OpenAI(api_key=OPENAI_API_KEY)
|
| 692 |
+
except Exception as e:
|
| 693 |
+
print(f"β οΈ OpenAI error: {e}")
|
| 694 |
+
USE_OPENAI = False
|
| 695 |
+
else:
|
| 696 |
+
print("β οΈ OPENAI_API_KEY not set. Using mock responses.")
|
| 697 |
+
|
| 698 |
+
# ==================== AI CHAT FUNCTION ====================
|
| 699 |
+
|
| 700 |
+
def ai_chat(message: str, history: List, username: str = "guest") -> tuple:
|
| 701 |
+
if not message.strip():
|
| 702 |
+
return history, ""
|
| 703 |
+
|
| 704 |
+
thinking_msg = "π€ ThutoAI is thinking..."
|
| 705 |
+
history.append((message, thinking_msg))
|
| 706 |
+
yield history, ""
|
| 707 |
+
|
| 708 |
+
if USE_OPENAI:
|
| 709 |
+
try:
|
| 710 |
+
system_prompt = f"""You are ThutoAI, a friendly and knowledgeable AI assistant for students.
|
| 711 |
+
Context from school:
|
| 712 |
+
{school_service.get_school_context_for_ai()}
|
| 713 |
+
|
| 714 |
+
Guidelines:
|
| 715 |
+
- Be encouraging, clear, and concise.
|
| 716 |
+
- Use emojis sparingly to keep it fun πππ―
|
| 717 |
+
- If asked about deadlines or exams, refer to context.
|
| 718 |
+
- Offer study tips if appropriate.
|
| 719 |
+
- Never invent facts β say 'I don't know' if unsure."""
|
| 720 |
+
|
| 721 |
+
response = client.chat.completions.create(
|
| 722 |
+
model="gpt-3.5-turbo",
|
| 723 |
+
messages=[
|
| 724 |
+
{"role": "system", "content": system_prompt},
|
| 725 |
+
{"role": "user", "content": message}
|
| 726 |
+
],
|
| 727 |
+
temperature=0.7,
|
| 728 |
+
max_tokens=500
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
reply = response.choices[0].message.content.strip()
|
| 732 |
+
|
| 733 |
+
keywords = ["exam", "test", "due", "assignment", "deadline", "when", "what", "grade", "score"]
|
| 734 |
+
if any(kw in message.lower() for kw in keywords):
|
| 735 |
+
matches = school_service.get_announcements()
|
| 736 |
+
relevant = [a for a in matches if any(kw in a["title"].lower() or kw in a["content"].lower() for kw in keywords)]
|
| 737 |
+
if relevant:
|
| 738 |
+
reply += "\n\nπ **Quick Info from School:**"
|
| 739 |
+
for ann in relevant[:2]:
|
| 740 |
+
emoji = "π¨" if ann["priority"] == "high" else "π" if ann["priority"] == "normal" else "βΉοΈ"
|
| 741 |
+
reply += f"\n{emoji} **{ann['title']}** ({ann['course']})\n β {ann['content'][:70]}..."
|
| 742 |
+
|
| 743 |
+
except Exception as e:
|
| 744 |
+
reply = f"β οΈ Sorry, I had a glitch: {str(e)}"
|
| 745 |
+
else:
|
| 746 |
+
time.sleep(1.5)
|
| 747 |
+
reply = f"π Hi! I'm ThutoAI. You asked: '{message}'.\nπ‘ *Pro tip: Add your OpenAI API key in HF Secrets for smarter answers!*"
|
| 748 |
+
|
| 749 |
+
history[-1] = (message, reply)
|
| 750 |
+
|
| 751 |
+
if username != "guest":
|
| 752 |
+
student_service.add_to_chat_history(username, message, reply)
|
| 753 |
+
|
| 754 |
+
yield history, ""
|
| 755 |
+
|
| 756 |
+
|
| 757 |
+
# ==================== UI RENDERING HELPERS ====================
|
| 758 |
+
|
| 759 |
+
def render_announcements(course: str) -> str:
|
| 760 |
+
announcements = school_service.get_announcements(course)
|
| 761 |
+
if not announcements:
|
| 762 |
+
return """
|
| 763 |
+
<div style='text-align: center; padding: 40px; color: #6c757d;'>
|
| 764 |
+
<div style='font-size: 4em; margin-bottom: 16px;'>π</div>
|
| 765 |
+
<h3>No announcements for this course.</h3>
|
| 766 |
+
<p>Check back later or select "All" to see everything.</p>
|
| 767 |
+
</div>
|
| 768 |
+
"""
|
| 769 |
+
|
| 770 |
+
html = "<div style='display: grid; gap: 16px;'>"
|
| 771 |
+
priority_icons = {"high": "π¨", "normal": "π", "low": "βΉοΈ"}
|
| 772 |
+
priority_colors = {"high": "#dc3545", "normal": "#ffc107", "low": "#6c757d"}
|
| 773 |
+
|
| 774 |
+
for ann in announcements:
|
| 775 |
+
icon = priority_icons.get(ann["priority"], "π")
|
| 776 |
+
color = priority_colors.get(ann["priority"], "#6c757d")
|
| 777 |
+
html += f"""
|
| 778 |
+
<div style='
|
| 779 |
+
background: white;
|
| 780 |
+
border-radius: 12px;
|
| 781 |
+
padding: 20px;
|
| 782 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
| 783 |
+
border-left: 4px solid {color};
|
| 784 |
+
transition: transform 0.2s, box-shadow 0.2s;
|
| 785 |
+
'>
|
| 786 |
+
<div style='display: flex; align-items: flex-start; gap: 12px;'>
|
| 787 |
+
<div style='font-size: 1.5em;'>{icon}</div>
|
| 788 |
+
<div style='flex: 1;'>
|
| 789 |
+
<div style='display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;'>
|
| 790 |
+
<h3 style='margin: 0; color: #212529;'>{ann['title']}</h3>
|
| 791 |
+
<span style='
|
| 792 |
+
background: {color};
|
| 793 |
+
color: white;
|
| 794 |
+
padding: 4px 10px;
|
| 795 |
+
border-radius: 20px;
|
| 796 |
+
font-size: 0.8em;
|
| 797 |
+
font-weight: bold;
|
| 798 |
+
'>{ann['priority'].upper()}</span>
|
| 799 |
+
</div>
|
| 800 |
+
<p style='margin: 8px 0; color: #495057; line-height: 1.5;'>{ann['content']}</p>
|
| 801 |
+
<div style='display: flex; justify-content: space-between; font-size: 0.85em; color: #6c757d; margin-top: 12px;'>
|
| 802 |
+
<span>π {ann['course']}</span>
|
| 803 |
+
<span>π
{ann['date']}</span>
|
| 804 |
+
<span>π¨βπ« {ann['posted_by']}</span>
|
| 805 |
+
<span>ποΈ {ann['views']} views</span>
|
| 806 |
+
</div>
|
| 807 |
+
</div>
|
| 808 |
+
</div>
|
| 809 |
+
</div>
|
| 810 |
+
"""
|
| 811 |
+
html += "</div>"
|
| 812 |
+
return html
|
| 813 |
+
|
| 814 |
+
|
| 815 |
+
def render_assignments(assignments: List[Dict]) -> str:
|
| 816 |
+
if not assignments:
|
| 817 |
+
return """
|
| 818 |
+
<div style='text-align: center; padding: 40px; color: #6c757d;'>
|
| 819 |
+
<div style='font-size: 4em; margin-bottom: 16px;'>π
</div>
|
| 820 |
+
<h3>No upcoming assignments.</h3>
|
| 821 |
+
<p>Ask your teacher or join a class group to see assignments.</p>
|
| 822 |
+
</div>
|
| 823 |
+
"""
|
| 824 |
+
|
| 825 |
+
html = "<div style='display: grid; gap: 16px;'>"
|
| 826 |
+
today = datetime.today().date()
|
| 827 |
+
|
| 828 |
+
for task in assignments:
|
| 829 |
+
due_date = datetime.strptime(task["due_date"], "%Y-%m-%d").date()
|
| 830 |
+
days_left = (due_date - today).days
|
| 831 |
+
is_overdue = days_left < 0
|
| 832 |
+
is_today = days_left == 0
|
| 833 |
+
|
| 834 |
+
if is_overdue:
|
| 835 |
+
badge = "π¨ OVERDUE"
|
| 836 |
+
color = "#dc3545"
|
| 837 |
+
elif is_today:
|
| 838 |
+
badge = "π― TODAY"
|
| 839 |
+
color = "#fd7e14"
|
| 840 |
+
elif days_left <= 2:
|
| 841 |
+
badge = f"β οΈ Due in {days_left} day{'s' if days_left != 1 else ''}"
|
| 842 |
+
color = "#ffc107"
|
| 843 |
+
else:
|
| 844 |
+
badge = f"β
Due in {days_left} days"
|
| 845 |
+
color = "#28a745"
|
| 846 |
+
|
| 847 |
+
html += f"""
|
| 848 |
+
<div style='
|
| 849 |
+
background: white;
|
| 850 |
+
border-radius: 12px;
|
| 851 |
+
padding: 20px;
|
| 852 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
| 853 |
+
border-left: 4px solid {color};
|
| 854 |
+
transition: transform 0.2s;
|
| 855 |
+
'>
|
| 856 |
+
<div style='display: flex; justify-content: space-between; align-items: flex-start;'>
|
| 857 |
+
<div>
|
| 858 |
+
<h3 style='margin: 0 0 8px 0; color: #212529;'>{task['title']}</h3>
|
| 859 |
+
<div style='color: #6c757d; margin-bottom: 8px;'>π {task['course']} Β· π©βπ« {task['assigned_by']}</div>
|
| 860 |
+
<div style='color: #495057;'>π
Due: {task['due_date']}</div>
|
| 861 |
+
<div style='color: #6c757d; font-size: 0.9em;'>{task.get('description', '')}</div>
|
| 862 |
+
</div>
|
| 863 |
+
<span style='
|
| 864 |
+
background: {color};
|
| 865 |
+
color: white;
|
| 866 |
+
padding: 6px 12px;
|
| 867 |
+
border-radius: 20px;
|
| 868 |
+
font-weight: bold;
|
| 869 |
+
font-size: 0.85em;
|
| 870 |
+
align-self: flex-start;
|
| 871 |
+
'>{badge}</span>
|
| 872 |
+
</div>
|
| 873 |
+
</div>
|
| 874 |
+
"""
|
| 875 |
+
|
| 876 |
+
html += "</div>"
|
| 877 |
+
return html
|
| 878 |
+
|
| 879 |
+
|
| 880 |
+
def render_groups(groups: List[str]) -> str:
|
| 881 |
+
if not groups:
|
| 882 |
+
return """
|
| 883 |
+
<div style='text-align: center; padding: 40px; color: #6c757d;'>
|
| 884 |
+
<div style='font-size: 4em; margin-bottom: 16px;'>π₯</div>
|
| 885 |
+
<h3>You haven't joined any class groups yet.</h3>
|
| 886 |
+
<p>Ask your teacher for a group code to join your class.</p>
|
| 887 |
+
</div>
|
| 888 |
+
"""
|
| 889 |
+
|
| 890 |
+
html = "<div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px;'>"
|
| 891 |
+
for group in groups:
|
| 892 |
+
html += f"""
|
| 893 |
+
<div style='
|
| 894 |
+
background: #e3f2fd;
|
| 895 |
+
border-radius: 12px;
|
| 896 |
+
padding: 20px;
|
| 897 |
+
text-align: center;
|
| 898 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 899 |
+
border: 2px solid #2196f3;
|
| 900 |
+
'>
|
| 901 |
+
<div style='font-size: 1.5em; margin-bottom: 8px;'>π</div>
|
| 902 |
+
<h3 style='margin: 0; color: #1976d2;'>{group}</h3>
|
| 903 |
+
<p style='margin: 8px 0 0 0; color: #555; font-size: 0.9em;'>Class Group</p>
|
| 904 |
+
</div>
|
| 905 |
+
"""
|
| 906 |
+
html += "</div>"
|
| 907 |
+
return html
|
| 908 |
+
|
| 909 |
+
|
| 910 |
+
def get_avatar_html(avatar_path: Optional[str], name: str) -> str:
|
| 911 |
+
if avatar_path and os.path.exists(avatar_path):
|
| 912 |
+
try:
|
| 913 |
+
with open(avatar_path, "rb") as f:
|
| 914 |
+
img_data = base64.b64encode(f.read()).decode()
|
| 915 |
+
img_html = f'<img src="data:image/png;base64,{img_data}" style="width: 60px; height: 60px; border-radius: 50%; object-fit: cover; border: 2px solid #6e8efb;">'
|
| 916 |
+
except:
|
| 917 |
+
img_html = f'<div style="width: 60px; height: 60px; border-radius: 50%; background: #6e8efb; color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.2em; border: 2px solid #6e8efb;">{name[0].upper()}</div>'
|
| 918 |
+
else:
|
| 919 |
+
img_html = f'<div style="width: 60px; height: 60px; border-radius: 50%; background: #6e8efb; color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.2em; border: 2px solid #6e8efb;">{name[0].upper()}</div>'
|
| 920 |
+
|
| 921 |
+
return img_html
|
| 922 |
+
|
| 923 |
+
|
| 924 |
+
def render_analytics() -> str:
|
| 925 |
+
total_students = student_service.get_total_students()
|
| 926 |
+
active_students = student_service.get_active_students()
|
| 927 |
+
total_assignments = student_service.get_total_assignments()
|
| 928 |
+
group_popularity = student_service.get_group_popularity()
|
| 929 |
+
recent_activity = student_service.get_recent_activity()
|
| 930 |
+
school_stats = school_service.get_stats()
|
| 931 |
+
|
| 932 |
+
html = f"""
|
| 933 |
+
<div style='display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; margin-bottom: 32px;'>
|
| 934 |
+
<div style='background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
|
| 935 |
+
<h3 style='color: #212529; margin: 0 0 16px 0;'>π₯ Student Engagement</h3>
|
| 936 |
+
<div style='display: grid; grid-template-columns: 1fr 1fr; gap: 16px;'>
|
| 937 |
+
<div style='text-align: center; padding: 16px; background: #e3f2fd; border-radius: 8px;'>
|
| 938 |
+
<div style='font-size: 2em; color: #1976d2;'>{total_students}</div>
|
| 939 |
+
<div>Total Students</div>
|
| 940 |
+
</div>
|
| 941 |
+
<div style='text-align: center; padding: 16px; background: #e8f5e8; border-radius: 8px;'>
|
| 942 |
+
<div style='font-size: 2em; color: #2e7d32;'>{active_students}</div>
|
| 943 |
+
<div>Active Students</div>
|
| 944 |
+
</div>
|
| 945 |
+
</div>
|
| 946 |
+
</div>
|
| 947 |
+
|
| 948 |
+
<div style='background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
|
| 949 |
+
<h3 style='color: #212529; margin: 0 0 16px 0;'>π Assignment Stats</h3>
|
| 950 |
+
<div style='display: grid; grid-template-columns: 1fr 1fr; gap: 16px;'>
|
| 951 |
+
<div style='text-align: center; padding: 16px; background: #fff3e0; border-radius: 8px;'>
|
| 952 |
+
<div style='font-size: 2em; color: #ef6c00;'>{total_assignments}</div>
|
| 953 |
+
<div>Total Assignments</div>
|
| 954 |
+
</div>
|
| 955 |
+
<div style='text-align: center; padding: 16px; background: #f3e5f5; border-radius: 8px;'>
|
| 956 |
+
<div style='font-size: 2em; color: #7b1fa2;'>N/A</div>
|
| 957 |
+
<div>Completion Rate</div>
|
| 958 |
+
</div>
|
| 959 |
+
</div>
|
| 960 |
+
</div>
|
| 961 |
+
</div>
|
| 962 |
+
|
| 963 |
+
<div style='display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-bottom: 32px;'>
|
| 964 |
+
<div style='background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
|
| 965 |
+
<h3 style='color: #212529; margin: 0 0 16px 0;'>π Popular Class Groups</h3>
|
| 966 |
+
<div style='display: grid; gap: 12px;'>
|
| 967 |
+
"""
|
| 968 |
+
|
| 969 |
+
sorted_groups = sorted(group_popularity.items(), key=lambda x: x[1], reverse=True)
|
| 970 |
+
for group, count in sorted_groups[:5]:
|
| 971 |
+
percentage = (count / total_students * 100) if total_students > 0 else 0
|
| 972 |
+
html += f"""
|
| 973 |
+
<div style='display: flex; align-items: center; gap: 12px;'>
|
| 974 |
+
<div style='width: 100%; background: #e0e0e0; height: 8px; border-radius: 4px;'>
|
| 975 |
+
<div style='width: {percentage}%; background: #6e8efb; height: 100%; border-radius: 4px;'></div>
|
| 976 |
+
</div>
|
| 977 |
+
<div style='min-width: 80px;'>{group}</div>
|
| 978 |
+
<div style='color: #6c757d;'>{count} students</div>
|
| 979 |
+
</div>
|
| 980 |
+
"""
|
| 981 |
+
|
| 982 |
+
html += """
|
| 983 |
+
</div>
|
| 984 |
+
</div>
|
| 985 |
+
|
| 986 |
+
<div style='background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
|
| 987 |
+
<h3 style='color: #212529; margin: 0 0 16px 0;'>π’ Announcement Stats</h3>
|
| 988 |
+
<div style='display: grid; gap: 12px;'>
|
| 989 |
+
"""
|
| 990 |
+
|
| 991 |
+
for ann in school_service.get_announcements()[:5]:
|
| 992 |
+
views = ann.get("views", 0)
|
| 993 |
+
max_views = max(a.get("views", 0) for a in school_service.get_announcements()) if school_service.get_announcements() else 1
|
| 994 |
+
width_percent = (views / max_views * 100) if max_views > 0 else 0
|
| 995 |
+
html += f"""
|
| 996 |
+
<div style='display: flex; align-items: center; gap: 12px;'>
|
| 997 |
+
<div style='width: 100%; background: #e0e0e0; height: 8px; border-radius: 4px;'>
|
| 998 |
+
<div style='width: {width_percent}%; background: #ff7043; height: 100%; border-radius: 4px;'></div>
|
| 999 |
+
</div>
|
| 1000 |
+
<div style='min-width: 120px; font-size: 0.9em;'>{ann['title'][:20]}...</div>
|
| 1001 |
+
<div style='color: #6c757d;'>{views} views</div>
|
| 1002 |
+
</div>
|
| 1003 |
+
"""
|
| 1004 |
+
|
| 1005 |
+
html += """
|
| 1006 |
+
</div>
|
| 1007 |
+
</div>
|
| 1008 |
+
</div>
|
| 1009 |
+
|
| 1010 |
+
<div style='background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);'>
|
| 1011 |
+
<h3 style='color: #212529; margin: 0 0 16px 0;'>β±οΈ Recent Activity</h3>
|
| 1012 |
+
<div style='display: grid; gap: 12px;'>
|
| 1013 |
+
"""
|
| 1014 |
+
|
| 1015 |
+
for activity in recent_activity:
|
| 1016 |
+
html += f"""
|
| 1017 |
+
<div style='padding: 12px; background: #f8f9fa; border-radius: 8px; display: flex; justify-content: space-between; align-items: center;'>
|
| 1018 |
+
<div>
|
| 1019 |
+
<strong>{activity['student']}</strong> received assignment: <em>{activity['last_assignment']}</em>
|
| 1020 |
+
</div>
|
| 1021 |
+
<div style='color: #6c757d; font-size: 0.9em;'>{activity['date']}</div>
|
| 1022 |
+
</div>
|
| 1023 |
+
"""
|
| 1024 |
+
|
| 1025 |
+
html += """
|
| 1026 |
+
</div>
|
| 1027 |
+
</div>
|
| 1028 |
+
"""
|
| 1029 |
+
|
| 1030 |
+
return html
|
| 1031 |
+
|
| 1032 |
+
# ==================== STATE MANAGEMENT ====================
|
| 1033 |
+
|
| 1034 |
+
CURRENT_USER = "guest"
|
| 1035 |
+
CURRENT_TEACHER = None
|
| 1036 |
+
DARK_MODE = False
|
| 1037 |
+
|
| 1038 |
+
def login_student(username: str, password: str) -> tuple:
|
| 1039 |
+
global CURRENT_USER, DARK_MODE
|
| 1040 |
+
student_data = student_service.authenticate_student(username, password)
|
| 1041 |
+
if student_
|
| 1042 |
+
CURRENT_USER = username
|
| 1043 |
+
DARK_MODE = student_data["dark_mode"]
|
| 1044 |
+
chat_history = student_service.get_chat_history(username)
|
| 1045 |
+
files = student_service.get_files(username)
|
| 1046 |
+
assignments = student_service.get_assignments(username)
|
| 1047 |
+
groups = student_service.get_groups(username)
|
| 1048 |
+
avatar_html = get_avatar_html(student_data["avatar"], student_data["name"])
|
| 1049 |
+
welcome_msg = f"Welcome back, {student_data['name']}!"
|
| 1050 |
+
|
| 1051 |
+
return (
|
| 1052 |
+
gr.update(visible=False), # login_group
|
| 1053 |
+
gr.update(visible=True), # main_app
|
| 1054 |
+
gr.update(value=chat_history), # chatbot
|
| 1055 |
+
gr.update(value=files), # files
|
| 1056 |
+
gr.update(value=render_assignments(assignments)), # assignments_display
|
| 1057 |
+
gr.update(value=render_groups(groups)), # groups_display
|
| 1058 |
+
gr.update(value=welcome_msg), # login_status
|
| 1059 |
+
gr.update(value=student_data["name"], visible=True), # user_display
|
| 1060 |
+
gr.update(visible=True), # logout_btn
|
| 1061 |
+
gr.update(value=avatar_html), # avatar_display
|
| 1062 |
+
gr.update(value="π Light Mode" if DARK_MODE else "βοΈ Dark Mode"), # dark_mode_btn
|
| 1063 |
+
gr.update() # css placeholder
|
| 1064 |
+
)
|
| 1065 |
+
return (
|
| 1066 |
+
gr.update(visible=True),
|
| 1067 |
+
gr.update(visible=False),
|
| 1068 |
+
gr.update(),
|
| 1069 |
+
gr.update(),
|
| 1070 |
+
gr.update(),
|
| 1071 |
+
gr.update(),
|
| 1072 |
+
gr.update(value="β Invalid username or password"),
|
| 1073 |
+
gr.update(visible=False),
|
| 1074 |
+
gr.update(visible=False),
|
| 1075 |
+
gr.update(),
|
| 1076 |
+
gr.update(),
|
| 1077 |
+
gr.update()
|
| 1078 |
+
)
|
| 1079 |
+
|
| 1080 |
+
def register_student(username: str, password: str, name: str) -> str:
|
| 1081 |
+
return student_service.register_student(username, password, name)
|
| 1082 |
+
|
| 1083 |
+
def logout_student() -> tuple:
|
| 1084 |
+
global CURRENT_USER, DARK_MODE
|
| 1085 |
+
CURRENT_USER = "guest"
|
| 1086 |
+
DARK_MODE = False
|
| 1087 |
+
return (
|
| 1088 |
+
gr.update(visible=True),
|
| 1089 |
+
gr.update(visible=False),
|
| 1090 |
+
gr.update(),
|
| 1091 |
+
gr.update(),
|
| 1092 |
+
gr.update(),
|
| 1093 |
+
gr.update(),
|
| 1094 |
+
gr.update(),
|
| 1095 |
+
gr.update(visible=False),
|
| 1096 |
+
gr.update(visible=False),
|
| 1097 |
+
gr.update(),
|
| 1098 |
+
gr.update(value="βοΈ Dark Mode"),
|
| 1099 |
+
gr.update()
|
| 1100 |
+
)
|
| 1101 |
+
|
| 1102 |
+
def toggle_dark_mode() -> tuple:
|
| 1103 |
+
global DARK_MODE, CURRENT_USER
|
| 1104 |
+
DARK_MODE = not DARK_MODE
|
| 1105 |
+
if CURRENT_USER != "guest":
|
| 1106 |
+
student_service.update_dark_mode(CURRENT_USER, DARK_MODE)
|
| 1107 |
+
btn_text = "π Light Mode" if DARK_MODE else "βοΈ Dark Mode"
|
| 1108 |
+
return btn_text, gr.update()
|
| 1109 |
+
|
| 1110 |
+
def upload_avatar(file) -> str:
|
| 1111 |
+
if not file:
|
| 1112 |
+
return "β No file selected"
|
| 1113 |
+
if CURRENT_USER == "guest":
|
| 1114 |
+
return "β Please log in first"
|
| 1115 |
+
# In production, save file to disk
|
| 1116 |
+
# For demo, just store filename
|
| 1117 |
+
student_service.update_avatar(CURRENT_USER, file.name)
|
| 1118 |
+
return f"β
Avatar updated!"
|
| 1119 |
+
|
| 1120 |
+
def join_group(group_code: str) -> tuple:
|
| 1121 |
+
if CURRENT_USER == "guest":
|
| 1122 |
+
return "β Please log in first.", gr.update()
|
| 1123 |
+
result = student_service.join_group(CURRENT_USER, group_code)
|
| 1124 |
+
groups = student_service.get_groups(CURRENT_USER)
|
| 1125 |
+
return result, gr.update(value=render_groups(groups))
|
| 1126 |
+
|
| 1127 |
+
def leave_group(group_code: str) -> tuple:
|
| 1128 |
+
if CURRENT_USER == "guest":
|
| 1129 |
+
return "β Please log in first.", gr.update()
|
| 1130 |
+
result = student_service.leave_group(CURRENT_USER, group_code)
|
| 1131 |
+
groups = student_service.get_groups(CURRENT_USER)
|
| 1132 |
+
return result, gr.update(value=render_groups(groups))
|
| 1133 |
+
|
| 1134 |
+
def upload_file_for_student(file) -> str:
|
| 1135 |
+
if not file:
|
| 1136 |
+
return "β No file selected"
|
| 1137 |
+
result = f"β
Uploaded: {file.name}"
|
| 1138 |
+
if CURRENT_USER != "guest":
|
| 1139 |
+
student_service.add_file(CURRENT_USER, file.name)
|
| 1140 |
+
return result
|
| 1141 |
+
|
| 1142 |
+
# ==================== TEACHER FUNCTIONS ====================
|
| 1143 |
+
|
| 1144 |
+
def teacher_login(username: str, password: str) -> tuple:
|
| 1145 |
+
global CURRENT_TEACHER
|
| 1146 |
+
teacher_data = admin_service.authenticate(username, password)
|
| 1147 |
+
if teacher_data and teacher_data["role"] == "teacher":
|
| 1148 |
+
CURRENT_TEACHER = teacher_data
|
| 1149 |
+
return (
|
| 1150 |
+
gr.update(visible=False),
|
| 1151 |
+
gr.update(visible=True),
|
| 1152 |
+
gr.update(value=f"β
Welcome, {teacher_data['name']}!"),
|
| 1153 |
+
gr.update(visible=True)
|
| 1154 |
+
)
|
| 1155 |
+
elif teacher_data and teacher_data["role"] == "admin":
|
| 1156 |
+
CURRENT_TEACHER = teacher_data
|
| 1157 |
+
return (
|
| 1158 |
+
gr.update(visible=False),
|
| 1159 |
+
gr.update(visible=True),
|
| 1160 |
+
gr.update(value=f"β
Welcome, Admin {teacher_data['name']}!"),
|
| 1161 |
+
gr.update(visible=True)
|
| 1162 |
+
)
|
| 1163 |
+
return (
|
| 1164 |
+
gr.update(visible=True),
|
| 1165 |
+
gr.update(visible=False),
|
| 1166 |
+
gr.update(value="β Invalid credentials or not a teacher account"),
|
| 1167 |
+
gr.update(visible=False)
|
| 1168 |
+
)
|
| 1169 |
+
|
| 1170 |
+
def teacher_logout():
|
| 1171 |
+
global CURRENT_TEACHER
|
| 1172 |
+
CURRENT_TEACHER = None
|
| 1173 |
+
return (
|
| 1174 |
+
gr.update(visible=True),
|
| 1175 |
+
gr.update(visible=False),
|
| 1176 |
+
gr.update(),
|
| 1177 |
+
gr.update(visible=False)
|
| 1178 |
+
)
|
| 1179 |
+
|
| 1180 |
+
def create_assignment(title: str, description: str, due_date: str, course: str) -> str:
|
| 1181 |
+
if not CURRENT_TEACHER:
|
| 1182 |
+
return "π Please log in as teacher first."
|
| 1183 |
+
if not title or not due_date or not course:
|
| 1184 |
+
return "β οΈ Title, due date, and course are required."
|
| 1185 |
+
|
| 1186 |
+
assignment_id = school_service.create_assignment(
|
| 1187 |
+
title=title,
|
| 1188 |
+
description=description,
|
| 1189 |
+
due_date=due_date,
|
| 1190 |
+
course=course,
|
| 1191 |
+
assigned_by=CURRENT_TEACHER["name"]
|
| 1192 |
+
)
|
| 1193 |
+
|
| 1194 |
+
if assignment_id == -1:
|
| 1195 |
+
return "β Failed to create assignment. Please try again."
|
| 1196 |
+
|
| 1197 |
+
# Count recipients
|
| 1198 |
+
conn = db_manager.get_connection()
|
| 1199 |
+
cursor = conn.cursor()
|
| 1200 |
+
cursor.execute('''
|
| 1201 |
+
SELECT COUNT(*) FROM student_sessions
|
| 1202 |
+
WHERE groups LIKE ?
|
| 1203 |
+
''', (f'%"{course}"%',))
|
| 1204 |
+
recipients = cursor.fetchone()[0]
|
| 1205 |
+
conn.close()
|
| 1206 |
+
|
| 1207 |
+
return f"β
Assignment created! ID: {assignment_id}. Sent to {recipients} students in {course}."
|
| 1208 |
+
|
| 1209 |
+
# ==================== VOICE INPUT ====================
|
| 1210 |
+
|
| 1211 |
+
VOICE_JS = """
|
| 1212 |
+
async function startVoiceInput() {
|
| 1213 |
+
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
|
| 1214 |
+
alert('ποΈ Voice input is not supported in your browser. Try Chrome or Edge.');
|
| 1215 |
+
return '';
|
| 1216 |
+
}
|
| 1217 |
+
|
| 1218 |
+
const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
|
| 1219 |
+
recognition.lang = 'en-US';
|
| 1220 |
+
recognition.interimResults = false;
|
| 1221 |
+
recognition.maxAlternatives = 1;
|
| 1222 |
+
|
| 1223 |
+
return new Promise((resolve) => {
|
| 1224 |
+
recognition.start();
|
| 1225 |
+
recognition.onresult = (event) => {
|
| 1226 |
+
const transcript = event.results[0][0].transcript;
|
| 1227 |
+
resolve(transcript);
|
| 1228 |
+
};
|
| 1229 |
+
recognition.onerror = (event) => {
|
| 1230 |
+
alert('ποΈ Error: ' + event.error);
|
| 1231 |
+
resolve('');
|
| 1232 |
+
};
|
| 1233 |
+
});
|
| 1234 |
+
}
|
| 1235 |
+
"""
|
| 1236 |
+
|
| 1237 |
+
def voice_input_handler() -> str:
|
| 1238 |
+
return ""
|
| 1239 |
+
|
| 1240 |
+
# ==================== CUSTOM CSS ====================
|
| 1241 |
+
|
| 1242 |
+
CUSTOM_CSS = """
|
| 1243 |
+
.gradio-container {
|
| 1244 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 1245 |
+
max-width: 1200px;
|
| 1246 |
+
margin: 0 auto;
|
| 1247 |
+
padding: 16px;
|
| 1248 |
+
transition: background-color 0.3s, color 0.3s;
|
| 1249 |
+
}
|
| 1250 |
+
|
| 1251 |
+
.dark-mode {
|
| 1252 |
+
--background-fill-primary: #1e1e1e !important;
|
| 1253 |
+
--background-fill-secondary: #2d2d2d !important;
|
| 1254 |
+
--text-color: #f0f0f0 !important;
|
| 1255 |
+
--button-primary-background-fill: #5a5a5a !important;
|
| 1256 |
+
--button-secondary-background-fill: #3a3a3a !important;
|
| 1257 |
+
--input-background-fill: #2d2d2d !important;
|
| 1258 |
+
--input-border-color: #444 !important;
|
| 1259 |
+
}
|
| 1260 |
+
|
| 1261 |
+
.primary {
|
| 1262 |
+
background: linear-gradient(135deg, #6e8efb, #a777e3) !important;
|
| 1263 |
+
border: none !important;
|
| 1264 |
+
color: white !important;
|
| 1265 |
+
}
|
| 1266 |
+
|
| 1267 |
+
.chatbot-container {
|
| 1268 |
+
background: #f8f9fa !important;
|
| 1269 |
+
border-radius: 16px !important;
|
| 1270 |
+
}
|
| 1271 |
+
|
| 1272 |
+
.dark-mode .chatbot-container {
|
| 1273 |
+
background: #2d2d2d !important;
|
| 1274 |
+
}
|
| 1275 |
+
|
| 1276 |
+
.user, .bot {
|
| 1277 |
+
border-radius: 18px !important;
|
| 1278 |
+
padding: 12px 16px !important;
|
| 1279 |
+
}
|
| 1280 |
+
|
| 1281 |
+
.file-upload {
|
| 1282 |
+
border: 2px dashed #6e8efb !important;
|
| 1283 |
+
border-radius: 12px !important;
|
| 1284 |
+
background: #f8f9ff !important;
|
| 1285 |
+
}
|
| 1286 |
+
|
| 1287 |
+
.dark-mode .file-upload {
|
| 1288 |
+
background: #2a2a2a !important;
|
| 1289 |
+
border-color: #5a5a5a !important;
|
| 1290 |
+
}
|
| 1291 |
+
"""
|
| 1292 |
+
|
| 1293 |
+
# ==================== BUILD UI ====================
|
| 1294 |
+
|
| 1295 |
+
with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo")) as demo:
|
| 1296 |
+
with gr.Row():
|
| 1297 |
+
gr.Markdown("# π ThutoAI β Your AI School Assistant")
|
| 1298 |
+
dark_mode_btn = gr.Button("βοΈ Dark Mode", variant="secondary")
|
| 1299 |
+
|
| 1300 |
+
with gr.Group() as login_group:
|
| 1301 |
+
gr.Markdown("### π Student Login")
|
| 1302 |
+
with gr.Row():
|
| 1303 |
+
login_username = gr.Textbox(label="Username", scale=3)
|
| 1304 |
+
login_password = gr.Textbox(label="Password", type="password", scale=3)
|
| 1305 |
+
login_btn = gr.Button("π Login", variant="primary")
|
| 1306 |
+
register_btn = gr.Button("π Register", variant="secondary")
|
| 1307 |
+
login_status = gr.Textbox(label="Status", interactive=False)
|
| 1308 |
+
|
| 1309 |
+
with gr.Accordion("π Register New Account", open=False):
|
| 1310 |
+
reg_username = gr.Textbox(label="Username")
|
| 1311 |
+
reg_password = gr.Textbox(label="Password", type="password")
|
| 1312 |
+
reg_name = gr.Textbox(label="Full Name")
|
| 1313 |
+
reg_btn = gr.Button("Create Account")
|
| 1314 |
+
reg_status = gr.Textbox(label="Registration Status")
|
| 1315 |
+
|
| 1316 |
+
with gr.Group(visible=False) as main_app:
|
| 1317 |
+
with gr.Row():
|
| 1318 |
+
avatar_display = gr.HTML()
|
| 1319 |
+
user_display = gr.Textbox(label="Logged in as", interactive=False, visible=True)
|
| 1320 |
+
logout_btn = gr.Button("β¬
οΈ Logout", variant="secondary")
|
| 1321 |
+
|
| 1322 |
+
with gr.Tabs():
|
| 1323 |
+
with gr.Tab("π’ Announcements"):
|
| 1324 |
+
gr.Markdown("### Filter by Course or Subject")
|
| 1325 |
+
with gr.Row():
|
| 1326 |
+
course_filter = gr.Dropdown(
|
| 1327 |
+
choices=["All", "Mathematics", "Science", "English", "History", "General"],
|
| 1328 |
+
value="All",
|
| 1329 |
+
label="Select Course",
|
| 1330 |
+
scale=3
|
| 1331 |
+
)
|
| 1332 |
+
refresh_btn = gr.Button("π Refresh", variant="secondary", scale=1)
|
| 1333 |
+
announcements_html = gr.HTML()
|
| 1334 |
+
course_filter.change(fn=render_announcements, inputs=course_filter, outputs=announcements_html, api_name="filter_announcements")
|
| 1335 |
+
refresh_btn.click(fn=render_announcements, inputs=course_filter, outputs=announcements_html, api_name="refresh_announcements")
|
| 1336 |
+
demo.load(fn=render_announcements, inputs=course_filter, outputs=announcements_html, api_name="load_announcements")
|
| 1337 |
+
|
| 1338 |
+
with gr.Tab("π¬ Ask ThutoAI"):
|
| 1339 |
+
gr.Markdown("### π‘ Ask me anything β I'm here to help!")
|
| 1340 |
+
chatbot = gr.Chatbot(
|
| 1341 |
+
height=480,
|
| 1342 |
+
type='messages'
|
| 1343 |
+
)
|
| 1344 |
+
with gr.Row():
|
| 1345 |
+
msg = gr.Textbox(
|
| 1346 |
+
label="Type your question",
|
| 1347 |
+
placeholder="E.g., How do I prepare for the Math exam?",
|
| 1348 |
+
scale=7
|
| 1349 |
+
)
|
| 1350 |
+
voice_btn = gr.Button("ποΈ Speak", variant="secondary", scale=1)
|
| 1351 |
+
submit_btn = gr.Button("β€ Send", variant="primary", scale=1)
|
| 1352 |
+
clear_btn = gr.Button("ποΈ Clear Chat", variant="secondary")
|
| 1353 |
+
|
| 1354 |
+
def respond(message, chat_history):
|
| 1355 |
+
for updated_history, _ in ai_chat(message, chat_history, CURRENT_USER):
|
| 1356 |
+
yield updated_history, ""
|
| 1357 |
+
|
| 1358 |
+
msg.submit(respond, [msg, chatbot], [msg, chatbot])
|
| 1359 |
+
submit_btn.click(respond, [msg, chatbot], [msg, chatbot])
|
| 1360 |
+
clear_btn.click(lambda: None, None, chatbot)
|
| 1361 |
+
|
| 1362 |
+
voice_btn.click(
|
| 1363 |
+
fn=voice_input_handler,
|
| 1364 |
+
inputs=None,
|
| 1365 |
+
outputs=msg,
|
| 1366 |
+
js=VOICE_JS + "return startVoiceInput();"
|
| 1367 |
+
)
|
| 1368 |
+
|
| 1369 |
+
with gr.Tab("π
Assignments"):
|
| 1370 |
+
gr.Markdown("### π Your Upcoming Assignments & Exams")
|
| 1371 |
+
assignments_display = gr.HTML()
|
| 1372 |
+
demo.load(
|
| 1373 |
+
fn=lambda: render_assignments(student_service.get_assignments(CURRENT_USER)) if CURRENT_USER != "guest" else "",
|
| 1374 |
+
inputs=None,
|
| 1375 |
+
outputs=assignments_display
|
| 1376 |
+
)
|
| 1377 |
+
|
| 1378 |
+
with gr.Tab("π₯ Class Groups"):
|
| 1379 |
+
gr.Markdown("### π Join Your Class Groups")
|
| 1380 |
+
with gr.Row():
|
| 1381 |
+
group_code_input = gr.Textbox(label="Group Code (e.g., MATH10A)", scale=3)
|
| 1382 |
+
join_btn = gr.Button("β Join Group", variant="primary", scale=1)
|
| 1383 |
+
leave_btn = gr.Button("β Leave Group", variant="secondary", scale=1)
|
| 1384 |
+
group_status = gr.Textbox(label="Status")
|
| 1385 |
+
groups_display = gr.HTML()
|
| 1386 |
+
join_btn.click(
|
| 1387 |
+
fn=join_group,
|
| 1388 |
+
inputs=group_code_input,
|
| 1389 |
+
outputs=[group_status, groups_display]
|
| 1390 |
+
)
|
| 1391 |
+
leave_btn.click(
|
| 1392 |
+
fn=leave_group,
|
| 1393 |
+
inputs=group_code_input,
|
| 1394 |
+
outputs=[group_status, groups_display]
|
| 1395 |
+
)
|
| 1396 |
+
demo.load(
|
| 1397 |
+
fn=lambda: render_groups(student_service.get_groups(CURRENT_USER)) if CURRENT_USER != "guest" else "",
|
| 1398 |
+
inputs=None,
|
| 1399 |
+
outputs=groups_display
|
| 1400 |
+
)
|
| 1401 |
+
|
| 1402 |
+
with gr.Tab("π My Files"):
|
| 1403 |
+
gr.Markdown("### π Upload & Manage Study Materials")
|
| 1404 |
+
with gr.Row():
|
| 1405 |
+
file_input = gr.File(label="Drag & drop or click to upload", elem_classes=["file-upload"])
|
| 1406 |
+
upload_btn = gr.Button("π€ Upload", variant="primary")
|
| 1407 |
+
upload_status = gr.Textbox(label="Status")
|
| 1408 |
+
file_list = gr.JSON(label="Your Files")
|
| 1409 |
+
|
| 1410 |
+
upload_btn.click(
|
| 1411 |
+
fn=upload_file_for_student,
|
| 1412 |
+
inputs=file_input,
|
| 1413 |
+
outputs=upload_status
|
| 1414 |
+
)
|
| 1415 |
+
demo.load(
|
| 1416 |
+
fn=lambda: student_service.get_files(CURRENT_USER) if CURRENT_USER != "guest" else [],
|
| 1417 |
+
inputs=None,
|
| 1418 |
+
outputs=file_list
|
| 1419 |
+
)
|
| 1420 |
+
|
| 1421 |
+
with gr.Tab("πΌοΈ Profile"):
|
| 1422 |
+
gr.Markdown("### πΌοΈ Update Your Profile Picture")
|
| 1423 |
+
with gr.Row():
|
| 1424 |
+
avatar_input = gr.File(label="Choose an image (PNG, JPG)", file_types=["image"])
|
| 1425 |
+
upload_avatar_btn = gr.Button("π€ Upload Avatar", variant="primary")
|
| 1426 |
+
avatar_status = gr.Textbox(label="Status")
|
| 1427 |
+
upload_avatar_btn.click(
|
| 1428 |
+
fn=upload_avatar,
|
| 1429 |
+
inputs=avatar_input,
|
| 1430 |
+
outputs=avatar_status
|
| 1431 |
+
)
|
| 1432 |
+
demo.load(
|
| 1433 |
+
fn=lambda: get_avatar_html(
|
| 1434 |
+
None, # We don't handle real avatar paths yet
|
| 1435 |
+
student_service.authenticate_student(CURRENT_USER, "")["name"] if CURRENT_USER != "guest" else "Guest"
|
| 1436 |
+
) if CURRENT_USER != "guest" else "",
|
| 1437 |
+
inputs=None,
|
| 1438 |
+
outputs=avatar_display
|
| 1439 |
+
)
|
| 1440 |
+
|
| 1441 |
+
with gr.Tab("π©βπ« Teacher Panel"):
|
| 1442 |
+
gr.Markdown("### π Teacher Login (for assignment creation)")
|
| 1443 |
+
|
| 1444 |
+
with gr.Group() as teacher_login_group:
|
| 1445 |
+
teacher_username = gr.Textbox(label="Teacher Username")
|
| 1446 |
+
teacher_password = gr.Textbox(label="Password", type="password")
|
| 1447 |
+
teacher_login_btn = gr.Button("π Login", variant="primary")
|
| 1448 |
+
teacher_status = gr.Textbox(label="Status")
|
| 1449 |
+
|
| 1450 |
+
with gr.Group(visible=False) as teacher_dashboard:
|
| 1451 |
+
gr.Markdown("### βοΈ Create New Assignment")
|
| 1452 |
+
assignment_title = gr.Textbox(label="Assignment Title", placeholder="e.g., Chapter 5 Quiz")
|
| 1453 |
+
assignment_description = gr.Textbox(label="Description", placeholder="Instructions for students...", lines=2)
|
| 1454 |
+
assignment_due_date = gr.Textbox(label="Due Date (YYYY-MM-DD)", placeholder="2025-05-01")
|
| 1455 |
+
assignment_course = gr.Dropdown(
|
| 1456 |
+
choices=["MATH10A", "MATH10B", "SCI11A", "SCI11B", "ENG10A", "ENG10B", "HIST9A", "HIST9B"],
|
| 1457 |
+
label="Assign to Class Group",
|
| 1458 |
+
value="MATH10A"
|
| 1459 |
+
)
|
| 1460 |
+
create_assignment_btn = gr.Button("π¬ Create Assignment", variant="primary")
|
| 1461 |
+
assignment_result = gr.Textbox(label="Result")
|
| 1462 |
+
|
| 1463 |
+
assignment_priority = gr.Radio(
|
| 1464 |
+
["low", "normal", "high"],
|
| 1465 |
+
label="Priority",
|
| 1466 |
+
value="normal"
|
| 1467 |
+
)
|
| 1468 |
+
|
| 1469 |
+
teacher_logout_btn = gr.Button("β¬
οΈ Logout", variant="secondary")
|
| 1470 |
+
|
| 1471 |
+
teacher_login_btn.click(
|
| 1472 |
+
fn=teacher_login,
|
| 1473 |
+
inputs=[teacher_username, teacher_password],
|
| 1474 |
+
outputs=[teacher_login_group, teacher_dashboard, teacher_status, create_assignment_btn]
|
| 1475 |
+
)
|
| 1476 |
+
teacher_logout_btn.click(
|
| 1477 |
+
fn=teacher_logout,
|
| 1478 |
+
inputs=None,
|
| 1479 |
+
outputs=[teacher_login_group, teacher_dashboard, teacher_status, assignment_result]
|
| 1480 |
+
)
|
| 1481 |
+
create_assignment_btn.click(
|
| 1482 |
+
fn=create_assignment,
|
| 1483 |
+
inputs=[assignment_title, assignment_description, assignment_due_date, assignment_course],
|
| 1484 |
+
outputs=assignment_result
|
| 1485 |
+
)
|
| 1486 |
+
|
| 1487 |
+
with gr.Tab("π Admin Analytics"):
|
| 1488 |
+
gr.Markdown("### π School Analytics Dashboard")
|
| 1489 |
+
analytics_display = gr.HTML()
|
| 1490 |
+
refresh_analytics_btn = gr.Button("π Refresh Analytics")
|
| 1491 |
+
refresh_analytics_btn.click(
|
| 1492 |
+
fn=render_analytics,
|
| 1493 |
+
inputs=None,
|
| 1494 |
+
outputs=analytics_display
|
| 1495 |
+
)
|
| 1496 |
+
demo.load(
|
| 1497 |
+
fn=render_analytics,
|
| 1498 |
+
inputs=None,
|
| 1499 |
+
outputs=analytics_display
|
| 1500 |
+
)
|
| 1501 |
+
|
| 1502 |
+
# β
FIXED: Use gr.update() for ALL outputs
|
| 1503 |
+
login_btn.click(
|
| 1504 |
+
fn=login_student,
|
| 1505 |
+
inputs=[login_username, login_password],
|
| 1506 |
+
outputs=[
|
| 1507 |
+
gr.update(), # login_group
|
| 1508 |
+
gr.update(), # main_app
|
| 1509 |
+
gr.update(), # chatbot
|
| 1510 |
+
gr.update(), # files
|
| 1511 |
+
gr.update(), # assignments_display
|
| 1512 |
+
gr.update(), # groups_display
|
| 1513 |
+
gr.update(), # login_status
|
| 1514 |
+
gr.update(), # user_display
|
| 1515 |
+
gr.update(), # logout_btn
|
| 1516 |
+
gr.update(), # avatar_display
|
| 1517 |
+
gr.update(), # dark_mode_btn
|
| 1518 |
+
gr.update() # css placeholder
|
| 1519 |
+
]
|
| 1520 |
+
)
|
| 1521 |
+
|
| 1522 |
+
register_btn.click(
|
| 1523 |
+
fn=register_student,
|
| 1524 |
+
inputs=[reg_username, reg_password, reg_name],
|
| 1525 |
+
outputs=gr.update()
|
| 1526 |
+
)
|
| 1527 |
+
|
| 1528 |
+
dark_mode_btn.click(
|
| 1529 |
+
fn=toggle_dark_mode,
|
| 1530 |
+
inputs=None,
|
| 1531 |
+
outputs=[dark_mode_btn, gr.update()]
|
| 1532 |
+
)
|
| 1533 |
+
|
| 1534 |
+
if __name__ == "__main__":
|
| 1535 |
+
demo.launch()
|