ngwakomadikwe commited on
Commit
ac93f69
Β·
verified Β·
1 Parent(s): 038a0f1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +893 -1
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 user
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()