Spaces:
Runtime error
Runtime error
Upload 28 files
Browse files- .env +2 -0
- Admin-Dashboard.html +1547 -0
- Dockerfile +16 -0
- __init__.py +0 -0
- backend.code-workspace +10 -0
- ch.png +0 -0
- chat.html +1360 -0
- chatbot.py +30 -0
- chunk_text.py +75 -0
- clean_text.py +57 -0
- co.py +49 -0
- embedding.py +210 -0
- forgot-password.html +310 -0
- index.html +364 -0
- index_lectures.py +7 -0
- ingest.py +7 -0
- login.html +471 -0
- main.py +1395 -0
- process_pdf.py +255 -0
- rag.py +157 -0
- register.html +568 -0
- requierments.txt +28 -0
- requirements.txt +18 -0
- reset-password.html +417 -0
- search.py +52 -0
- start.sh +14 -0
- university_chatbot.db +0 -0
- verify-email.html +216 -0
.env
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MAILTRAP_USER = "universityai.com@gmail.com"
|
| 2 |
+
MAILTRAP_PASSWORD = "megg neiq boli dhzt"
|
Admin-Dashboard.html
ADDED
|
@@ -0,0 +1,1547 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" dir="ltr">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Admin Dashboard - University AI</title>
|
| 7 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.6/purify.min.js"></script>
|
| 8 |
+
<style>
|
| 9 |
+
* {
|
| 10 |
+
margin: 0;
|
| 11 |
+
padding: 0;
|
| 12 |
+
box-sizing: border-box;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
:root {
|
| 16 |
+
--olive-light: #3A662A;
|
| 17 |
+
--olive-dark: #5C6E4A;
|
| 18 |
+
--bg-light: #F5F5F5;
|
| 19 |
+
--bg-dark: #1A1A1A;
|
| 20 |
+
--text-light: #2C2C2C;
|
| 21 |
+
--text-dark: #F5F5F5;
|
| 22 |
+
--card-light: #FFFFFF;
|
| 23 |
+
--card-dark: #2D2D2D;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
body {
|
| 27 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 28 |
+
transition: all 0.3s ease;
|
| 29 |
+
min-height: 100vh;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
body.light-mode {
|
| 33 |
+
background: var(--bg-light);
|
| 34 |
+
color: var(--text-light);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
body.dark-mode {
|
| 38 |
+
background: var(--bg-dark);
|
| 39 |
+
color: var(--text-dark);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/* Sidebar */
|
| 43 |
+
.sidebar {
|
| 44 |
+
position: fixed;
|
| 45 |
+
left: 0;
|
| 46 |
+
top: 0;
|
| 47 |
+
width: 250px;
|
| 48 |
+
height: 100vh;
|
| 49 |
+
padding: 20px;
|
| 50 |
+
transition: all 0.3s ease;
|
| 51 |
+
z-index: 100;
|
| 52 |
+
display: flex;
|
| 53 |
+
flex-direction: column;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.light-mode .sidebar {
|
| 57 |
+
background: var(--card-light);
|
| 58 |
+
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.dark-mode .sidebar {
|
| 62 |
+
background: var(--card-dark);
|
| 63 |
+
box-shadow: 2px 0 10px rgba(0,0,0,0.3);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.logo {
|
| 67 |
+
font-size: 24px;
|
| 68 |
+
font-weight: bold;
|
| 69 |
+
color: var(--olive-light);
|
| 70 |
+
margin-bottom: 40px;
|
| 71 |
+
text-align: center;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.menu-item {
|
| 75 |
+
padding: 15px 20px;
|
| 76 |
+
margin-bottom: 10px;
|
| 77 |
+
border-radius: 8px;
|
| 78 |
+
cursor: pointer;
|
| 79 |
+
transition: all 0.3s ease;
|
| 80 |
+
display: flex;
|
| 81 |
+
align-items: center;
|
| 82 |
+
gap: 10px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.menu-item:hover {
|
| 86 |
+
background: var(--olive-light);
|
| 87 |
+
color: white;
|
| 88 |
+
transform: translateX(5px);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.menu-item.active {
|
| 92 |
+
background: var(--olive-light);
|
| 93 |
+
color: white;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.reset-btn {
|
| 97 |
+
padding: 12px;
|
| 98 |
+
background: var(--olive-dark);
|
| 99 |
+
color: white;
|
| 100 |
+
border: none;
|
| 101 |
+
border-radius: 8px;
|
| 102 |
+
cursor: pointer;
|
| 103 |
+
transition: all 0.3s ease;
|
| 104 |
+
margin-bottom: 10px;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.reset-btn:hover {
|
| 108 |
+
background: var(--olive-light);
|
| 109 |
+
transform: translateY(-2px);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.logout-btn {
|
| 113 |
+
margin-top: auto;
|
| 114 |
+
padding: 12px;
|
| 115 |
+
background: #c33;
|
| 116 |
+
color: white;
|
| 117 |
+
border: none;
|
| 118 |
+
border-radius: 8px;
|
| 119 |
+
cursor: pointer;
|
| 120 |
+
transition: all 0.3s ease;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.logout-btn:hover {
|
| 124 |
+
background: #a22;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/* Main Content */
|
| 128 |
+
.main-content {
|
| 129 |
+
margin-left: 250px;
|
| 130 |
+
padding: 30px;
|
| 131 |
+
min-height: 100vh;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.header-bar {
|
| 135 |
+
display: flex;
|
| 136 |
+
justify-content: space-between;
|
| 137 |
+
align-items: center;
|
| 138 |
+
margin-bottom: 30px;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.header-bar h1 {
|
| 142 |
+
font-size: 32px;
|
| 143 |
+
color: var(--olive-light);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.theme-toggle {
|
| 147 |
+
width: 50px;
|
| 148 |
+
height: 26px;
|
| 149 |
+
background: var(--olive-light);
|
| 150 |
+
border-radius: 13px;
|
| 151 |
+
position: relative;
|
| 152 |
+
cursor: pointer;
|
| 153 |
+
transition: all 0.3s ease;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.theme-toggle::after {
|
| 157 |
+
content: '☀️';
|
| 158 |
+
position: absolute;
|
| 159 |
+
top: 3px;
|
| 160 |
+
left: 3px;
|
| 161 |
+
width: 20px;
|
| 162 |
+
height: 20px;
|
| 163 |
+
background: white;
|
| 164 |
+
border-radius: 50%;
|
| 165 |
+
transition: all 0.3s ease;
|
| 166 |
+
display: flex;
|
| 167 |
+
align-items: center;
|
| 168 |
+
justify-content: center;
|
| 169 |
+
font-size: 12px;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.dark-mode .theme-toggle::after {
|
| 173 |
+
content: '🌙';
|
| 174 |
+
left: 27px;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
/* Stats Cards */
|
| 178 |
+
.stats-grid {
|
| 179 |
+
display: grid;
|
| 180 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 181 |
+
gap: 20px;
|
| 182 |
+
margin-bottom: 40px;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.stat-card {
|
| 186 |
+
padding: 25px;
|
| 187 |
+
border-radius: 12px;
|
| 188 |
+
transition: all 0.3s ease;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.light-mode .stat-card {
|
| 192 |
+
background: var(--card-light);
|
| 193 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.dark-mode .stat-card {
|
| 197 |
+
background: var(--card-dark);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.stat-card:hover {
|
| 201 |
+
transform: translateY(-5px);
|
| 202 |
+
box-shadow: 0 5px 20px rgba(58, 102, 42, 0.3);
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.stat-icon {
|
| 206 |
+
font-size: 40px;
|
| 207 |
+
margin-bottom: 15px;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.stat-value {
|
| 211 |
+
font-size: 36px;
|
| 212 |
+
font-weight: bold;
|
| 213 |
+
color: var(--olive-light);
|
| 214 |
+
margin-bottom: 5px;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.stat-label {
|
| 218 |
+
font-size: 14px;
|
| 219 |
+
opacity: 0.7;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
/* Section */
|
| 223 |
+
.section {
|
| 224 |
+
margin-bottom: 40px;
|
| 225 |
+
padding: 25px;
|
| 226 |
+
border-radius: 12px;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.light-mode .section {
|
| 230 |
+
background: var(--card-light);
|
| 231 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.dark-mode .section {
|
| 235 |
+
background: var(--card-dark);
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.section-header {
|
| 239 |
+
display: flex;
|
| 240 |
+
justify-content: space-between;
|
| 241 |
+
align-items: center;
|
| 242 |
+
margin-bottom: 20px;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
.section-title {
|
| 246 |
+
font-size: 24px;
|
| 247 |
+
color: var(--olive-light);
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.btn {
|
| 251 |
+
padding: 10px 20px;
|
| 252 |
+
border: none;
|
| 253 |
+
border-radius: 8px;
|
| 254 |
+
cursor: pointer;
|
| 255 |
+
font-size: 14px;
|
| 256 |
+
transition: all 0.3s ease;
|
| 257 |
+
background: var(--olive-light);
|
| 258 |
+
color: white;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.btn:hover {
|
| 262 |
+
transform: translateY(-2px);
|
| 263 |
+
box-shadow: 0 5px 15px rgba(58, 102, 42, 0.4);
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
/* Upload Form */
|
| 267 |
+
.upload-form {
|
| 268 |
+
display: flex;
|
| 269 |
+
gap: 15px;
|
| 270 |
+
margin-bottom: 20px;
|
| 271 |
+
flex-wrap: wrap;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
input[type="text"],
|
| 275 |
+
input[type="file"] {
|
| 276 |
+
padding: 12px;
|
| 277 |
+
border-radius: 8px;
|
| 278 |
+
border: 2px solid transparent;
|
| 279 |
+
font-size: 14px;
|
| 280 |
+
transition: all 0.3s ease;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.light-mode input[type="text"],
|
| 284 |
+
.light-mode input[type="file"] {
|
| 285 |
+
background: var(--bg-light);
|
| 286 |
+
color: var(--text-light);
|
| 287 |
+
border-color: #e0e0e0;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.dark-mode input[type="text"],
|
| 291 |
+
.dark-mode input[type="file"] {
|
| 292 |
+
background: var(--bg-dark);
|
| 293 |
+
color: var(--text-dark);
|
| 294 |
+
border-color: #444;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
input[type="text"]:focus {
|
| 298 |
+
outline: none;
|
| 299 |
+
border-color: var(--olive-light);
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
/* Table */
|
| 303 |
+
table {
|
| 304 |
+
width: 100%;
|
| 305 |
+
border-collapse: collapse;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
th, td {
|
| 309 |
+
padding: 15px;
|
| 310 |
+
text-align: left;
|
| 311 |
+
border-bottom: 1px solid;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
.light-mode th,
|
| 315 |
+
.light-mode td {
|
| 316 |
+
border-color: #e0e0e0;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.dark-mode th,
|
| 320 |
+
.dark-mode td {
|
| 321 |
+
border-color: #444;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
th {
|
| 325 |
+
font-weight: 600;
|
| 326 |
+
color: var(--olive-light);
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.badge {
|
| 330 |
+
padding: 5px 10px;
|
| 331 |
+
border-radius: 5px;
|
| 332 |
+
font-size: 12px;
|
| 333 |
+
font-weight: 600;
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
.badge.admin {
|
| 337 |
+
background: #f90;
|
| 338 |
+
color: white;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.badge.student {
|
| 342 |
+
background: var(--olive-light);
|
| 343 |
+
color: white;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.badge.positive {
|
| 347 |
+
background: #10b981;
|
| 348 |
+
color: white;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
.badge.negative {
|
| 352 |
+
background: #ef4444;
|
| 353 |
+
color: white;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
/* Feedback Card */
|
| 357 |
+
.feedback-card {
|
| 358 |
+
padding: 20px;
|
| 359 |
+
margin-bottom: 15px;
|
| 360 |
+
border-radius: 10px;
|
| 361 |
+
transition: all 0.3s ease;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.light-mode .feedback-card {
|
| 365 |
+
background: var(--bg-light);
|
| 366 |
+
border: 1px solid #e0e0e0;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
.dark-mode .feedback-card {
|
| 370 |
+
background: var(--bg-dark);
|
| 371 |
+
border: 1px solid #444;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.feedback-card:hover {
|
| 375 |
+
transform: translateY(-2px);
|
| 376 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.feedback-header {
|
| 380 |
+
display: flex;
|
| 381 |
+
justify-content: space-between;
|
| 382 |
+
align-items: center;
|
| 383 |
+
margin-bottom: 10px;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.feedback-user {
|
| 387 |
+
font-weight: 600;
|
| 388 |
+
color: var(--olive-light);
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.feedback-date {
|
| 392 |
+
font-size: 12px;
|
| 393 |
+
opacity: 0.7;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
.feedback-message {
|
| 397 |
+
margin: 10px 0;
|
| 398 |
+
padding: 10px;
|
| 399 |
+
border-radius: 5px;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.light-mode .feedback-message {
|
| 403 |
+
background: white;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.dark-mode .feedback-message {
|
| 407 |
+
background: var(--card-dark);
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
.feedback-type {
|
| 411 |
+
display: inline-flex;
|
| 412 |
+
align-items: center;
|
| 413 |
+
gap: 5px;
|
| 414 |
+
font-size: 18px;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
.feedback-stats {
|
| 418 |
+
display: grid;
|
| 419 |
+
grid-template-columns: repeat(3, 1fr);
|
| 420 |
+
gap: 15px;
|
| 421 |
+
margin-bottom: 20px;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.feedback-stat-box {
|
| 425 |
+
padding: 15px;
|
| 426 |
+
border-radius: 8px;
|
| 427 |
+
text-align: center;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
.light-mode .feedback-stat-box {
|
| 431 |
+
background: var(--bg-light);
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
.dark-mode .feedback-stat-box {
|
| 435 |
+
background: var(--bg-dark);
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
.feedback-stat-value {
|
| 439 |
+
font-size: 24px;
|
| 440 |
+
font-weight: bold;
|
| 441 |
+
margin-bottom: 5px;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
.feedback-stat-label {
|
| 445 |
+
font-size: 12px;
|
| 446 |
+
opacity: 0.7;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.alert {
|
| 450 |
+
padding: 15px;
|
| 451 |
+
border-radius: 8px;
|
| 452 |
+
margin-bottom: 20px;
|
| 453 |
+
display: none;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
.alert.show {
|
| 457 |
+
display: block;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
.alert.success {
|
| 461 |
+
background: #d1fae5;
|
| 462 |
+
color: #065f46;
|
| 463 |
+
border: 1px solid #10b981;
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
.alert.error {
|
| 467 |
+
background: #fee2e2;
|
| 468 |
+
color: #991b1b;
|
| 469 |
+
border: 1px solid #ef4444;
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
.alert.info {
|
| 473 |
+
background: #dbeafe;
|
| 474 |
+
color: #1e40af;
|
| 475 |
+
border: 1px solid #3b82f6;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
/* Course Card Styles */
|
| 479 |
+
.course-card {
|
| 480 |
+
cursor: pointer;
|
| 481 |
+
border-left: 4px solid var(--olive-light);
|
| 482 |
+
position: relative;
|
| 483 |
+
}
|
| 484 |
+
.course-card:hover {
|
| 485 |
+
transform: translateY(-3px);
|
| 486 |
+
box-shadow: 0 8px 25px rgba(58, 102, 42, 0.2);
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
/* Mobile Menu & Overlay */
|
| 490 |
+
.menu-toggle {
|
| 491 |
+
display: none;
|
| 492 |
+
position: fixed;
|
| 493 |
+
top: 20px;
|
| 494 |
+
left: 20px;
|
| 495 |
+
z-index: 1001;
|
| 496 |
+
background: var(--olive-light);
|
| 497 |
+
color: white;
|
| 498 |
+
border: none;
|
| 499 |
+
padding: 10px;
|
| 500 |
+
border-radius: 8px;
|
| 501 |
+
cursor: pointer;
|
| 502 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
| 503 |
+
font-size: 20px;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
.sidebar-overlay {
|
| 507 |
+
display: none;
|
| 508 |
+
position: fixed;
|
| 509 |
+
top: 0;
|
| 510 |
+
left: 0;
|
| 511 |
+
width: 100%;
|
| 512 |
+
height: 100%;
|
| 513 |
+
background: rgba(0,0,0,0.5);
|
| 514 |
+
z-index: 99;
|
| 515 |
+
opacity: 0;
|
| 516 |
+
transition: opacity 0.3s;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
@media (max-width: 768px) {
|
| 520 |
+
.sidebar {
|
| 521 |
+
transform: translateX(-100%);
|
| 522 |
+
width: 200px;
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
.sidebar.active {
|
| 526 |
+
transform: translateX(0);
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
.sidebar-overlay.active {
|
| 530 |
+
display: block;
|
| 531 |
+
opacity: 1;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
.menu-toggle {
|
| 535 |
+
display: block;
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
.main-content {
|
| 539 |
+
margin-left: 0;
|
| 540 |
+
padding-top: 70px; /* Space for toggle button */
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
.stats-grid {
|
| 544 |
+
grid-template-columns: 1fr;
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
.feedback-stats {
|
| 548 |
+
grid-template-columns: 1fr;
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
table {
|
| 552 |
+
font-size: 12px;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
th, td {
|
| 556 |
+
padding: 10px;
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
/* Make tables scrollable on mobile */
|
| 560 |
+
#usersTable, #lecturesTable, #feedbacksContainer {
|
| 561 |
+
overflow-x: auto;
|
| 562 |
+
-webkit-overflow-scrolling: touch;
|
| 563 |
+
}
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
/* Modal Styles */
|
| 567 |
+
.modal {
|
| 568 |
+
display: none;
|
| 569 |
+
position: fixed;
|
| 570 |
+
z-index: 1000;
|
| 571 |
+
left: 0;
|
| 572 |
+
top: 0;
|
| 573 |
+
width: 100%;
|
| 574 |
+
height: 100%;
|
| 575 |
+
background-color: rgba(0,0,0,0.5);
|
| 576 |
+
align-items: center;
|
| 577 |
+
justify-content: center;
|
| 578 |
+
}
|
| 579 |
+
.modal-content {
|
| 580 |
+
padding: 25px;
|
| 581 |
+
border-radius: 12px;
|
| 582 |
+
width: 90%;
|
| 583 |
+
max-width: 400px;
|
| 584 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
|
| 585 |
+
}
|
| 586 |
+
.light-mode .modal-content { background: var(--card-light); }
|
| 587 |
+
.dark-mode .modal-content { background: var(--card-dark); }
|
| 588 |
+
|
| 589 |
+
.delete-course-btn {
|
| 590 |
+
position: absolute;
|
| 591 |
+
top: 10px;
|
| 592 |
+
right: 10px;
|
| 593 |
+
background: #ef4444;
|
| 594 |
+
color: white;
|
| 595 |
+
border: none;
|
| 596 |
+
padding: 5px 10px;
|
| 597 |
+
border-radius: 5px;
|
| 598 |
+
cursor: pointer;
|
| 599 |
+
z-index: 10;
|
| 600 |
+
opacity: 0.7;
|
| 601 |
+
transition: 0.2s;
|
| 602 |
+
}
|
| 603 |
+
.delete-course-btn:hover { opacity: 1; transform: scale(1.1); }
|
| 604 |
+
|
| 605 |
+
@keyframes fadeInItem { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
|
| 606 |
+
.new-item-fade-in {
|
| 607 |
+
animation: fadeInItem 0.4s ease-out;
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
/* Loading Overlay */
|
| 611 |
+
.loading-overlay {
|
| 612 |
+
display: none;
|
| 613 |
+
position: fixed;
|
| 614 |
+
top: 0;
|
| 615 |
+
left: 0;
|
| 616 |
+
width: 100%;
|
| 617 |
+
height: 100%;
|
| 618 |
+
background: rgba(255, 255, 255, 0.8);
|
| 619 |
+
z-index: 2000;
|
| 620 |
+
justify-content: center;
|
| 621 |
+
align-items: center;
|
| 622 |
+
flex-direction: column;
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
.dark-mode .loading-overlay {
|
| 626 |
+
background: rgba(0, 0, 0, 0.8);
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
.spinner {
|
| 630 |
+
width: 50px;
|
| 631 |
+
height: 50px;
|
| 632 |
+
border: 5px solid rgba(58, 102, 42, 0.3);
|
| 633 |
+
border-radius: 50%;
|
| 634 |
+
border-top-color: var(--olive-light);
|
| 635 |
+
animation: spin 1s ease-in-out infinite;
|
| 636 |
+
margin-bottom: 15px;
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
@keyframes spin {
|
| 640 |
+
to { transform: rotate(360deg); }
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
.loading-text {
|
| 644 |
+
font-weight: 600;
|
| 645 |
+
color: var(--olive-light);
|
| 646 |
+
}
|
| 647 |
+
</style>
|
| 648 |
+
</head>
|
| 649 |
+
<body class="light-mode">
|
| 650 |
+
<!-- Mobile Menu Toggle -->
|
| 651 |
+
<button class="menu-toggle" onclick="toggleSidebar()">☰</button>
|
| 652 |
+
|
| 653 |
+
<!-- Sidebar Overlay -->
|
| 654 |
+
<div class="sidebar-overlay" onclick="toggleSidebar()"></div>
|
| 655 |
+
|
| 656 |
+
<!-- Loading Overlay -->
|
| 657 |
+
<div id="loadingOverlay" class="loading-overlay">
|
| 658 |
+
<div class="spinner"></div>
|
| 659 |
+
<div class="loading-text">Loading...</div>
|
| 660 |
+
</div>
|
| 661 |
+
|
| 662 |
+
<!-- Sidebar -->
|
| 663 |
+
<div class="sidebar" id="sidebar">
|
| 664 |
+
<div class="logo">🎓 University AI</div>
|
| 665 |
+
|
| 666 |
+
<div class="menu-item active" data-section="dashboard">
|
| 667 |
+
<span>📊</span> Dashboard
|
| 668 |
+
</div>
|
| 669 |
+
<div class="menu-item" data-section="users">
|
| 670 |
+
<span>👥</span> Users
|
| 671 |
+
</div>
|
| 672 |
+
<div class="menu-item" data-section="lectures">
|
| 673 |
+
<span>📚</span> Courses
|
| 674 |
+
</div>
|
| 675 |
+
<!--<div class="menu-item" data-section="feedbacks">
|
| 676 |
+
<span>💬</span> Feedbacks
|
| 677 |
+
</div>-->
|
| 678 |
+
|
| 679 |
+
<button class="logout-btn" onclick="logout()">
|
| 680 |
+
🚪 Logout
|
| 681 |
+
</button>
|
| 682 |
+
</div>
|
| 683 |
+
|
| 684 |
+
<!-- Main Content -->
|
| 685 |
+
<div class="main-content">
|
| 686 |
+
<div class="header-bar">
|
| 687 |
+
<h1>Admin Dashboard 👨💼</h1>
|
| 688 |
+
<div class="theme-toggle" onclick="toggleTheme()"></div>
|
| 689 |
+
</div>
|
| 690 |
+
|
| 691 |
+
<div id="alert" class="alert"></div>
|
| 692 |
+
|
| 693 |
+
<!-- Dashboard Section -->
|
| 694 |
+
<div id="dashboardSection">
|
| 695 |
+
<div class="stats-grid">
|
| 696 |
+
<div class="stat-card">
|
| 697 |
+
<div class="stat-icon">👥</div>
|
| 698 |
+
<div class="stat-value" id="totalStudents">-</div>
|
| 699 |
+
<div class="stat-label">Total Students</div>
|
| 700 |
+
</div>
|
| 701 |
+
<div class="stat-card">
|
| 702 |
+
<div class="stat-icon">💬</div>
|
| 703 |
+
<div class="stat-value" id="totalConversations">-</div>
|
| 704 |
+
<div class="stat-label">Conversations</div>
|
| 705 |
+
</div>
|
| 706 |
+
<div class="stat-card">
|
| 707 |
+
<div class="stat-icon">📚</div>
|
| 708 |
+
<div class="stat-value" id="totalCourses">-</div>
|
| 709 |
+
<div class="stat-label">Courses</div>
|
| 710 |
+
</div>
|
| 711 |
+
<!--<div class="stat-card">
|
| 712 |
+
<div class="stat-icon">⭐</div>
|
| 713 |
+
<div class="stat-value" id="totalFeedbacks">-</div>
|
| 714 |
+
<div class="stat-label">Feedbacks</div>
|
| 715 |
+
</div>-->
|
| 716 |
+
</div>
|
| 717 |
+
</div>
|
| 718 |
+
|
| 719 |
+
<!-- Users Section -->
|
| 720 |
+
<div id="usersSection" class="section" style="display: none;">
|
| 721 |
+
<div class="section-header">
|
| 722 |
+
<h2 class="section-title">Users Management</h2>
|
| 723 |
+
</div>
|
| 724 |
+
|
| 725 |
+
<div class="upload-form" style="margin-bottom: 20px;">
|
| 726 |
+
<input
|
| 727 |
+
type="text"
|
| 728 |
+
id="userSearchInput"
|
| 729 |
+
placeholder="🔍 Search users by email..."
|
| 730 |
+
style="width: 100%;"
|
| 731 |
+
onkeyup="filterUsers()"
|
| 732 |
+
>
|
| 733 |
+
</div>
|
| 734 |
+
|
| 735 |
+
<div id="usersTable"></div>
|
| 736 |
+
</div>
|
| 737 |
+
|
| 738 |
+
<!-- Lectures Section -->
|
| 739 |
+
<div id="lecturesSection" class="section" style="display: none;">
|
| 740 |
+
<div class="section-header">
|
| 741 |
+
<h2 class="section-title" id="lecturesTitle">Course Management</h2>
|
| 742 |
+
<button id="backToCoursesBtn" class="btn" style="display: none;" onclick="showCoursesGrid()">
|
| 743 |
+
← Back to Courses
|
| 744 |
+
</button>
|
| 745 |
+
</div>
|
| 746 |
+
|
| 747 |
+
<!-- Tier 1: Course Grid -->
|
| 748 |
+
<div id="coursesGridView">
|
| 749 |
+
<div class="upload-form" style="justify-content: flex-end; margin-bottom: 20px;">
|
| 750 |
+
<button class="btn" onclick="createNewCourse()">
|
| 751 |
+
+ Create New Course
|
| 752 |
+
</button>
|
| 753 |
+
</div>
|
| 754 |
+
<div id="coursesGrid" class="stats-grid"></div>
|
| 755 |
+
</div>
|
| 756 |
+
|
| 757 |
+
<!-- Tier 2: Detail View -->
|
| 758 |
+
<div id="courseDetailView" style="display: none;">
|
| 759 |
+
<div class="upload-form">
|
| 760 |
+
<input type="file" id="lectureFile" accept=".pdf">
|
| 761 |
+
<input type="text" id="lectureSubject" placeholder="Subject" style="flex: 1;" disabled>
|
| 762 |
+
<button class="btn" onclick="uploadLecture()">
|
| 763 |
+
📤 Upload to Context
|
| 764 |
+
</button>
|
| 765 |
+
</div>
|
| 766 |
+
<!-- Progress Bar -->
|
| 767 |
+
<div id="uploadProgressContainer" style="display: none; margin-bottom: 20px;">
|
| 768 |
+
<div style="width: 100%; background-color: #e0e0e0; border-radius: 8px; overflow: hidden;">
|
| 769 |
+
<div id="uploadProgressBar" style="width: 0%; height: 20px; background-color: var(--olive-light); text-align: center; color: white; line-height: 20px; font-size: 12px; transition: width 0.3s ease;">0%</div>
|
| 770 |
+
</div>
|
| 771 |
+
<div id="uploadStatusText" style="font-size: 12px; margin-top: 5px; text-align: center; opacity: 0.8;">Starting upload...</div>
|
| 772 |
+
</div>
|
| 773 |
+
<div id="lecturesTable"></div>
|
| 774 |
+
|
| 775 |
+
<!-- Delete Course Action (Low Visibility) -->
|
| 776 |
+
<div style="margin-top: 20px; text-align: right; border-top: 1px solid #eee; padding-top: 15px;">
|
| 777 |
+
<button class="btn" style="background: #ef4444; font-size: 12px; opacity: 0.8;" onclick="initiateDeleteCourse()">
|
| 778 |
+
🗑️ Delete Course
|
| 779 |
+
</button>
|
| 780 |
+
</div>
|
| 781 |
+
</div>
|
| 782 |
+
</div>
|
| 783 |
+
|
| 784 |
+
<!-- Feedbacks Section -->
|
| 785 |
+
<div id="feedbacksSection" class="section" style="display: none;">
|
| 786 |
+
<div class="section-header">
|
| 787 |
+
<h2 class="section-title">Student Feedbacks</h2>
|
| 788 |
+
</div>
|
| 789 |
+
|
| 790 |
+
<!-- Feedback Statistics -->
|
| 791 |
+
<div class="feedback-stats">
|
| 792 |
+
<div class="feedback-stat-box">
|
| 793 |
+
<div class="feedback-stat-value" style="color: #10b981;" id="positiveFeedbacks">0</div>
|
| 794 |
+
<div class="feedback-stat-label">👍 Positive</div>
|
| 795 |
+
</div>
|
| 796 |
+
<div class="feedback-stat-box">
|
| 797 |
+
<div class="feedback-stat-value" style="color: #ef4444;" id="negativeFeedbacks">0</div>
|
| 798 |
+
<div class="feedback-stat-label">👎 Negative</div>
|
| 799 |
+
</div>
|
| 800 |
+
<div class="feedback-stat-box">
|
| 801 |
+
<div class="feedback-stat-value" style="color: var(--olive-light);" id="totalFeedbacksCount">0</div>
|
| 802 |
+
<div class="feedback-stat-label">📊 Total</div>
|
| 803 |
+
</div>
|
| 804 |
+
</div>
|
| 805 |
+
|
| 806 |
+
<div id="feedbacksContainer"></div>
|
| 807 |
+
</div>
|
| 808 |
+
</div>
|
| 809 |
+
|
| 810 |
+
<!-- Create Course Modal -->
|
| 811 |
+
<div id="addCourseModal" class="modal">
|
| 812 |
+
<div class="modal-content">
|
| 813 |
+
<h3 style="margin-bottom: 15px; color: var(--olive-light);">Create New Course</h3>
|
| 814 |
+
<input type="text" id="newCourseName" placeholder="Enter course name..." style="width: 100%; margin-bottom: 20px;">
|
| 815 |
+
<div style="display: flex; justify-content: flex-end; gap: 10px;">
|
| 816 |
+
<button class="btn" style="background: #666;" onclick="closeModal()">Cancel</button>
|
| 817 |
+
<button class="btn" onclick="submitNewCourse()">Create</button>
|
| 818 |
+
</div>
|
| 819 |
+
</div>
|
| 820 |
+
</div>
|
| 821 |
+
|
| 822 |
+
<!-- Delete Course Confirmation Modal -->
|
| 823 |
+
<div id="deleteCourseModal" class="modal">
|
| 824 |
+
<div class="modal-content">
|
| 825 |
+
<h3 style="margin-bottom: 15px; color: #ef4444;">Delete Course</h3>
|
| 826 |
+
<p style="margin-bottom: 20px;">Are you sure you want to delete this course? This cannot be undone.</p>
|
| 827 |
+
<div style="display: flex; justify-content: flex-end; gap: 10px;">
|
| 828 |
+
<button class="btn" style="background: #666;" onclick="closeDeleteCourseModal()">Cancel</button>
|
| 829 |
+
<button class="btn" style="background: #ef4444;" onclick="confirmDeleteCourse()">Delete</button>
|
| 830 |
+
</div>
|
| 831 |
+
</div>
|
| 832 |
+
</div>
|
| 833 |
+
|
| 834 |
+
<!-- Delete Confirmation Modal -->
|
| 835 |
+
<div id="deleteConfirmModal" class="modal">
|
| 836 |
+
<div class="modal-content">
|
| 837 |
+
<h3 style="margin-bottom: 15px; color: #ef4444;">Confirm Deletion</h3>
|
| 838 |
+
<p style="margin-bottom: 20px;">Are you sure you want to delete this lecture? This action cannot be undone.</p>
|
| 839 |
+
<div style="display: flex; justify-content: flex-end; gap: 10px;">
|
| 840 |
+
<button class="btn" style="background: #666;" onclick="closeDeleteModal()">Cancel</button>
|
| 841 |
+
<button class="btn" style="background: #ef4444;" onclick="confirmDeleteLecture()">Delete</button>
|
| 842 |
+
</div>
|
| 843 |
+
</div>
|
| 844 |
+
</div>
|
| 845 |
+
|
| 846 |
+
<script>
|
| 847 |
+
const API_URL = ''; // Empty string uses current origin (relative path)
|
| 848 |
+
|
| 849 |
+
// Theme Toggle
|
| 850 |
+
function toggleTheme() {
|
| 851 |
+
const body = document.body;
|
| 852 |
+
if (body.classList.contains('light-mode')) {
|
| 853 |
+
body.classList.remove('light-mode');
|
| 854 |
+
body.classList.add('dark-mode');
|
| 855 |
+
localStorage.setItem('theme', 'dark');
|
| 856 |
+
} else {
|
| 857 |
+
body.classList.remove('dark-mode');
|
| 858 |
+
body.classList.add('light-mode');
|
| 859 |
+
localStorage.setItem('theme', 'light');
|
| 860 |
+
}
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
// Toggle Sidebar for Mobile
|
| 864 |
+
function toggleSidebar() {
|
| 865 |
+
document.getElementById('sidebar').classList.toggle('active');
|
| 866 |
+
const overlay = document.querySelector('.sidebar-overlay');
|
| 867 |
+
overlay.classList.toggle('active');
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
// Loading Spinner Functions
|
| 871 |
+
function showLoading(message = 'Loading...') {
|
| 872 |
+
document.querySelector('.loading-text').textContent = message;
|
| 873 |
+
document.getElementById('loadingOverlay').style.display = 'flex';
|
| 874 |
+
}
|
| 875 |
+
|
| 876 |
+
function hideLoading() {
|
| 877 |
+
document.getElementById('loadingOverlay').style.display = 'none';
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
// Load theme and initialize
|
| 881 |
+
document.addEventListener("DOMContentLoaded", async () => {
|
| 882 |
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
| 883 |
+
document.body.classList.remove("light-mode", "dark-mode");
|
| 884 |
+
document.body.classList.add(savedTheme + "-mode");
|
| 885 |
+
|
| 886 |
+
await checkAuth();
|
| 887 |
+
await loadStats();
|
| 888 |
+
|
| 889 |
+
// Menu item click handlers
|
| 890 |
+
document.querySelectorAll('.menu-item').forEach(item => {
|
| 891 |
+
item.addEventListener('click', function() {
|
| 892 |
+
showSection(this.getAttribute('data-section'));
|
| 893 |
+
// Close sidebar on mobile when item clicked
|
| 894 |
+
if (window.innerWidth <= 768) {
|
| 895 |
+
toggleSidebar();
|
| 896 |
+
}
|
| 897 |
+
});
|
| 898 |
+
});
|
| 899 |
+
});
|
| 900 |
+
|
| 901 |
+
// Check authentication
|
| 902 |
+
async function checkAuth() {
|
| 903 |
+
const token = localStorage.getItem('token');
|
| 904 |
+
const role = localStorage.getItem('role');
|
| 905 |
+
|
| 906 |
+
if (!token || role !== 'admin') {
|
| 907 |
+
window.location.href = 'login.html';
|
| 908 |
+
return;
|
| 909 |
+
}
|
| 910 |
+
|
| 911 |
+
try {
|
| 912 |
+
const response = await fetch(`${API_URL}/user/me`, {
|
| 913 |
+
headers: { 'Authorization': `Bearer ${token}` }
|
| 914 |
+
});
|
| 915 |
+
|
| 916 |
+
if (!response.ok) {
|
| 917 |
+
localStorage.clear();
|
| 918 |
+
window.location.href = 'login.html';
|
| 919 |
+
return;
|
| 920 |
+
}
|
| 921 |
+
|
| 922 |
+
const userData = await response.json();
|
| 923 |
+
if (userData.role !== 'admin') {
|
| 924 |
+
localStorage.clear();
|
| 925 |
+
window.location.href = 'login.html';
|
| 926 |
+
}
|
| 927 |
+
} catch (error) {
|
| 928 |
+
console.error('Auth check failed:', error);
|
| 929 |
+
localStorage.clear();
|
| 930 |
+
window.location.href = 'login.html';
|
| 931 |
+
}
|
| 932 |
+
}
|
| 933 |
+
|
| 934 |
+
// Logout
|
| 935 |
+
function logout() {
|
| 936 |
+
localStorage.clear();
|
| 937 |
+
window.location.href = 'login.html';
|
| 938 |
+
}
|
| 939 |
+
|
| 940 |
+
// Show alert
|
| 941 |
+
function showAlert(message, type = 'success') {
|
| 942 |
+
const alert = document.getElementById('alert');
|
| 943 |
+
alert.textContent = message;
|
| 944 |
+
alert.className = `alert ${type} show`;
|
| 945 |
+
|
| 946 |
+
setTimeout(() => {
|
| 947 |
+
alert.classList.remove('show');
|
| 948 |
+
}, 5000);
|
| 949 |
+
}
|
| 950 |
+
|
| 951 |
+
// Show section
|
| 952 |
+
function showSection(section) {
|
| 953 |
+
document.querySelectorAll('.menu-item').forEach(item => {
|
| 954 |
+
item.classList.remove('active');
|
| 955 |
+
});
|
| 956 |
+
document.querySelector(`.menu-item[data-section="${section}"]`).classList.add('active');
|
| 957 |
+
|
| 958 |
+
document.getElementById('dashboardSection').style.display = 'none';
|
| 959 |
+
document.getElementById('usersSection').style.display = 'none';
|
| 960 |
+
document.getElementById('lecturesSection').style.display = 'none';
|
| 961 |
+
document.getElementById('feedbacksSection').style.display = 'none';
|
| 962 |
+
|
| 963 |
+
if (section === 'dashboard') {
|
| 964 |
+
document.getElementById('dashboardSection').style.display = 'block';
|
| 965 |
+
loadStats();
|
| 966 |
+
} else if (section === 'users') {
|
| 967 |
+
document.getElementById('usersSection').style.display = 'block';
|
| 968 |
+
loadUsers();
|
| 969 |
+
} else if (section === 'lectures') {
|
| 970 |
+
document.getElementById('lecturesSection').style.display = 'block';
|
| 971 |
+
showCoursesGrid();
|
| 972 |
+
} else if (section === 'feedbacks') {
|
| 973 |
+
document.getElementById('feedbacksSection').style.display = 'block';
|
| 974 |
+
loadFeedbacks();
|
| 975 |
+
}
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
// Load statistics
|
| 979 |
+
async function loadStats() {
|
| 980 |
+
const token = localStorage.getItem('token');
|
| 981 |
+
showLoading('Updating Stats...');
|
| 982 |
+
|
| 983 |
+
try {
|
| 984 |
+
const response = await fetch(`${API_URL}/admin/get-stats`, {
|
| 985 |
+
headers: { 'Authorization': `Bearer ${token}` }
|
| 986 |
+
});
|
| 987 |
+
|
| 988 |
+
if (response.ok) {
|
| 989 |
+
const data = await response.json();
|
| 990 |
+
const stats = data.stats;
|
| 991 |
+
|
| 992 |
+
document.getElementById('totalStudents').textContent = stats.users?.total_students || 0;
|
| 993 |
+
document.getElementById('totalConversations').textContent = stats.activity?.total_conversations || 0;
|
| 994 |
+
document.getElementById('totalCourses').textContent = stats.courses?.total || 0;
|
| 995 |
+
document.getElementById('totalFeedbacks').textContent = stats.feedback?.total || 0;
|
| 996 |
+
} else if (response.status === 401) {
|
| 997 |
+
logout();
|
| 998 |
+
}
|
| 999 |
+
} catch (error) {
|
| 1000 |
+
console.error('Error loading stats:', error);
|
| 1001 |
+
}
|
| 1002 |
+
hideLoading();
|
| 1003 |
+
}
|
| 1004 |
+
|
| 1005 |
+
let allUsers = [];
|
| 1006 |
+
let lectureIdToDelete = null;
|
| 1007 |
+
let courseIdToDelete = null;
|
| 1008 |
+
let currentSelectedCourse = null;
|
| 1009 |
+
let currentSelectedCourseId = null;
|
| 1010 |
+
|
| 1011 |
+
// Load users
|
| 1012 |
+
async function loadUsers() {
|
| 1013 |
+
const token = localStorage.getItem('token');
|
| 1014 |
+
showLoading('Loading Users...');
|
| 1015 |
+
|
| 1016 |
+
try {
|
| 1017 |
+
const response = await fetch(`${API_URL}/admin/get-users`, {
|
| 1018 |
+
headers: { 'Authorization': `Bearer ${token}` }
|
| 1019 |
+
});
|
| 1020 |
+
|
| 1021 |
+
if (response.ok) {
|
| 1022 |
+
const data = await response.json();
|
| 1023 |
+
allUsers = data.users;
|
| 1024 |
+
displayUsers(allUsers);
|
| 1025 |
+
}
|
| 1026 |
+
} catch (error) {
|
| 1027 |
+
console.error('Error loading users:', error);
|
| 1028 |
+
}
|
| 1029 |
+
hideLoading();
|
| 1030 |
+
}
|
| 1031 |
+
|
| 1032 |
+
// Filter users
|
| 1033 |
+
function filterUsers() {
|
| 1034 |
+
const query = document.getElementById('userSearchInput').value.toLowerCase();
|
| 1035 |
+
const filtered = allUsers.filter(user =>
|
| 1036 |
+
user.email.toLowerCase().includes(query)
|
| 1037 |
+
);
|
| 1038 |
+
displayUsers(filtered);
|
| 1039 |
+
}
|
| 1040 |
+
|
| 1041 |
+
// Display users
|
| 1042 |
+
function displayUsers(users) {
|
| 1043 |
+
const container = document.getElementById('usersTable');
|
| 1044 |
+
|
| 1045 |
+
if (!users || users.length === 0) {
|
| 1046 |
+
container.innerHTML = '<p style="text-align: center; opacity: 0.5;">No users found</p>';
|
| 1047 |
+
return;
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
container.innerHTML = `
|
| 1051 |
+
<table>
|
| 1052 |
+
<thead>
|
| 1053 |
+
<tr>
|
| 1054 |
+
<th>ID</th>
|
| 1055 |
+
<th>Email</th>
|
| 1056 |
+
<th>Role</th>
|
| 1057 |
+
<th>Registration Date</th>
|
| 1058 |
+
</tr>
|
| 1059 |
+
</thead>
|
| 1060 |
+
<tbody>
|
| 1061 |
+
${users.map(user => `
|
| 1062 |
+
<tr>
|
| 1063 |
+
<td>${user.id}</td>
|
| 1064 |
+
<td>${DOMPurify.sanitize(user.email)}</td>
|
| 1065 |
+
<td><span class="badge ${user.role}">${user.role === 'admin' ? 'Admin' : 'Student'}</span></td>
|
| 1066 |
+
<td>${new Date(user.created_at).toLocaleDateString('en-US')}</td>
|
| 1067 |
+
</tr>
|
| 1068 |
+
`).join('')}
|
| 1069 |
+
</tbody>
|
| 1070 |
+
</table>
|
| 1071 |
+
`;
|
| 1072 |
+
}
|
| 1073 |
+
|
| 1074 |
+
// Upload lecture
|
| 1075 |
+
function uploadLecture() {
|
| 1076 |
+
const fileInput = document.getElementById('lectureFile');
|
| 1077 |
+
const subjectInput = document.getElementById('lectureSubject');
|
| 1078 |
+
const token = localStorage.getItem('token');
|
| 1079 |
+
|
| 1080 |
+
if (!fileInput.files[0]) {
|
| 1081 |
+
showAlert('⚠️ Please select a PDF file', 'error');
|
| 1082 |
+
return;
|
| 1083 |
+
}
|
| 1084 |
+
|
| 1085 |
+
const subject = subjectInput.value.trim();
|
| 1086 |
+
if (!subject) {
|
| 1087 |
+
showAlert('⚠️ Please enter subject name', 'error');
|
| 1088 |
+
return;
|
| 1089 |
+
}
|
| 1090 |
+
|
| 1091 |
+
const formData = new FormData();
|
| 1092 |
+
formData.append('file', fileInput.files[0]);
|
| 1093 |
+
formData.append('subject', subject);
|
| 1094 |
+
|
| 1095 |
+
// UI Elements
|
| 1096 |
+
const progressContainer = document.getElementById('uploadProgressContainer');
|
| 1097 |
+
const progressBar = document.getElementById('uploadProgressBar');
|
| 1098 |
+
const statusText = document.getElementById('uploadStatusText');
|
| 1099 |
+
const uploadBtn = document.querySelector('#courseDetailView .upload-form button');
|
| 1100 |
+
|
| 1101 |
+
// Reset UI
|
| 1102 |
+
progressContainer.style.display = 'block';
|
| 1103 |
+
progressBar.style.width = '0%';
|
| 1104 |
+
progressBar.textContent = '0%';
|
| 1105 |
+
statusText.textContent = 'Starting upload...';
|
| 1106 |
+
uploadBtn.disabled = true;
|
| 1107 |
+
uploadBtn.style.opacity = '0.6';
|
| 1108 |
+
|
| 1109 |
+
const xhr = new XMLHttpRequest();
|
| 1110 |
+
xhr.open('POST', `${API_URL}/admin/upload-lecture`, true);
|
| 1111 |
+
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
|
| 1112 |
+
|
| 1113 |
+
// Track upload progress
|
| 1114 |
+
xhr.upload.onprogress = function(e) {
|
| 1115 |
+
if (e.lengthComputable) {
|
| 1116 |
+
const percentComplete = Math.round((e.loaded / e.total) * 100);
|
| 1117 |
+
progressBar.style.width = percentComplete + '%';
|
| 1118 |
+
progressBar.textContent = percentComplete + '%';
|
| 1119 |
+
|
| 1120 |
+
if (percentComplete < 100) {
|
| 1121 |
+
statusText.textContent = `Uploading: ${percentComplete}%`;
|
| 1122 |
+
} else {
|
| 1123 |
+
statusText.textContent = 'Processing on server... This may take a while.';
|
| 1124 |
+
}
|
| 1125 |
+
}
|
| 1126 |
+
};
|
| 1127 |
+
|
| 1128 |
+
xhr.onload = async function() {
|
| 1129 |
+
uploadBtn.disabled = false;
|
| 1130 |
+
uploadBtn.style.opacity = '1';
|
| 1131 |
+
progressContainer.style.display = 'none';
|
| 1132 |
+
|
| 1133 |
+
if (xhr.status >= 200 && xhr.status < 300) {
|
| 1134 |
+
const data = JSON.parse(xhr.responseText);
|
| 1135 |
+
showAlert('✅ Lecture uploaded and processed successfully!', 'success');
|
| 1136 |
+
fileInput.value = '';
|
| 1137 |
+
await openCourse(currentSelectedCourse, currentSelectedCourseId); // Refresh current view
|
| 1138 |
+
await loadStats();
|
| 1139 |
+
} else {
|
| 1140 |
+
let errorMessage = 'Upload failed';
|
| 1141 |
+
try {
|
| 1142 |
+
const data = JSON.parse(xhr.responseText);
|
| 1143 |
+
errorMessage = data.detail || errorMessage;
|
| 1144 |
+
} catch (e) {}
|
| 1145 |
+
showAlert(`❌ ${errorMessage}`, 'error');
|
| 1146 |
+
}
|
| 1147 |
+
};
|
| 1148 |
+
|
| 1149 |
+
xhr.onerror = function() {
|
| 1150 |
+
uploadBtn.disabled = false;
|
| 1151 |
+
uploadBtn.style.opacity = '1';
|
| 1152 |
+
progressContainer.style.display = 'none';
|
| 1153 |
+
console.error('Error uploading lecture');
|
| 1154 |
+
showAlert('❌ Server connection error', 'error');
|
| 1155 |
+
};
|
| 1156 |
+
|
| 1157 |
+
xhr.send(formData);
|
| 1158 |
+
}
|
| 1159 |
+
|
| 1160 |
+
// Load courses for the main grid view
|
| 1161 |
+
async function loadCourses() {
|
| 1162 |
+
showLoading('Loading Courses...');
|
| 1163 |
+
const token = localStorage.getItem('token');
|
| 1164 |
+
try {
|
| 1165 |
+
const response = await fetch(`${API_URL}/admin/get-courses`, {
|
| 1166 |
+
headers: { 'Authorization': `Bearer ${token}` }
|
| 1167 |
+
});
|
| 1168 |
+
if (response.ok) {
|
| 1169 |
+
const data = await response.json();
|
| 1170 |
+
renderCoursesGrid(data.courses);
|
| 1171 |
+
} else {
|
| 1172 |
+
showAlert('❌ Failed to load courses.', 'error');
|
| 1173 |
+
}
|
| 1174 |
+
} catch (error) {
|
| 1175 |
+
console.error('Error loading courses:', error);
|
| 1176 |
+
showAlert('❌ Network error loading courses.', 'error');
|
| 1177 |
+
}
|
| 1178 |
+
hideLoading();
|
| 1179 |
+
}
|
| 1180 |
+
|
| 1181 |
+
// Render Tier 1: Courses Grid
|
| 1182 |
+
function renderCoursesGrid(courses) {
|
| 1183 |
+
const grid = document.getElementById('coursesGrid');
|
| 1184 |
+
|
| 1185 |
+
if (!courses || courses.length === 0) {
|
| 1186 |
+
grid.innerHTML = '<p style="grid-column: 1/-1; text-align: center; opacity: 0.5;">No courses found. Create one!</p>';
|
| 1187 |
+
return;
|
| 1188 |
+
}
|
| 1189 |
+
|
| 1190 |
+
grid.innerHTML = courses.map(course => `
|
| 1191 |
+
<div class="stat-card course-card" id="course-card-${course.id}" onclick="openCourse('${course.name}', ${course.id})">
|
| 1192 |
+
<div class="stat-icon">📚</div>
|
| 1193 |
+
<div class="stat-value" style="font-size: 24px;">${DOMPurify.sanitize(course.name)}</div>
|
| 1194 |
+
<div class="stat-label">${course.lecture_count} Lectures</div>
|
| 1195 |
+
<div style="margin-top: 10px; font-size: 12px; opacity: 0.6;">
|
| 1196 |
+
Created: ${new Date(course.created_at).toLocaleDateString()}
|
| 1197 |
+
</div>
|
| 1198 |
+
</div>
|
| 1199 |
+
`).join('');
|
| 1200 |
+
}
|
| 1201 |
+
|
| 1202 |
+
// Create New Course
|
| 1203 |
+
function createNewCourse() {
|
| 1204 |
+
document.getElementById('addCourseModal').style.display = 'flex';
|
| 1205 |
+
document.getElementById('newCourseName').focus();
|
| 1206 |
+
}
|
| 1207 |
+
|
| 1208 |
+
function closeModal() {
|
| 1209 |
+
document.getElementById('addCourseModal').style.display = 'none';
|
| 1210 |
+
document.getElementById('newCourseName').value = '';
|
| 1211 |
+
}
|
| 1212 |
+
|
| 1213 |
+
async function submitNewCourse() {
|
| 1214 |
+
const name = document.getElementById('newCourseName').value;
|
| 1215 |
+
if (!name || !name.trim()) {
|
| 1216 |
+
showAlert('⚠️ Course name cannot be empty.', 'error');
|
| 1217 |
+
return;
|
| 1218 |
+
}
|
| 1219 |
+
|
| 1220 |
+
showLoading('Creating Course...');
|
| 1221 |
+
const token = localStorage.getItem('token');
|
| 1222 |
+
try {
|
| 1223 |
+
const response = await fetch(`${API_URL}/admin/create-course`, {
|
| 1224 |
+
method: 'POST',
|
| 1225 |
+
headers: {
|
| 1226 |
+
'Content-Type': 'application/json',
|
| 1227 |
+
'Authorization': `Bearer ${token}`
|
| 1228 |
+
},
|
| 1229 |
+
body: JSON.stringify({ name: name.trim() })
|
| 1230 |
+
});
|
| 1231 |
+
|
| 1232 |
+
const data = await response.json();
|
| 1233 |
+
|
| 1234 |
+
if (response.ok) {
|
| 1235 |
+
showAlert('✅ Course created successfully!', 'success');
|
| 1236 |
+
closeModal();
|
| 1237 |
+
|
| 1238 |
+
// Silent Injection (Optimistic UI)
|
| 1239 |
+
const grid = document.getElementById('coursesGrid');
|
| 1240 |
+
if (grid.innerHTML.includes('No courses found')) grid.innerHTML = '';
|
| 1241 |
+
|
| 1242 |
+
const newCourseHtml = `
|
| 1243 |
+
<div class="stat-card course-card new-item-fade-in" id="course-card-${data.course_id}" onclick="openCourse('${data.name}', ${data.course_id})">
|
| 1244 |
+
<div class="stat-icon">📚</div>
|
| 1245 |
+
<div class="stat-value" style="font-size: 24px;">${data.name}</div>
|
| 1246 |
+
<div class="stat-label">0 Lectures</div>
|
| 1247 |
+
<div style="margin-top: 10px; font-size: 12px; opacity: 0.6;">
|
| 1248 |
+
Created: ${new Date().toLocaleDateString()}
|
| 1249 |
+
</div>
|
| 1250 |
+
</div>
|
| 1251 |
+
`;
|
| 1252 |
+
grid.insertAdjacentHTML('afterbegin', newCourseHtml);
|
| 1253 |
+
|
| 1254 |
+
// Update stats counter
|
| 1255 |
+
const countEl = document.getElementById('totalCourses');
|
| 1256 |
+
countEl.textContent = (parseInt(countEl.textContent) || 0) + 1;
|
| 1257 |
+
} else {
|
| 1258 |
+
showAlert(`❌ ${data.detail || 'Failed to create course'}`, 'error');
|
| 1259 |
+
}
|
| 1260 |
+
} catch (error) {
|
| 1261 |
+
console.error('Error creating course:', error);
|
| 1262 |
+
showAlert('❌ Server connection error', 'error');
|
| 1263 |
+
}
|
| 1264 |
+
hideLoading();
|
| 1265 |
+
}
|
| 1266 |
+
|
| 1267 |
+
// Delete Course Functions
|
| 1268 |
+
function initiateDeleteCourse() {
|
| 1269 |
+
courseIdToDelete = currentSelectedCourseId;
|
| 1270 |
+
document.getElementById('deleteCourseModal').style.display = 'flex';
|
| 1271 |
+
}
|
| 1272 |
+
|
| 1273 |
+
function closeDeleteCourseModal() {
|
| 1274 |
+
document.getElementById('deleteCourseModal').style.display = 'none';
|
| 1275 |
+
courseIdToDelete = null;
|
| 1276 |
+
}
|
| 1277 |
+
|
| 1278 |
+
async function confirmDeleteCourse() {
|
| 1279 |
+
if (!courseIdToDelete) return;
|
| 1280 |
+
showLoading('Deleting Course...');
|
| 1281 |
+
const token = localStorage.getItem('token');
|
| 1282 |
+
try {
|
| 1283 |
+
const response = await fetch(`${API_URL}/admin/delete-course/${courseIdToDelete}`, {
|
| 1284 |
+
method: 'DELETE',
|
| 1285 |
+
headers: { 'Authorization': `Bearer ${token}` }
|
| 1286 |
+
});
|
| 1287 |
+
|
| 1288 |
+
if (response.ok) {
|
| 1289 |
+
showAlert('✅ Course deleted successfully');
|
| 1290 |
+
|
| 1291 |
+
// Silent Removal Logic
|
| 1292 |
+
const card = document.getElementById(`course-card-${courseIdToDelete}`);
|
| 1293 |
+
if (card) card.remove();
|
| 1294 |
+
|
| 1295 |
+
// Update stats counter
|
| 1296 |
+
const countEl = document.getElementById('totalCourses');
|
| 1297 |
+
countEl.textContent = Math.max(0, (parseInt(countEl.textContent) || 0) - 1);
|
| 1298 |
+
|
| 1299 |
+
// Check empty state
|
| 1300 |
+
const grid = document.getElementById('coursesGrid');
|
| 1301 |
+
if (grid.children.length === 0) {
|
| 1302 |
+
grid.innerHTML = '<p style="grid-column: 1/-1; text-align: center; opacity: 0.5;">No courses found. Create one!</p>';
|
| 1303 |
+
}
|
| 1304 |
+
} else {
|
| 1305 |
+
const data = await response.json();
|
| 1306 |
+
showAlert(`❌ ${data.detail || 'Failed to delete course'}`, 'error');
|
| 1307 |
+
}
|
| 1308 |
+
} catch (error) {
|
| 1309 |
+
console.error('Error deleting course:', error);
|
| 1310 |
+
showAlert('❌ Server connection error', 'error');
|
| 1311 |
+
}
|
| 1312 |
+
closeDeleteCourseModal();
|
| 1313 |
+
hideLoading();
|
| 1314 |
+
}
|
| 1315 |
+
|
| 1316 |
+
// Open Tier 2: Detail View
|
| 1317 |
+
async function openCourse(courseName, courseId) {
|
| 1318 |
+
currentSelectedCourse = courseName;
|
| 1319 |
+
currentSelectedCourseId = courseId;
|
| 1320 |
+
|
| 1321 |
+
// Update UI State
|
| 1322 |
+
document.getElementById('coursesGridView').style.display = 'none';
|
| 1323 |
+
document.getElementById('backToCoursesBtn').style.display = 'block';
|
| 1324 |
+
document.getElementById('lecturesTitle').textContent = `Course: ${courseName}`;
|
| 1325 |
+
|
| 1326 |
+
// Contextual Automation
|
| 1327 |
+
document.getElementById('lectureSubject').value = courseName;
|
| 1328 |
+
|
| 1329 |
+
showLoading(`Loading lectures for ${courseName}...`);
|
| 1330 |
+
// Fetch and display lectures for this course
|
| 1331 |
+
const token = localStorage.getItem('token');
|
| 1332 |
+
try {
|
| 1333 |
+
const response = await fetch(`${API_URL}/admin/course/${courseName}/lectures`, {
|
| 1334 |
+
headers: { 'Authorization': `Bearer ${token}` }
|
| 1335 |
+
});
|
| 1336 |
+
if (response.ok) {
|
| 1337 |
+
const data = await response.json();
|
| 1338 |
+
document.getElementById('courseDetailView').style.display = 'block';
|
| 1339 |
+
displayLectures(data.lectures);
|
| 1340 |
+
} else {
|
| 1341 |
+
showAlert(`❌ Failed to load lectures for ${courseName}.`, 'error');
|
| 1342 |
+
displayLectures([]); // Show empty state
|
| 1343 |
+
}
|
| 1344 |
+
} catch (error) {
|
| 1345 |
+
console.error('Error loading course lectures:', error);
|
| 1346 |
+
showAlert('❌ Network error loading lectures.', 'error');
|
| 1347 |
+
}
|
| 1348 |
+
hideLoading();
|
| 1349 |
+
}
|
| 1350 |
+
|
| 1351 |
+
// Back to Grid
|
| 1352 |
+
function showCoursesGrid() {
|
| 1353 |
+
currentSelectedCourse = null;
|
| 1354 |
+
currentSelectedCourseId = null;
|
| 1355 |
+
document.getElementById('coursesGridView').style.display = 'block';
|
| 1356 |
+
document.getElementById('courseDetailView').style.display = 'none';
|
| 1357 |
+
document.getElementById('backToCoursesBtn').style.display = 'none';
|
| 1358 |
+
document.getElementById('lecturesTitle').textContent = 'Course Management';
|
| 1359 |
+
loadCourses();
|
| 1360 |
+
}
|
| 1361 |
+
|
| 1362 |
+
// Display lectures
|
| 1363 |
+
function displayLectures(lectures) {
|
| 1364 |
+
const container = document.getElementById('lecturesTable');
|
| 1365 |
+
|
| 1366 |
+
if (!lectures || lectures.length === 0) {
|
| 1367 |
+
container.innerHTML = '<p style="text-align: center; opacity: 0.5; margin-top: 20px;">No lectures in this course yet. Upload one above!</p>';
|
| 1368 |
+
return;
|
| 1369 |
+
}
|
| 1370 |
+
|
| 1371 |
+
container.innerHTML = `
|
| 1372 |
+
<table>
|
| 1373 |
+
<thead>
|
| 1374 |
+
<tr>
|
| 1375 |
+
<th>ID</th>
|
| 1376 |
+
<th>Filename</th>
|
| 1377 |
+
<th>Status</th>
|
| 1378 |
+
<th>Upload Date</th>
|
| 1379 |
+
<th>Actions</th>
|
| 1380 |
+
</tr>
|
| 1381 |
+
</thead>
|
| 1382 |
+
<tbody>
|
| 1383 |
+
${lectures.map(lecture => `
|
| 1384 |
+
<tr>
|
| 1385 |
+
<td>${lecture.id}</td>
|
| 1386 |
+
<td>${DOMPurify.sanitize(lecture.filename)}</td>
|
| 1387 |
+
<td><span class="badge ${getStatusClass(lecture.processing_status)}">${getStatusText(lecture.processing_status)}</span></td>
|
| 1388 |
+
<td>${new Date(lecture.uploaded_at).toLocaleDateString('en-US')}</td>
|
| 1389 |
+
<td>
|
| 1390 |
+
<button class="btn" style="background: #ef4444; padding: 5px 10px; font-size: 12px;" onclick="deleteLecture(${lecture.id})">Delete</button>
|
| 1391 |
+
</td>
|
| 1392 |
+
</tr>
|
| 1393 |
+
`).join('')}
|
| 1394 |
+
</tbody>
|
| 1395 |
+
</table>
|
| 1396 |
+
`;
|
| 1397 |
+
}
|
| 1398 |
+
|
| 1399 |
+
// Open Delete Modal
|
| 1400 |
+
function deleteLecture(id) {
|
| 1401 |
+
lectureIdToDelete = id;
|
| 1402 |
+
document.getElementById('deleteConfirmModal').style.display = 'flex';
|
| 1403 |
+
}
|
| 1404 |
+
|
| 1405 |
+
function closeDeleteModal() {
|
| 1406 |
+
document.getElementById('deleteConfirmModal').style.display = 'none';
|
| 1407 |
+
lectureIdToDelete = null;
|
| 1408 |
+
}
|
| 1409 |
+
|
| 1410 |
+
async function confirmDeleteLecture() {
|
| 1411 |
+
if (!lectureIdToDelete) return;
|
| 1412 |
+
showLoading('Deleting Lecture...');
|
| 1413 |
+
const id = lectureIdToDelete;
|
| 1414 |
+
const token = localStorage.getItem('token');
|
| 1415 |
+
try {
|
| 1416 |
+
const response = await fetch(`${API_URL}/admin/delete-lecture/${id}`, {
|
| 1417 |
+
method: 'DELETE',
|
| 1418 |
+
headers: { 'Authorization': `Bearer ${token}` }
|
| 1419 |
+
});
|
| 1420 |
+
|
| 1421 |
+
if (response.ok) {
|
| 1422 |
+
showAlert('✅ Lecture deleted successfully');
|
| 1423 |
+
openCourse(currentSelectedCourse, currentSelectedCourseId); // Refresh the current course view
|
| 1424 |
+
loadStats(); // Also refresh stats
|
| 1425 |
+
} else {
|
| 1426 |
+
showAlert('❌ Failed to delete lecture', 'error');
|
| 1427 |
+
}
|
| 1428 |
+
} catch (error) {
|
| 1429 |
+
console.error('Error deleting lecture:', error);
|
| 1430 |
+
showAlert('❌ Server connection error', 'error');
|
| 1431 |
+
}
|
| 1432 |
+
closeDeleteModal();
|
| 1433 |
+
hideLoading();
|
| 1434 |
+
}
|
| 1435 |
+
|
| 1436 |
+
// Get status text
|
| 1437 |
+
function getStatusText(status) {
|
| 1438 |
+
const statusMap = {
|
| 1439 |
+
'completed': 'Completed',
|
| 1440 |
+
'processing': 'Processing',
|
| 1441 |
+
'failed': 'Failed',
|
| 1442 |
+
'pending': 'Pending'
|
| 1443 |
+
};
|
| 1444 |
+
return statusMap[status] || status;
|
| 1445 |
+
}
|
| 1446 |
+
|
| 1447 |
+
function getStatusClass(status) {
|
| 1448 |
+
if (status === 'completed') return 'positive';
|
| 1449 |
+
if (status === 'failed') return 'negative';
|
| 1450 |
+
return 'admin'; // Orange/Yellow for pending/processing
|
| 1451 |
+
}
|
| 1452 |
+
|
| 1453 |
+
// Load ALL feedbacks
|
| 1454 |
+
async function loadFeedbacks() {
|
| 1455 |
+
const token = localStorage.getItem('token');
|
| 1456 |
+
showLoading('Loading Feedbacks...');
|
| 1457 |
+
|
| 1458 |
+
try {
|
| 1459 |
+
const response = await fetch(`${API_URL}/admin/get-feedbacks`, {
|
| 1460 |
+
headers: { 'Authorization': `Bearer ${token}` }
|
| 1461 |
+
});
|
| 1462 |
+
|
| 1463 |
+
if (response.ok) {
|
| 1464 |
+
const data = await response.json();
|
| 1465 |
+
|
| 1466 |
+
// Handle if backend returns a list directly OR { feedbacks: [] }
|
| 1467 |
+
let items = [];
|
| 1468 |
+
if (Array.isArray(data)) {
|
| 1469 |
+
items = data;
|
| 1470 |
+
} else if (data.feedbacks) {
|
| 1471 |
+
items = data.feedbacks;
|
| 1472 |
+
}
|
| 1473 |
+
|
| 1474 |
+
displayFeedbacks(items);
|
| 1475 |
+
} else {
|
| 1476 |
+
console.error("Feedback API Error:", response.status);
|
| 1477 |
+
showAlert(`❌ Failed to load feedbacks (Status: ${response.status})`, 'error');
|
| 1478 |
+
}
|
| 1479 |
+
} catch (error) {
|
| 1480 |
+
console.error('Error loading feedbacks:', error);
|
| 1481 |
+
showAlert('❌ Network error loading feedbacks', 'error');
|
| 1482 |
+
}
|
| 1483 |
+
hideLoading();
|
| 1484 |
+
}
|
| 1485 |
+
|
| 1486 |
+
// Display ALL feedbacks
|
| 1487 |
+
function displayFeedbacks(feedbacks) {
|
| 1488 |
+
const container = document.getElementById('feedbacksContainer');
|
| 1489 |
+
|
| 1490 |
+
if (!feedbacks || feedbacks.length === 0) {
|
| 1491 |
+
container.innerHTML = '<p style="text-align: center; opacity: 0.5;">No feedbacks yet</p>';
|
| 1492 |
+
|
| 1493 |
+
// Reset stats
|
| 1494 |
+
document.getElementById('positiveFeedbacks').textContent = '0';
|
| 1495 |
+
document.getElementById('negativeFeedbacks').textContent = '0';
|
| 1496 |
+
document.getElementById('totalFeedbacksCount').textContent = '0';
|
| 1497 |
+
return;
|
| 1498 |
+
}
|
| 1499 |
+
|
| 1500 |
+
// Calculate statistics
|
| 1501 |
+
const positive = feedbacks.filter(f => (f.feedback_type || '').toLowerCase() === 'positive').length;
|
| 1502 |
+
const negative = feedbacks.filter(f => (f.feedback_type || '').toLowerCase() === 'negative').length;
|
| 1503 |
+
|
| 1504 |
+
document.getElementById('positiveFeedbacks').textContent = positive;
|
| 1505 |
+
document.getElementById('negativeFeedbacks').textContent = negative;
|
| 1506 |
+
document.getElementById('totalFeedbacksCount').textContent = feedbacks.length;
|
| 1507 |
+
|
| 1508 |
+
// Display all feedbacks with contextual greeting
|
| 1509 |
+
container.innerHTML = feedbacks.map(feedback => {
|
| 1510 |
+
const type = (feedback.feedback_type || 'neutral').toLowerCase();
|
| 1511 |
+
|
| 1512 |
+
return `
|
| 1513 |
+
<div class="feedback-card">
|
| 1514 |
+
<div class="feedback-header">
|
| 1515 |
+
<div>
|
| 1516 |
+
<span class="feedback-user">${DOMPurify.sanitize(feedback.user_email || 'Unknown User')}</span>
|
| 1517 |
+
<br>
|
| 1518 |
+
<small class="feedback-date">${new Date(feedback.created_at).toLocaleString('en-US')}</small>
|
| 1519 |
+
</div>
|
| 1520 |
+
<div class="feedback-type">
|
| 1521 |
+
${type === 'positive' ? '👍' : '👎'}
|
| 1522 |
+
<span class="badge ${type}">${type === 'positive' ? 'Positive' : 'Negative'}</span>
|
| 1523 |
+
</div>
|
| 1524 |
+
</div>
|
| 1525 |
+
|
| 1526 |
+
<div style="margin: 10px 0; opacity: 0.7; font-size: 12px;">
|
| 1527 |
+
Conversation: ${DOMPurify.sanitize(feedback.conversation_title || 'Untitled')}
|
| 1528 |
+
</div>
|
| 1529 |
+
|
| 1530 |
+
<div class="feedback-message">
|
| 1531 |
+
<strong>Message:</strong>
|
| 1532 |
+
<p style="margin: 5px 0;">${DOMPurify.sanitize(feedback.message_content || '')}</p>
|
| 1533 |
+
</div>
|
| 1534 |
+
|
| 1535 |
+
${feedback.comment ? `
|
| 1536 |
+
<div style="margin-top: 10px; padding: 10px; border-radius: 5px; background: rgba(58, 102, 42, 0.1);">
|
| 1537 |
+
<strong>Comment:</strong>
|
| 1538 |
+
<p style="margin: 5px 0;">${DOMPurify.sanitize(feedback.comment || '')}</p>
|
| 1539 |
+
</div>
|
| 1540 |
+
` : ''}
|
| 1541 |
+
</div>
|
| 1542 |
+
`}).join('');
|
| 1543 |
+
}
|
| 1544 |
+
|
| 1545 |
+
</script>
|
| 1546 |
+
</body>
|
| 1547 |
+
</html>
|
Dockerfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# استخدام صورة بايثون رسمية
|
| 2 |
+
FROM python:3.10
|
| 3 |
+
|
| 4 |
+
# تعيين مجلد العمل
|
| 5 |
+
WORKDIR /code
|
| 6 |
+
|
| 7 |
+
# نسخ ملف المتطلبات وتثبيتها
|
| 8 |
+
COPY ./requirements.txt /code/requirements.txt
|
| 9 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 10 |
+
|
| 11 |
+
# نسخ باقي ملفات المشروع
|
| 12 |
+
COPY . /code
|
| 13 |
+
|
| 14 |
+
# منح صلاحية التنفيذ لسكربت التشغيل وتشغيله
|
| 15 |
+
RUN chmod +x /code/start.sh
|
| 16 |
+
CMD ["/code/start.sh"]
|
__init__.py
ADDED
|
File without changes
|
backend.code-workspace
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"folders": [
|
| 3 |
+
{
|
| 4 |
+
"path": "."
|
| 5 |
+
},
|
| 6 |
+
{
|
| 7 |
+
"path": "../scr"
|
| 8 |
+
}
|
| 9 |
+
]
|
| 10 |
+
}
|
ch.png
ADDED
|
chat.html
ADDED
|
@@ -0,0 +1,1360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" dir="ltr">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Chat - University AI</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
:root {
|
| 15 |
+
--olive-light: #3A662A;
|
| 16 |
+
--olive-dark: #5C6E4A;
|
| 17 |
+
--bg-light: #F5F5F5;
|
| 18 |
+
--bg-dark: #1A1A1A;
|
| 19 |
+
--text-light: #2C2C2C;
|
| 20 |
+
--text-dark: #F5F5F5;
|
| 21 |
+
--card-light: #FFFFFF;
|
| 22 |
+
--card-dark: #2D2D2D;
|
| 23 |
+
--error-color: #c33;
|
| 24 |
+
--error-hover: #a22;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
body {
|
| 28 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 29 |
+
transition: all 0.3s ease;
|
| 30 |
+
min-height: 100vh;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
body.light-mode {
|
| 34 |
+
background: var(--bg-light);
|
| 35 |
+
color: var(--text-light);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
body.dark-mode {
|
| 39 |
+
background: var(--bg-dark);
|
| 40 |
+
color: var(--text-dark);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/* Modal Styles */
|
| 44 |
+
.modal {
|
| 45 |
+
display: none;
|
| 46 |
+
position: fixed;
|
| 47 |
+
z-index: 1000;
|
| 48 |
+
left: 0;
|
| 49 |
+
top: 0;
|
| 50 |
+
width: 100%;
|
| 51 |
+
height: 100%;
|
| 52 |
+
background-color: rgba(0, 0, 0, 0.6);
|
| 53 |
+
backdrop-filter: blur(5px);
|
| 54 |
+
animation: fadeIn 0.3s ease;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
@keyframes fadeIn {
|
| 58 |
+
from { opacity: 0; }
|
| 59 |
+
to { opacity: 1; }
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.modal-content {
|
| 63 |
+
background-color: white;
|
| 64 |
+
margin: 5% auto;
|
| 65 |
+
padding: 30px;
|
| 66 |
+
border-radius: 16px;
|
| 67 |
+
max-width: 500px;
|
| 68 |
+
width: 90%;
|
| 69 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
| 70 |
+
text-align: center;
|
| 71 |
+
animation: slideDown 0.4s ease;
|
| 72 |
+
position: relative;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
@keyframes slideDown {
|
| 76 |
+
from {
|
| 77 |
+
transform: translateY(-50px);
|
| 78 |
+
opacity: 0;
|
| 79 |
+
}
|
| 80 |
+
to {
|
| 81 |
+
transform: translateY(0);
|
| 82 |
+
opacity: 1;
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.modal-content h3 {
|
| 87 |
+
color: var(--olive-light);
|
| 88 |
+
font-size: 24px;
|
| 89 |
+
margin-bottom: 10px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.modal-content ol {
|
| 93 |
+
margin-left: 20px;
|
| 94 |
+
margin-bottom: 8px;
|
| 95 |
+
margin-top: 5px;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.modal-content li {
|
| 99 |
+
margin-bottom: 6px;
|
| 100 |
+
text-align: left;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.modal-btn {
|
| 104 |
+
background: var(--olive-light);
|
| 105 |
+
color: white;
|
| 106 |
+
border: none;
|
| 107 |
+
padding: 12px 40px;
|
| 108 |
+
border-radius: 8px;
|
| 109 |
+
font-size: 16px;
|
| 110 |
+
cursor: pointer;
|
| 111 |
+
margin-top: 20px;
|
| 112 |
+
transition: all 0.3s ease;
|
| 113 |
+
font-weight: 600;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.modal-btn:hover {
|
| 117 |
+
background: var(--olive-dark);
|
| 118 |
+
transform: translateY(-2px);
|
| 119 |
+
box-shadow: 0 5px 15px rgba(58, 102, 42, 0.4);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/* Sidebar */
|
| 123 |
+
.sidebar {
|
| 124 |
+
position: fixed;
|
| 125 |
+
left: 0;
|
| 126 |
+
top: 0;
|
| 127 |
+
width: 250px;
|
| 128 |
+
height: 100vh;
|
| 129 |
+
padding: 20px;
|
| 130 |
+
transition: transform 0.3s ease;
|
| 131 |
+
z-index: 100;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.light-mode .sidebar {
|
| 135 |
+
background: var(--card-light);
|
| 136 |
+
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.dark-mode .sidebar {
|
| 140 |
+
background: var(--card-dark);
|
| 141 |
+
box-shadow: 2px 0 10px rgba(0,0,0,0.3);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
/* Mobile menu button */
|
| 145 |
+
.menu-toggle {
|
| 146 |
+
display: none;
|
| 147 |
+
position: fixed;
|
| 148 |
+
top: 20px;
|
| 149 |
+
left: 20px;
|
| 150 |
+
z-index: 101;
|
| 151 |
+
background: var(--olive-light);
|
| 152 |
+
color: white;
|
| 153 |
+
border: none;
|
| 154 |
+
width: 40px;
|
| 155 |
+
height: 40px;
|
| 156 |
+
border-radius: 8px;
|
| 157 |
+
cursor: pointer;
|
| 158 |
+
font-size: 20px;
|
| 159 |
+
align-items: center;
|
| 160 |
+
justify-content: center;
|
| 161 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
| 162 |
+
transition: all 0.3s ease;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.menu-toggle:hover {
|
| 166 |
+
transform: scale(1.1);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
/* Overlay for mobile */
|
| 170 |
+
.sidebar-overlay {
|
| 171 |
+
display: none;
|
| 172 |
+
position: fixed;
|
| 173 |
+
top: 0;
|
| 174 |
+
left: 0;
|
| 175 |
+
right: 0;
|
| 176 |
+
bottom: 0;
|
| 177 |
+
background: rgba(0,0,0,0.5);
|
| 178 |
+
z-index: 99;
|
| 179 |
+
opacity: 0;
|
| 180 |
+
transition: opacity 0.3s ease;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.sidebar-overlay.active {
|
| 184 |
+
opacity: 1;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.logo {
|
| 188 |
+
font-size: 24px;
|
| 189 |
+
font-weight: bold;
|
| 190 |
+
color: var(--olive-light);
|
| 191 |
+
margin-bottom: 40px;
|
| 192 |
+
text-align: center;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
.new-chat-btn {
|
| 196 |
+
width: 100%;
|
| 197 |
+
padding: 12px;
|
| 198 |
+
background: var(--olive-light);
|
| 199 |
+
color: white;
|
| 200 |
+
border: none;
|
| 201 |
+
border-radius: 8px;
|
| 202 |
+
cursor: pointer;
|
| 203 |
+
margin-bottom: 20px;
|
| 204 |
+
font-size: 16px;
|
| 205 |
+
transition: all 0.3s ease;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.new-chat-btn:hover {
|
| 209 |
+
transform: translateY(-2px);
|
| 210 |
+
box-shadow: 0 5px 15px rgba(58, 102, 42, 0.4);
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.conversations-list {
|
| 214 |
+
max-height: calc(100vh - 300px);
|
| 215 |
+
overflow-y: auto;
|
| 216 |
+
margin-bottom: 20px;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.conversation-item {
|
| 220 |
+
padding: 12px;
|
| 221 |
+
margin-bottom: 8px;
|
| 222 |
+
border-radius: 8px;
|
| 223 |
+
cursor: pointer;
|
| 224 |
+
transition: all 0.3s ease;
|
| 225 |
+
display: flex;
|
| 226 |
+
justify-content: space-between;
|
| 227 |
+
align-items: center;
|
| 228 |
+
gap: 10px;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.light-mode .conversation-item {
|
| 232 |
+
background: var(--bg-light);
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.dark-mode .conversation-item {
|
| 236 |
+
background: var(--bg-dark);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.conversation-item:hover {
|
| 240 |
+
background: var(--olive-light);
|
| 241 |
+
color: white;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.conversation-item.active {
|
| 245 |
+
background: var(--olive-light);
|
| 246 |
+
color: white;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.conversation-title {
|
| 250 |
+
flex: 1;
|
| 251 |
+
overflow: hidden;
|
| 252 |
+
text-overflow: ellipsis;
|
| 253 |
+
white-space: nowrap;
|
| 254 |
+
font-size: 14px;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.delete-conv-btn {
|
| 258 |
+
background: rgba(255, 255, 255, 0.2);
|
| 259 |
+
border: none;
|
| 260 |
+
padding: 5px 10px;
|
| 261 |
+
border-radius: 5px;
|
| 262 |
+
cursor: pointer;
|
| 263 |
+
font-size: 14px;
|
| 264 |
+
transition: all 0.2s ease;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.delete-conv-btn:hover {
|
| 268 |
+
background: rgba(255, 255, 255, 0.3);
|
| 269 |
+
transform: scale(1.1);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.logout-btn {
|
| 273 |
+
position: absolute;
|
| 274 |
+
bottom: 20px;
|
| 275 |
+
left: 20px;
|
| 276 |
+
right: 20px;
|
| 277 |
+
padding: 12px;
|
| 278 |
+
background: var(--error-color);
|
| 279 |
+
color: white;
|
| 280 |
+
border: none;
|
| 281 |
+
border-radius: 8px;
|
| 282 |
+
cursor: pointer;
|
| 283 |
+
transition: all 0.3s ease;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.logout-btn:hover {
|
| 287 |
+
background: var(--error-hover);
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
/* Main Content */
|
| 291 |
+
.main-content {
|
| 292 |
+
margin-left: 250px;
|
| 293 |
+
padding: 30px;
|
| 294 |
+
min-height: 100vh;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.header-bar {
|
| 298 |
+
display: flex;
|
| 299 |
+
justify-content: space-between;
|
| 300 |
+
align-items: center;
|
| 301 |
+
margin-bottom: 30px;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.header-bar h1 {
|
| 305 |
+
font-size: 32px;
|
| 306 |
+
color: var(--olive-light);
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
.theme-toggle {
|
| 310 |
+
width: 50px;
|
| 311 |
+
height: 26px;
|
| 312 |
+
background: var(--olive-light);
|
| 313 |
+
border-radius: 13px;
|
| 314 |
+
position: relative;
|
| 315 |
+
cursor: pointer;
|
| 316 |
+
transition: all 0.3s ease;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.theme-toggle::after {
|
| 320 |
+
content: '☀️';
|
| 321 |
+
position: absolute;
|
| 322 |
+
top: 3px;
|
| 323 |
+
left: 3px;
|
| 324 |
+
width: 20px;
|
| 325 |
+
height: 20px;
|
| 326 |
+
background: white;
|
| 327 |
+
border-radius: 50%;
|
| 328 |
+
transition: all 0.3s ease;
|
| 329 |
+
display: flex;
|
| 330 |
+
align-items: center;
|
| 331 |
+
justify-content: center;
|
| 332 |
+
font-size: 12px;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
.dark-mode .theme-toggle::after {
|
| 336 |
+
content: '🌙';
|
| 337 |
+
left: 27px;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
/* Chat Area */
|
| 341 |
+
.chat-area {
|
| 342 |
+
display: flex;
|
| 343 |
+
flex-direction: column;
|
| 344 |
+
height: calc(100vh - 140px);
|
| 345 |
+
height: calc(100dvh - 140px); /* Better mobile support */
|
| 346 |
+
border-radius: 12px;
|
| 347 |
+
overflow: hidden;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.light-mode .chat-area {
|
| 351 |
+
background: var(--card-light);
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.dark-mode .chat-area {
|
| 355 |
+
background: var(--card-dark);
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
.messages-container {
|
| 359 |
+
flex: 1;
|
| 360 |
+
padding: 20px;
|
| 361 |
+
overflow-y: auto;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.message {
|
| 365 |
+
margin-bottom: 20px;
|
| 366 |
+
display: flex;
|
| 367 |
+
gap: 10px;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.message.user {
|
| 371 |
+
justify-content: flex-end;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.message.ai {
|
| 375 |
+
flex-direction: column;
|
| 376 |
+
align-items: flex-start;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.message-content {
|
| 380 |
+
max-width: 70%;
|
| 381 |
+
padding: 15px 20px;
|
| 382 |
+
border-radius: 12px;
|
| 383 |
+
line-height: 1.6;
|
| 384 |
+
word-wrap: break-word;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
.message.user .message-content {
|
| 388 |
+
background: var(--olive-light);
|
| 389 |
+
color: white;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
.light-mode .message.ai .message-content {
|
| 393 |
+
background: var(--bg-light);
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
.dark-mode .message.ai .message-content {
|
| 397 |
+
background: var(--bg-dark);
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
/* Feedback Buttons */
|
| 401 |
+
.feedback-buttons {
|
| 402 |
+
display: flex;
|
| 403 |
+
gap: 8px;
|
| 404 |
+
margin-top: 8px;
|
| 405 |
+
margin-left: 5px;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
.feedback-btn {
|
| 409 |
+
background: none;
|
| 410 |
+
border: none;
|
| 411 |
+
font-size: 18px;
|
| 412 |
+
cursor: pointer;
|
| 413 |
+
padding: 5px 10px;
|
| 414 |
+
border-radius: 5px;
|
| 415 |
+
transition: all 0.2s ease;
|
| 416 |
+
opacity: 0.6;
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
.feedback-btn:hover {
|
| 420 |
+
opacity: 1;
|
| 421 |
+
transform: scale(1.2);
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.feedback-btn.active {
|
| 425 |
+
opacity: 1;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.feedback-btn.thumbs-up:hover,
|
| 429 |
+
.feedback-btn.thumbs-up.active {
|
| 430 |
+
background: rgba(76, 175, 80, 0.1);
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.feedback-btn.thumbs-down:hover,
|
| 434 |
+
.feedback-btn.thumbs-down.active {
|
| 435 |
+
background: rgba(244, 67, 54, 0.1);
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
.chat-input-area {
|
| 439 |
+
padding: 20px;
|
| 440 |
+
border-top: 2px solid var(--olive-light);
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
.input-wrapper {
|
| 444 |
+
display: flex;
|
| 445 |
+
gap: 10px;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
#messageInput {
|
| 449 |
+
flex: 1;
|
| 450 |
+
padding: 15px;
|
| 451 |
+
border-radius: 8px;
|
| 452 |
+
border: 2px solid transparent;
|
| 453 |
+
font-size: 16px;
|
| 454 |
+
transition: all 0.3s ease;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
.light-mode #messageInput {
|
| 458 |
+
background: var(--bg-light);
|
| 459 |
+
color: var(--text-light);
|
| 460 |
+
border-color: #e0e0e0;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
.dark-mode #messageInput {
|
| 464 |
+
background: var(--bg-dark);
|
| 465 |
+
color: var(--text-dark);
|
| 466 |
+
border-color: #444;
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
#messageInput:focus {
|
| 470 |
+
outline: none;
|
| 471 |
+
border-color: var(--olive-light);
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.send-btn {
|
| 475 |
+
padding: 15px 30px;
|
| 476 |
+
background: var(--olive-light);
|
| 477 |
+
color: white;
|
| 478 |
+
border: none;
|
| 479 |
+
border-radius: 8px;
|
| 480 |
+
cursor: pointer;
|
| 481 |
+
font-size: 16px;
|
| 482 |
+
transition: all 0.3s ease;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
.send-btn:hover:not(:disabled) {
|
| 486 |
+
transform: translateY(-2px);
|
| 487 |
+
box-shadow: 0 5px 15px rgba(58, 102, 42, 0.4);
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.send-btn:disabled {
|
| 491 |
+
opacity: 0.5;
|
| 492 |
+
cursor: not-allowed;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
.send-btn .spinner {
|
| 496 |
+
display: inline-block;
|
| 497 |
+
width: 16px;
|
| 498 |
+
height: 16px;
|
| 499 |
+
border: 2px solid rgba(255,255,255,.3);
|
| 500 |
+
border-radius: 50%;
|
| 501 |
+
border-top-color: white;
|
| 502 |
+
animation: spin 0.8s linear infinite;
|
| 503 |
+
vertical-align: middle;
|
| 504 |
+
margin-left: 5px;
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
.empty-state {
|
| 508 |
+
display: flex;
|
| 509 |
+
flex-direction: column;
|
| 510 |
+
align-items: center;
|
| 511 |
+
justify-content: center;
|
| 512 |
+
height: 100%;
|
| 513 |
+
opacity: 0.5;
|
| 514 |
+
text-align: center;
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
.empty-state img {
|
| 518 |
+
width: 150px;
|
| 519 |
+
margin-bottom: 20px;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
.empty-state h2 {
|
| 523 |
+
margin-bottom: 10px;
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
/* Loading indicator */
|
| 527 |
+
.loading-indicator {
|
| 528 |
+
display: inline-block;
|
| 529 |
+
width: 20px;
|
| 530 |
+
height: 20px;
|
| 531 |
+
border: 3px solid rgba(255,255,255,.3);
|
| 532 |
+
border-radius: 50%;
|
| 533 |
+
border-top-color: white;
|
| 534 |
+
animation: spin 1s ease-in-out infinite;
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
@keyframes spin {
|
| 538 |
+
to { transform: rotate(360deg); }
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
/* Typing indicator */
|
| 542 |
+
.typing-indicator {
|
| 543 |
+
display: flex;
|
| 544 |
+
align-items: center;
|
| 545 |
+
gap: 10px;
|
| 546 |
+
padding: 15px 20px;
|
| 547 |
+
max-width: 70%;
|
| 548 |
+
border-radius: 12px;
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
.light-mode .typing-indicator {
|
| 552 |
+
background: var(--bg-light);
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.dark-mode .typing-indicator {
|
| 556 |
+
background: var(--bg-dark);
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
.typing-dots {
|
| 560 |
+
display: flex;
|
| 561 |
+
gap: 4px;
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.typing-dot {
|
| 565 |
+
width: 8px;
|
| 566 |
+
height: 8px;
|
| 567 |
+
background: var(--olive-light);
|
| 568 |
+
border-radius: 50%;
|
| 569 |
+
animation: typing 1.4s infinite;
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
.typing-dot:nth-child(2) {
|
| 573 |
+
animation-delay: 0.2s;
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
.typing-dot:nth-child(3) {
|
| 577 |
+
animation-delay: 0.4s;
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
@keyframes typing {
|
| 581 |
+
0%, 60%, 100% {
|
| 582 |
+
transform: translateY(0);
|
| 583 |
+
opacity: 0.7;
|
| 584 |
+
}
|
| 585 |
+
30% {
|
| 586 |
+
transform: translateY(-10px);
|
| 587 |
+
opacity: 1;
|
| 588 |
+
}
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
/* Mobile responsive */
|
| 592 |
+
@media (max-width: 768px) {
|
| 593 |
+
.menu-toggle {
|
| 594 |
+
display: flex;
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
.sidebar {
|
| 598 |
+
transform: translateX(-100%);
|
| 599 |
+
width: 250px;
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
.sidebar.active {
|
| 603 |
+
transform: translateX(0);
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
.sidebar-overlay {
|
| 607 |
+
display: block;
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
.main-content {
|
| 611 |
+
margin-left: 0;
|
| 612 |
+
padding: 80px 20px 20px 20px;
|
| 613 |
+
height: 100dvh; /* Full viewport height on mobile */
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
.message-content {
|
| 617 |
+
max-width: 85%;
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
.header-bar {
|
| 621 |
+
margin-top: 20px;
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
.header-bar h1 {
|
| 625 |
+
font-size: 24px;
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
.modal-content {
|
| 629 |
+
margin: 10% auto;
|
| 630 |
+
padding: 20px;
|
| 631 |
+
}
|
| 632 |
+
}
|
| 633 |
+
</style>
|
| 634 |
+
</head>
|
| 635 |
+
<body class="light-mode">
|
| 636 |
+
<!-- Welcome Modal -->
|
| 637 |
+
<div id="welcomeModal" class="modal">
|
| 638 |
+
<div class="modal-content">
|
| 639 |
+
<div style="font-size: 40px; margin-bottom: 15px;">👋</div>
|
| 640 |
+
<h3>Welcome to University AI!</h3>
|
| 641 |
+
<div style="text-align: left; line-height: 1.5; opacity: 0.9; max-height: 300px; overflow-y: auto; padding-right: 10px;">
|
| 642 |
+
<p style="margin-bottom: 8px;">
|
| 643 |
+
Please note the following: You are only allowed to ask about lectures covered in the following courses:
|
| 644 |
+
</p>
|
| 645 |
+
|
| 646 |
+
<p style="margin-bottom: 5px; margin-top: 8px; font-weight: bold;">Semester 7 Courses:</p>
|
| 647 |
+
<ol>
|
| 648 |
+
<li>Networks</li>
|
| 649 |
+
<li>Information Security</li>
|
| 650 |
+
<li>Mobile Applications</li>
|
| 651 |
+
<li>Computation Theory</li>
|
| 652 |
+
<li>Operating Systems</li>
|
| 653 |
+
</ol>
|
| 654 |
+
|
| 655 |
+
<p style="margin-bottom: 5px; margin-top: 8px; font-weight: bold;">Semester 8 Courses:</p>
|
| 656 |
+
<ol>
|
| 657 |
+
<li>Human-Computer Interaction</li>
|
| 658 |
+
<li>Computer Graphics</li>
|
| 659 |
+
<li>Algorithm Analysis and Design</li>
|
| 660 |
+
<li>Compiler Design</li>
|
| 661 |
+
<li>Computer Architecture</li>
|
| 662 |
+
<li>Machine Learning</li>
|
| 663 |
+
</ol>
|
| 664 |
+
</div>
|
| 665 |
+
<button class="modal-btn" onclick="closeWelcomeModal()">Got it!</button>
|
| 666 |
+
</div>
|
| 667 |
+
</div>
|
| 668 |
+
|
| 669 |
+
<!-- Mobile Menu Toggle -->
|
| 670 |
+
<button class="menu-toggle" id="menuToggle">☰</button>
|
| 671 |
+
|
| 672 |
+
<!-- Sidebar Overlay for Mobile -->
|
| 673 |
+
<div class="sidebar-overlay" id="sidebarOverlay"></div>
|
| 674 |
+
|
| 675 |
+
<!-- Sidebar -->
|
| 676 |
+
<div class="sidebar" id="sidebar">
|
| 677 |
+
<div class="logo">🎓 University AI</div>
|
| 678 |
+
|
| 679 |
+
<button class="new-chat-btn" id="newChatBtn">
|
| 680 |
+
➕ New Conversation
|
| 681 |
+
</button>
|
| 682 |
+
|
| 683 |
+
<div class="conversations-list" id="conversationsList">
|
| 684 |
+
<!-- Conversations will be loaded here -->
|
| 685 |
+
</div>
|
| 686 |
+
|
| 687 |
+
<button class="logout-btn" id="logoutBtn">
|
| 688 |
+
🚪 Logout
|
| 689 |
+
</button>
|
| 690 |
+
</div>
|
| 691 |
+
|
| 692 |
+
<!-- Main Content -->
|
| 693 |
+
<div class="main-content">
|
| 694 |
+
<div class="header-bar">
|
| 695 |
+
<h1>Welcome 👋</h1>
|
| 696 |
+
<div class="theme-toggle" id="themeToggle"></div>
|
| 697 |
+
</div>
|
| 698 |
+
|
| 699 |
+
<!-- Chat Area -->
|
| 700 |
+
<div class="chat-area">
|
| 701 |
+
<div class="messages-container" id="messagesContainer">
|
| 702 |
+
<div class="empty-state">
|
| 703 |
+
<img src="/static/ch.png" alt="Start conversation" onerror="this.style.display='none'">
|
| 704 |
+
<h2>Start a new conversation</h2>
|
| 705 |
+
<p>Ask any question about your lectures or study materials</p>
|
| 706 |
+
</div>
|
| 707 |
+
</div>
|
| 708 |
+
|
| 709 |
+
<div class="chat-input-area">
|
| 710 |
+
<div class="input-wrapper">
|
| 711 |
+
<input
|
| 712 |
+
type="text"
|
| 713 |
+
id="messageInput"
|
| 714 |
+
placeholder="Type your message here..."
|
| 715 |
+
autocomplete="off"
|
| 716 |
+
>
|
| 717 |
+
<button class="send-btn" id="sendBtn">
|
| 718 |
+
Send 📤
|
| 719 |
+
</button>
|
| 720 |
+
</div>
|
| 721 |
+
</div>
|
| 722 |
+
</div>
|
| 723 |
+
</div>
|
| 724 |
+
|
| 725 |
+
<script>
|
| 726 |
+
// Configuration
|
| 727 |
+
const CONFIG = {
|
| 728 |
+
API_URL: '', // Empty string uses the current origin (relative path)
|
| 729 |
+
// RAG_URL removed: Client talks to Main API, Main API talks to RAG
|
| 730 |
+
STORAGE_KEYS: {
|
| 731 |
+
TOKEN: 'token',
|
| 732 |
+
ROLE: 'role',
|
| 733 |
+
THEME: 'theme',
|
| 734 |
+
WELCOME_SHOWN: 'welcome_shown'
|
| 735 |
+
},
|
| 736 |
+
ROLES: {
|
| 737 |
+
STUDENT: 'student'
|
| 738 |
+
}
|
| 739 |
+
};
|
| 740 |
+
|
| 741 |
+
// State management
|
| 742 |
+
const state = {
|
| 743 |
+
currentConversationId: null,
|
| 744 |
+
messageIdCounter: 0,
|
| 745 |
+
isTyping: false
|
| 746 |
+
};
|
| 747 |
+
|
| 748 |
+
// Utility functions
|
| 749 |
+
function closeWelcomeModal() {
|
| 750 |
+
document.getElementById('welcomeModal').style.display = 'none';
|
| 751 |
+
utils.setStorageItem(CONFIG.STORAGE_KEYS.WELCOME_SHOWN, 'true');
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
function showWelcomeModal() {
|
| 755 |
+
const welcomeShown = utils.getStorageItem(CONFIG.STORAGE_KEYS.WELCOME_SHOWN);
|
| 756 |
+
if (!welcomeShown) {
|
| 757 |
+
document.getElementById('welcomeModal').style.display = 'block';
|
| 758 |
+
}
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
const utils = {
|
| 762 |
+
escapeHtml(text) {
|
| 763 |
+
const div = document.createElement('div');
|
| 764 |
+
div.textContent = text;
|
| 765 |
+
return div.innerHTML;
|
| 766 |
+
},
|
| 767 |
+
|
| 768 |
+
getStorageItem(key) {
|
| 769 |
+
try {
|
| 770 |
+
return localStorage.getItem(key);
|
| 771 |
+
} catch (e) {
|
| 772 |
+
console.error('Error reading from localStorage:', e);
|
| 773 |
+
return null;
|
| 774 |
+
}
|
| 775 |
+
},
|
| 776 |
+
|
| 777 |
+
setStorageItem(key, value) {
|
| 778 |
+
try {
|
| 779 |
+
localStorage.setItem(key, value);
|
| 780 |
+
return true;
|
| 781 |
+
} catch (e) {
|
| 782 |
+
console.error('Error writing to localStorage:', e);
|
| 783 |
+
return false;
|
| 784 |
+
}
|
| 785 |
+
},
|
| 786 |
+
|
| 787 |
+
removeStorageItem(key) {
|
| 788 |
+
try {
|
| 789 |
+
localStorage.removeItem(key);
|
| 790 |
+
} catch (e) {
|
| 791 |
+
console.error('Error removing from localStorage:', e);
|
| 792 |
+
}
|
| 793 |
+
},
|
| 794 |
+
|
| 795 |
+
clearStorage() {
|
| 796 |
+
try {
|
| 797 |
+
localStorage.clear();
|
| 798 |
+
} catch (e) {
|
| 799 |
+
console.error('Error clearing localStorage:', e);
|
| 800 |
+
}
|
| 801 |
+
}
|
| 802 |
+
};
|
| 803 |
+
|
| 804 |
+
// API functions
|
| 805 |
+
const api = {
|
| 806 |
+
async makeRequest(url, options = {}) {
|
| 807 |
+
const token = utils.getStorageItem(CONFIG.STORAGE_KEYS.TOKEN);
|
| 808 |
+
|
| 809 |
+
const defaultOptions = {
|
| 810 |
+
headers: {
|
| 811 |
+
'Content-Type': 'application/json',
|
| 812 |
+
...(token && { 'Authorization': `Bearer ${token}` })
|
| 813 |
+
}
|
| 814 |
+
};
|
| 815 |
+
|
| 816 |
+
try {
|
| 817 |
+
const response = await fetch(url, { ...defaultOptions, ...options });
|
| 818 |
+
return response;
|
| 819 |
+
} catch (error) {
|
| 820 |
+
console.error('Network error:', error);
|
| 821 |
+
throw error;
|
| 822 |
+
}
|
| 823 |
+
},
|
| 824 |
+
|
| 825 |
+
async sendMessage(message) {
|
| 826 |
+
// Send to Main API instead of internal RAG service
|
| 827 |
+
const payload = {
|
| 828 |
+
message: message
|
| 829 |
+
};
|
| 830 |
+
if (state.currentConversationId) {
|
| 831 |
+
payload.conversation_id = state.currentConversationId;
|
| 832 |
+
}
|
| 833 |
+
return await this.makeRequest(`${CONFIG.API_URL}/student/chat`, {
|
| 834 |
+
method: 'POST',
|
| 835 |
+
body: JSON.stringify(payload)
|
| 836 |
+
});
|
| 837 |
+
},
|
| 838 |
+
|
| 839 |
+
async sendFeedback(messageId, feedbackType) {
|
| 840 |
+
return await this.makeRequest(`${CONFIG.API_URL}/feedback`, {
|
| 841 |
+
method: 'POST',
|
| 842 |
+
body: JSON.stringify({
|
| 843 |
+
message_id: messageId,
|
| 844 |
+
feedback_type: feedbackType
|
| 845 |
+
})
|
| 846 |
+
});
|
| 847 |
+
},
|
| 848 |
+
|
| 849 |
+
async checkAuth() {
|
| 850 |
+
return await this.makeRequest(`${CONFIG.API_URL}/user/me`);
|
| 851 |
+
},
|
| 852 |
+
|
| 853 |
+
async getConversations() {
|
| 854 |
+
return await this.makeRequest(`${CONFIG.API_URL}/student/conversations`);
|
| 855 |
+
},
|
| 856 |
+
|
| 857 |
+
async getConversation(id) {
|
| 858 |
+
return await this.makeRequest(`${CONFIG.API_URL}/student/conversation/${id}`);
|
| 859 |
+
},
|
| 860 |
+
|
| 861 |
+
async deleteConversation(id) {
|
| 862 |
+
return await this.makeRequest(`${CONFIG.API_URL}/student/conversations/${id}`, {
|
| 863 |
+
method: 'DELETE'
|
| 864 |
+
});
|
| 865 |
+
}
|
| 866 |
+
};
|
| 867 |
+
|
| 868 |
+
// UI functions
|
| 869 |
+
const ui = {
|
| 870 |
+
elements: {
|
| 871 |
+
messagesContainer: document.getElementById('messagesContainer'),
|
| 872 |
+
messageInput: document.getElementById('messageInput'),
|
| 873 |
+
sendBtn: document.getElementById('sendBtn'),
|
| 874 |
+
newChatBtn: document.getElementById('newChatBtn'),
|
| 875 |
+
logoutBtn: document.getElementById('logoutBtn'),
|
| 876 |
+
themeToggle: document.getElementById('themeToggle'),
|
| 877 |
+
conversationsList: document.getElementById('conversationsList'),
|
| 878 |
+
menuToggle: document.getElementById('menuToggle'),
|
| 879 |
+
sidebar: document.getElementById('sidebar'),
|
| 880 |
+
sidebarOverlay: document.getElementById('sidebarOverlay')
|
| 881 |
+
},
|
| 882 |
+
|
| 883 |
+
toggleSidebar() {
|
| 884 |
+
this.elements.sidebar.classList.toggle('active');
|
| 885 |
+
this.elements.sidebarOverlay.classList.toggle('active');
|
| 886 |
+
},
|
| 887 |
+
|
| 888 |
+
closeSidebar() {
|
| 889 |
+
if (state.isMobile) {
|
| 890 |
+
this.elements.sidebar.classList.remove('active');
|
| 891 |
+
this.elements.sidebarOverlay.classList.remove('active');
|
| 892 |
+
}
|
| 893 |
+
},
|
| 894 |
+
|
| 895 |
+
clearEmptyState() {
|
| 896 |
+
const emptyState = this.elements.messagesContainer.querySelector('.empty-state');
|
| 897 |
+
if (emptyState) {
|
| 898 |
+
this.elements.messagesContainer.innerHTML = '';
|
| 899 |
+
}
|
| 900 |
+
},
|
| 901 |
+
|
| 902 |
+
scrollToBottom() {
|
| 903 |
+
this.elements.messagesContainer.scrollTop = this.elements.messagesContainer.scrollHeight;
|
| 904 |
+
},
|
| 905 |
+
|
| 906 |
+
addUserMessage(text) {
|
| 907 |
+
this.clearEmptyState();
|
| 908 |
+
|
| 909 |
+
const messageDiv = document.createElement('div');
|
| 910 |
+
messageDiv.className = 'message user';
|
| 911 |
+
messageDiv.innerHTML = `<div class='message-content'>${utils.escapeHtml(text)}</div>`;
|
| 912 |
+
|
| 913 |
+
this.elements.messagesContainer.appendChild(messageDiv);
|
| 914 |
+
this.scrollToBottom();
|
| 915 |
+
},
|
| 916 |
+
|
| 917 |
+
addAIMessage(text, messageId) {
|
| 918 |
+
const messageDiv = document.createElement('div');
|
| 919 |
+
messageDiv.className = 'message ai';
|
| 920 |
+
messageDiv.innerHTML = `
|
| 921 |
+
<div class='message-content'>${utils.escapeHtml(text)}</div>
|
| 922 |
+
<div class='feedback-buttons'>
|
| 923 |
+
<button type="button" class='feedback-btn copy-btn' data-action="copy" title='Copy to clipboard'>
|
| 924 |
+
📋
|
| 925 |
+
</button>
|
| 926 |
+
</div>
|
| 927 |
+
`;
|
| 928 |
+
|
| 929 |
+
this.elements.messagesContainer.appendChild(messageDiv);
|
| 930 |
+
this.scrollToBottom();
|
| 931 |
+
return messageDiv.querySelector('.message-content');
|
| 932 |
+
},
|
| 933 |
+
|
| 934 |
+
showTypingIndicator() {
|
| 935 |
+
if (state.isTyping) return;
|
| 936 |
+
|
| 937 |
+
state.isTyping = true;
|
| 938 |
+
const typingDiv = document.createElement('div');
|
| 939 |
+
typingDiv.className = 'message ai';
|
| 940 |
+
typingDiv.id = 'typing-indicator';
|
| 941 |
+
typingDiv.innerHTML = `
|
| 942 |
+
<div class='typing-indicator'>
|
| 943 |
+
<div class='typing-dots'>
|
| 944 |
+
<div class='typing-dot'></div>
|
| 945 |
+
<div class='typing-dot'></div>
|
| 946 |
+
<div class='typing-dot'></div>
|
| 947 |
+
</div>
|
| 948 |
+
</div>
|
| 949 |
+
`;
|
| 950 |
+
|
| 951 |
+
this.elements.messagesContainer.appendChild(typingDiv);
|
| 952 |
+
this.scrollToBottom();
|
| 953 |
+
},
|
| 954 |
+
|
| 955 |
+
hideTypingIndicator() {
|
| 956 |
+
state.isTyping = false;
|
| 957 |
+
const typingIndicator = document.getElementById('typing-indicator');
|
| 958 |
+
if (typingIndicator) {
|
| 959 |
+
typingIndicator.remove();
|
| 960 |
+
}
|
| 961 |
+
},
|
| 962 |
+
|
| 963 |
+
setInputState(disabled) {
|
| 964 |
+
this.elements.messageInput.disabled = disabled;
|
| 965 |
+
this.elements.sendBtn.disabled = disabled;
|
| 966 |
+
|
| 967 |
+
if (disabled) {
|
| 968 |
+
this.elements.sendBtn.innerHTML = 'Sending <span class="spinner"></span>';
|
| 969 |
+
} else {
|
| 970 |
+
this.elements.sendBtn.innerHTML = 'Send 📤';
|
| 971 |
+
}
|
| 972 |
+
},
|
| 973 |
+
|
| 974 |
+
resetChat() {
|
| 975 |
+
this.elements.messagesContainer.innerHTML = `
|
| 976 |
+
<div class="empty-state">
|
| 977 |
+
<img src="/static/ch.png" alt="Start conversation" onerror="this.style.display='none'">
|
| 978 |
+
<h2>New Conversation</h2>
|
| 979 |
+
<p>Ask any question to start</p>
|
| 980 |
+
</div>
|
| 981 |
+
`;
|
| 982 |
+
this.elements.messageInput.value = '';
|
| 983 |
+
state.messageIdCounter = 0;
|
| 984 |
+
},
|
| 985 |
+
|
| 986 |
+
toggleTheme() {
|
| 987 |
+
const body = document.body;
|
| 988 |
+
const isDark = body.classList.contains('dark-mode');
|
| 989 |
+
|
| 990 |
+
body.classList.remove('light-mode', 'dark-mode');
|
| 991 |
+
body.classList.add(isDark ? 'light-mode' : 'dark-mode');
|
| 992 |
+
|
| 993 |
+
utils.setStorageItem(CONFIG.STORAGE_KEYS.THEME, isDark ? 'light' : 'dark');
|
| 994 |
+
},
|
| 995 |
+
|
| 996 |
+
initTheme() {
|
| 997 |
+
const savedTheme = utils.getStorageItem(CONFIG.STORAGE_KEYS.THEME) || 'light';
|
| 998 |
+
document.body.classList.remove('light-mode', 'dark-mode');
|
| 999 |
+
document.body.classList.add(`${savedTheme}-mode`);
|
| 1000 |
+
},
|
| 1001 |
+
|
| 1002 |
+
async loadConversations() {
|
| 1003 |
+
try {
|
| 1004 |
+
const response = await api.getConversations();
|
| 1005 |
+
|
| 1006 |
+
if (response.ok) {
|
| 1007 |
+
const data = await response.json();
|
| 1008 |
+
this.renderConversations(data.conversations || []);
|
| 1009 |
+
} else {
|
| 1010 |
+
console.error('Failed to load conversations');
|
| 1011 |
+
}
|
| 1012 |
+
} catch (error) {
|
| 1013 |
+
console.error('Error loading conversations:', error);
|
| 1014 |
+
}
|
| 1015 |
+
},
|
| 1016 |
+
|
| 1017 |
+
renderConversations(conversations) {
|
| 1018 |
+
const list = this.elements.conversationsList;
|
| 1019 |
+
list.innerHTML = '';
|
| 1020 |
+
|
| 1021 |
+
if (!conversations || conversations.length === 0) {
|
| 1022 |
+
list.innerHTML = '<p style="text-align: center; opacity: 0.5; padding: 20px; font-size: 14px;"></p>';
|
| 1023 |
+
return;
|
| 1024 |
+
}
|
| 1025 |
+
|
| 1026 |
+
conversations.forEach(conv => {
|
| 1027 |
+
const convDiv = document.createElement('div');
|
| 1028 |
+
convDiv.className = `conversation-item ${conv.id === state.currentConversationId ? 'active' : ''}`;
|
| 1029 |
+
convDiv.dataset.conversationId = conv.id;
|
| 1030 |
+
|
| 1031 |
+
const titleSpan = document.createElement('span');
|
| 1032 |
+
titleSpan.className = 'conversation-title';
|
| 1033 |
+
titleSpan.textContent = conv.title || 'New Conversation';
|
| 1034 |
+
|
| 1035 |
+
const deleteBtn = document.createElement('button');
|
| 1036 |
+
deleteBtn.className = 'delete-conv-btn';
|
| 1037 |
+
deleteBtn.innerHTML = '🗑️';
|
| 1038 |
+
deleteBtn.title = 'Delete';
|
| 1039 |
+
deleteBtn.dataset.action = 'delete';
|
| 1040 |
+
deleteBtn.dataset.conversationId = conv.id;
|
| 1041 |
+
|
| 1042 |
+
convDiv.appendChild(titleSpan);
|
| 1043 |
+
convDiv.appendChild(deleteBtn);
|
| 1044 |
+
list.appendChild(convDiv);
|
| 1045 |
+
});
|
| 1046 |
+
},
|
| 1047 |
+
|
| 1048 |
+
async loadConversation(id) {
|
| 1049 |
+
try {
|
| 1050 |
+
const response = await api.getConversation(id);
|
| 1051 |
+
|
| 1052 |
+
if (response.ok) {
|
| 1053 |
+
const data = await response.json();
|
| 1054 |
+
state.currentConversationId = parseInt(id);
|
| 1055 |
+
|
| 1056 |
+
this.elements.messagesContainer.innerHTML = '';
|
| 1057 |
+
|
| 1058 |
+
if (data.messages && data.messages.length > 0) {
|
| 1059 |
+
data.messages.forEach(msg => {
|
| 1060 |
+
const isUser = msg.role === 'user' || msg.sender === 'user';
|
| 1061 |
+
|
| 1062 |
+
if (isUser) {
|
| 1063 |
+
const messageDiv = document.createElement('div');
|
| 1064 |
+
messageDiv.className = 'message user';
|
| 1065 |
+
messageDiv.innerHTML = `<div class='message-content'>${utils.escapeHtml(msg.content)}</div>`;
|
| 1066 |
+
this.elements.messagesContainer.appendChild(messageDiv);
|
| 1067 |
+
} else {
|
| 1068 |
+
const messageDiv = document.createElement('div');
|
| 1069 |
+
messageDiv.className = 'message ai';
|
| 1070 |
+
messageDiv.innerHTML = `
|
| 1071 |
+
<div class='message-content'>${utils.escapeHtml(msg.content)}</div>
|
| 1072 |
+
<div class='feedback-buttons'>
|
| 1073 |
+
<button type="button" class='feedback-btn copy-btn' data-action="copy" title='Copy to clipboard'>
|
| 1074 |
+
📋
|
| 1075 |
+
</button>
|
| 1076 |
+
|
| 1077 |
+
</div>
|
| 1078 |
+
`;
|
| 1079 |
+
this.elements.messagesContainer.appendChild(messageDiv);
|
| 1080 |
+
}
|
| 1081 |
+
});
|
| 1082 |
+
this.scrollToBottom();
|
| 1083 |
+
}
|
| 1084 |
+
|
| 1085 |
+
document.querySelectorAll('.conversation-item').forEach(item => {
|
| 1086 |
+
item.classList.remove('active');
|
| 1087 |
+
if (parseInt(item.dataset.conversationId) === parseInt(id)) {
|
| 1088 |
+
item.classList.add('active');
|
| 1089 |
+
}
|
| 1090 |
+
});
|
| 1091 |
+
|
| 1092 |
+
this.closeSidebar();
|
| 1093 |
+
} else {
|
| 1094 |
+
console.error('Failed to load conversation, status:', response.status);
|
| 1095 |
+
}
|
| 1096 |
+
} catch (error) {
|
| 1097 |
+
console.error('Error loading conversation:', error);
|
| 1098 |
+
}
|
| 1099 |
+
}
|
| 1100 |
+
};
|
| 1101 |
+
|
| 1102 |
+
// Chat functions
|
| 1103 |
+
const chat = {
|
| 1104 |
+
async sendMessage() {
|
| 1105 |
+
const message = ui.elements.messageInput.value.trim();
|
| 1106 |
+
|
| 1107 |
+
if (!message) return;
|
| 1108 |
+
|
| 1109 |
+
ui.setInputState(true);
|
| 1110 |
+
ui.addUserMessage(message);
|
| 1111 |
+
ui.elements.messageInput.value = '';
|
| 1112 |
+
|
| 1113 |
+
ui.showTypingIndicator();
|
| 1114 |
+
|
| 1115 |
+
try {
|
| 1116 |
+
const response = await api.sendMessage(message);
|
| 1117 |
+
|
| 1118 |
+
ui.hideTypingIndicator();
|
| 1119 |
+
|
| 1120 |
+
if (response.ok) {
|
| 1121 |
+
// Get IDs from headers
|
| 1122 |
+
const conversationId = response.headers.get("X-Conversation-Id");
|
| 1123 |
+
const messageId = response.headers.get("X-Message-Id");
|
| 1124 |
+
|
| 1125 |
+
if (conversationId) state.currentConversationId = parseInt(conversationId);
|
| 1126 |
+
|
| 1127 |
+
// Add empty AI message bubble
|
| 1128 |
+
const contentDiv = ui.addAIMessage("", messageId);
|
| 1129 |
+
|
| 1130 |
+
// Read the stream
|
| 1131 |
+
const reader = response.body.getReader();
|
| 1132 |
+
const decoder = new TextDecoder();
|
| 1133 |
+
|
| 1134 |
+
while (true) {
|
| 1135 |
+
const { done, value } = await reader.read();
|
| 1136 |
+
if (done) break;
|
| 1137 |
+
|
| 1138 |
+
const chunk = decoder.decode(value, { stream: true });
|
| 1139 |
+
contentDiv.textContent += chunk;
|
| 1140 |
+
ui.scrollToBottom();
|
| 1141 |
+
}
|
| 1142 |
+
|
| 1143 |
+
await ui.loadConversations();
|
| 1144 |
+
} else {
|
| 1145 |
+
state.messageIdCounter++;
|
| 1146 |
+
ui.addAIMessage("⚠️ Error getting response from server");
|
| 1147 |
+
}
|
| 1148 |
+
} catch (error) {
|
| 1149 |
+
console.error('Error sending message:', error);
|
| 1150 |
+
ui.hideTypingIndicator();
|
| 1151 |
+
state.messageIdCounter++;
|
| 1152 |
+
ui.addAIMessage("⚠️ Error connecting to server. Please try again.");
|
| 1153 |
+
} finally {
|
| 1154 |
+
ui.setInputState(false);
|
| 1155 |
+
ui.elements.messageInput.focus();
|
| 1156 |
+
}
|
| 1157 |
+
},
|
| 1158 |
+
|
| 1159 |
+
async copyMessage(button) {
|
| 1160 |
+
const messageDiv = button.closest('.message');
|
| 1161 |
+
const content = messageDiv.querySelector('.message-content').textContent;
|
| 1162 |
+
|
| 1163 |
+
try {
|
| 1164 |
+
await navigator.clipboard.writeText(content);
|
| 1165 |
+
|
| 1166 |
+
const originalText = button.textContent;
|
| 1167 |
+
button.textContent = '✅';
|
| 1168 |
+
|
| 1169 |
+
setTimeout(() => {
|
| 1170 |
+
button.textContent = originalText;
|
| 1171 |
+
}, 2000);
|
| 1172 |
+
} catch (error) {
|
| 1173 |
+
console.error('Failed to copy message:', error);
|
| 1174 |
+
button.textContent = '❌';
|
| 1175 |
+
setTimeout(() => {
|
| 1176 |
+
button.textContent = '📋';
|
| 1177 |
+
}, 2000);
|
| 1178 |
+
}
|
| 1179 |
+
},
|
| 1180 |
+
|
| 1181 |
+
async sendFeedback(button, messageId, feedbackType) {
|
| 1182 |
+
try {
|
| 1183 |
+
const response = await api.sendFeedback(messageId, feedbackType);
|
| 1184 |
+
|
| 1185 |
+
if (response.ok) {
|
| 1186 |
+
const feedbackButtons = button.closest('.feedback-buttons');
|
| 1187 |
+
const thumbsUp = feedbackButtons.querySelector('.thumbs-up');
|
| 1188 |
+
const thumbsDown = feedbackButtons.querySelector('.thumbs-down');
|
| 1189 |
+
|
| 1190 |
+
thumbsUp.classList.remove('active');
|
| 1191 |
+
thumbsDown.classList.remove('active');
|
| 1192 |
+
|
| 1193 |
+
if (feedbackType === 'positive') {
|
| 1194 |
+
thumbsUp.classList.add('active');
|
| 1195 |
+
} else {
|
| 1196 |
+
thumbsDown.classList.add('active');
|
| 1197 |
+
}
|
| 1198 |
+
} else {
|
| 1199 |
+
console.error('Failed to send feedback');
|
| 1200 |
+
}
|
| 1201 |
+
} catch (error) {
|
| 1202 |
+
console.error('Error sending feedback:', error);
|
| 1203 |
+
}
|
| 1204 |
+
},
|
| 1205 |
+
|
| 1206 |
+
async deleteConversation(id) {
|
| 1207 |
+
if (!confirm('Are you sure you want to delete this conversation?')) {
|
| 1208 |
+
return;
|
| 1209 |
+
}
|
| 1210 |
+
|
| 1211 |
+
try {
|
| 1212 |
+
const response = await api.deleteConversation(id);
|
| 1213 |
+
|
| 1214 |
+
if (response.ok) {
|
| 1215 |
+
if (state.currentConversationId === parseInt(id)) {
|
| 1216 |
+
ui.resetChat();
|
| 1217 |
+
state.currentConversationId = null;
|
| 1218 |
+
}
|
| 1219 |
+
await ui.loadConversations();
|
| 1220 |
+
} else {
|
| 1221 |
+
console.error('Failed to delete conversation');
|
| 1222 |
+
}
|
| 1223 |
+
} catch (error) {
|
| 1224 |
+
console.error('Error deleting conversation:', error);
|
| 1225 |
+
}
|
| 1226 |
+
}
|
| 1227 |
+
};
|
| 1228 |
+
|
| 1229 |
+
// Auth functions
|
| 1230 |
+
const auth = {
|
| 1231 |
+
async checkAuth() {
|
| 1232 |
+
const token = utils.getStorageItem(CONFIG.STORAGE_KEYS.TOKEN);
|
| 1233 |
+
const role = utils.getStorageItem(CONFIG.STORAGE_KEYS.ROLE);
|
| 1234 |
+
|
| 1235 |
+
if (!token) {
|
| 1236 |
+
this.redirectToLogin();
|
| 1237 |
+
return false;
|
| 1238 |
+
}
|
| 1239 |
+
|
| 1240 |
+
if (role === CONFIG.ROLES.STUDENT) {
|
| 1241 |
+
return true;
|
| 1242 |
+
}
|
| 1243 |
+
|
| 1244 |
+
try {
|
| 1245 |
+
const response = await api.checkAuth();
|
| 1246 |
+
|
| 1247 |
+
if (!response.ok) {
|
| 1248 |
+
console.error('Auth check failed with status:', response.status);
|
| 1249 |
+
this.redirectToLogin();
|
| 1250 |
+
return false;
|
| 1251 |
+
}
|
| 1252 |
+
|
| 1253 |
+
const userData = await response.json();
|
| 1254 |
+
if (userData.role !== CONFIG.ROLES.STUDENT) {
|
| 1255 |
+
console.error('User is not a student');
|
| 1256 |
+
this.redirectToLogin();
|
| 1257 |
+
return false;
|
| 1258 |
+
}
|
| 1259 |
+
|
| 1260 |
+
return true;
|
| 1261 |
+
} catch (error) {
|
| 1262 |
+
console.error('Auth check failed:', error);
|
| 1263 |
+
return false;
|
| 1264 |
+
}
|
| 1265 |
+
},
|
| 1266 |
+
|
| 1267 |
+
logout() {
|
| 1268 |
+
utils.clearStorage();
|
| 1269 |
+
window.location.href = 'login.html';
|
| 1270 |
+
},
|
| 1271 |
+
|
| 1272 |
+
redirectToLogin() {
|
| 1273 |
+
utils.clearStorage();
|
| 1274 |
+
window.location.href = 'login.html';
|
| 1275 |
+
}
|
| 1276 |
+
};
|
| 1277 |
+
|
| 1278 |
+
// Event handlers
|
| 1279 |
+
function setupEventListeners() {
|
| 1280 |
+
ui.elements.sendBtn.addEventListener('click', () => chat.sendMessage());
|
| 1281 |
+
|
| 1282 |
+
ui.elements.messageInput.addEventListener('keypress', (e) => {
|
| 1283 |
+
if (e.key === 'Enter') {
|
| 1284 |
+
chat.sendMessage();
|
| 1285 |
+
}
|
| 1286 |
+
});
|
| 1287 |
+
|
| 1288 |
+
ui.elements.newChatBtn.addEventListener('click', () => {
|
| 1289 |
+
ui.resetChat();
|
| 1290 |
+
state.currentConversationId = null;
|
| 1291 |
+
});
|
| 1292 |
+
|
| 1293 |
+
ui.elements.logoutBtn.addEventListener('click', () => auth.logout());
|
| 1294 |
+
|
| 1295 |
+
ui.elements.themeToggle.addEventListener('click', () => ui.toggleTheme());
|
| 1296 |
+
|
| 1297 |
+
ui.elements.messagesContainer.addEventListener('click', (e) => {
|
| 1298 |
+
const button = e.target.closest('.feedback-btn');
|
| 1299 |
+
if (!button) return;
|
| 1300 |
+
|
| 1301 |
+
e.preventDefault();
|
| 1302 |
+
e.stopPropagation();
|
| 1303 |
+
|
| 1304 |
+
const action = button.dataset.action;
|
| 1305 |
+
|
| 1306 |
+
if (action === 'copy') {
|
| 1307 |
+
chat.copyMessage(button);
|
| 1308 |
+
} else if (action === 'feedback') {
|
| 1309 |
+
const messageId = parseInt(button.dataset.messageId);
|
| 1310 |
+
const feedbackType = button.dataset.feedbackType;
|
| 1311 |
+
chat.sendFeedback(button, messageId, feedbackType);
|
| 1312 |
+
}
|
| 1313 |
+
});
|
| 1314 |
+
|
| 1315 |
+
ui.elements.conversationsList.addEventListener('click', async (e) => {
|
| 1316 |
+
const deleteBtn = e.target.closest('[data-action="delete"]');
|
| 1317 |
+
|
| 1318 |
+
if (deleteBtn) {
|
| 1319 |
+
e.preventDefault();
|
| 1320 |
+
e.stopPropagation();
|
| 1321 |
+
const id = deleteBtn.dataset.conversationId;
|
| 1322 |
+
await chat.deleteConversation(id);
|
| 1323 |
+
return;
|
| 1324 |
+
}
|
| 1325 |
+
|
| 1326 |
+
const conversationItem = e.target.closest('.conversation-item');
|
| 1327 |
+
if (conversationItem) {
|
| 1328 |
+
const id = conversationItem.dataset.conversationId;
|
| 1329 |
+
await ui.loadConversation(id);
|
| 1330 |
+
}
|
| 1331 |
+
});
|
| 1332 |
+
|
| 1333 |
+
ui.elements.menuToggle.addEventListener('click', () => ui.toggleSidebar());
|
| 1334 |
+
|
| 1335 |
+
ui.elements.sidebarOverlay.addEventListener('click', () => ui.toggleSidebar());
|
| 1336 |
+
}
|
| 1337 |
+
|
| 1338 |
+
// Initialize app
|
| 1339 |
+
async function init() {
|
| 1340 |
+
ui.initTheme();
|
| 1341 |
+
setupEventListeners();
|
| 1342 |
+
|
| 1343 |
+
const isAuthenticated = await auth.checkAuth();
|
| 1344 |
+
|
| 1345 |
+
if (isAuthenticated !== false) {
|
| 1346 |
+
ui.elements.messageInput.focus();
|
| 1347 |
+
await ui.loadConversations();
|
| 1348 |
+
|
| 1349 |
+
showWelcomeModal();
|
| 1350 |
+
}
|
| 1351 |
+
}
|
| 1352 |
+
|
| 1353 |
+
if (document.readyState === 'loading') {
|
| 1354 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 1355 |
+
} else {
|
| 1356 |
+
init();
|
| 1357 |
+
}
|
| 1358 |
+
</script>
|
| 1359 |
+
</body>
|
| 1360 |
+
</html>
|
chatbot.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dotenv import load_dotenv
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
load_dotenv()
|
| 5 |
+
|
| 6 |
+
QDRANT_URL = os.getenv("QDRANT_URL")
|
| 7 |
+
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
|
| 8 |
+
from langchain_ollama import OllamaLLM
|
| 9 |
+
|
| 10 |
+
def rag_answer(question, context):
|
| 11 |
+
prompt = f"""
|
| 12 |
+
You are an AI assistant. Use ONLY the context below to answer the user's question.
|
| 13 |
+
Reply in English only.
|
| 14 |
+
|
| 15 |
+
Context:
|
| 16 |
+
{context}
|
| 17 |
+
|
| 18 |
+
Question:
|
| 19 |
+
{question}
|
| 20 |
+
|
| 21 |
+
Answer:
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
llm = OllamaLLM(model="llama3:latest")
|
| 25 |
+
return llm.invoke(prompt)
|
| 26 |
+
|
| 27 |
+
if __name__ == "__main__":
|
| 28 |
+
user_q = input("Enter your question: ")
|
| 29 |
+
answer = rag_answer(user_q, "No context loaded here yet.")
|
| 30 |
+
print("\nAI:", answer)
|
chunk_text.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# scr/chunk_text.py
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
import os
|
| 4 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
QDRANT_URL = os.getenv("QDRANT_URL")
|
| 9 |
+
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
|
| 10 |
+
|
| 11 |
+
# المسارات
|
| 12 |
+
BASE_PATH = os.getcwd()
|
| 13 |
+
CLEAN_FOLDER = os.path.join(BASE_PATH, "data", "clean")
|
| 14 |
+
CHUNK_FOLDER = os.path.join(BASE_PATH, "data", "chunks")
|
| 15 |
+
|
| 16 |
+
# إنشاء فولدر chunks إذا ما موجود
|
| 17 |
+
os.makedirs(CHUNK_FOLDER, exist_ok=True)
|
| 18 |
+
|
| 19 |
+
# إعدادات التقطيع
|
| 20 |
+
text_splitter = RecursiveCharacterTextSplitter(
|
| 21 |
+
chunk_size=500,
|
| 22 |
+
chunk_overlap=50,
|
| 23 |
+
length_function=len
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# ✅ الدالة الجديدة - تستقبل text مباشرة
|
| 28 |
+
def chunk_text(text):
|
| 29 |
+
"""
|
| 30 |
+
تقسيم نص واحد إلى chunks
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
text (str): النص المراد تقسيمه
|
| 34 |
+
|
| 35 |
+
Returns:
|
| 36 |
+
list: قائمة بالـ chunks
|
| 37 |
+
"""
|
| 38 |
+
if not text or len(text.strip()) == 0:
|
| 39 |
+
return []
|
| 40 |
+
|
| 41 |
+
chunks = text_splitter.split_text(text)
|
| 42 |
+
return chunks
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# ✅ الدالة القديمة - للتوافق مع الكود القديم
|
| 46 |
+
def chunk_all_clean_files():
|
| 47 |
+
"""
|
| 48 |
+
تقسيم كل الملفات في مجلد clean
|
| 49 |
+
(الدالة القديمة - للـ backward compatibility)
|
| 50 |
+
"""
|
| 51 |
+
print("📌 Chunking files...\n")
|
| 52 |
+
|
| 53 |
+
for filename in os.listdir(CLEAN_FOLDER):
|
| 54 |
+
if filename.endswith(".txt"):
|
| 55 |
+
file_path = os.path.join(CLEAN_FOLDER, filename)
|
| 56 |
+
|
| 57 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 58 |
+
text = f.read()
|
| 59 |
+
|
| 60 |
+
chunks = text_splitter.split_text(text)
|
| 61 |
+
|
| 62 |
+
# حفظ الشنكات
|
| 63 |
+
output_path = os.path.join(CHUNK_FOLDER, filename)
|
| 64 |
+
with open(output_path, "w", encoding="utf-8") as f:
|
| 65 |
+
for chunk in chunks:
|
| 66 |
+
f.write(chunk + "\n---CHUNK---\n")
|
| 67 |
+
|
| 68 |
+
print(f"✔ تم تقطيع الملف: {filename} → {len(chunks)} chunks")
|
| 69 |
+
|
| 70 |
+
print("\n🎉 Done! All files chunked successfully.")
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
if __name__ == "__main__":
|
| 74 |
+
chunk_all_clean_files()
|
| 75 |
+
print("done")
|
clean_text.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dotenv import load_dotenv
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
load_dotenv()
|
| 5 |
+
|
| 6 |
+
QDRANT_URL = os.getenv("QDRANT_URL")
|
| 7 |
+
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
|
| 8 |
+
import os
|
| 9 |
+
import re
|
| 10 |
+
|
| 11 |
+
BASE_PATH = os.getcwd()
|
| 12 |
+
|
| 13 |
+
INPUT_FOLDER = os.path.join(BASE_PATH, "data", "processed")
|
| 14 |
+
OUTPUT_FOLDER = os.path.join(BASE_PATH, "data", "clean")
|
| 15 |
+
|
| 16 |
+
os.makedirs(OUTPUT_FOLDER, exist_ok=True)
|
| 17 |
+
|
| 18 |
+
def clean_text(text):
|
| 19 |
+
# Remove weird unicode characters
|
| 20 |
+
text = text.encode("utf-8", "ignore").decode("utf-8", "ignore")
|
| 21 |
+
|
| 22 |
+
# Remove multiple spaces
|
| 23 |
+
text = re.sub(r"\s+", " ", text)
|
| 24 |
+
|
| 25 |
+
# Remove lines with only symbols
|
| 26 |
+
text = re.sub(r"[^\w\s.,?!\-–—/]+", "", text)
|
| 27 |
+
|
| 28 |
+
# Remove extra newlines
|
| 29 |
+
text = re.sub(r"\n+", "\n", text)
|
| 30 |
+
|
| 31 |
+
return text.strip()
|
| 32 |
+
|
| 33 |
+
def clean_all_files():
|
| 34 |
+
print("Cleaning text files...")
|
| 35 |
+
print("Input:", INPUT_FOLDER)
|
| 36 |
+
print("Output:", OUTPUT_FOLDER)
|
| 37 |
+
|
| 38 |
+
for file in os.listdir(INPUT_FOLDER):
|
| 39 |
+
if file.endswith(".txt"):
|
| 40 |
+
in_path = os.path.join(INPUT_FOLDER, file)
|
| 41 |
+
out_path = os.path.join(OUTPUT_FOLDER, file)
|
| 42 |
+
|
| 43 |
+
with open(in_path, "r", encoding="utf-8", errors="ignore") as f:
|
| 44 |
+
raw = f.read()
|
| 45 |
+
|
| 46 |
+
cleaned = clean_text(raw)
|
| 47 |
+
|
| 48 |
+
with open(out_path, "w", encoding="utf-8") as f:
|
| 49 |
+
f.write(cleaned)
|
| 50 |
+
|
| 51 |
+
print("Cleaned:", file)
|
| 52 |
+
|
| 53 |
+
print("\n✨ Done! Text cleaned successfully.")
|
| 54 |
+
|
| 55 |
+
if __name__ == "__main__":
|
| 56 |
+
clean_all_files()
|
| 57 |
+
print("done")
|
co.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 4 |
+
|
| 5 |
+
from fastapi import FastAPI, HTTPException
|
| 6 |
+
from fastapi.responses import StreamingResponse
|
| 7 |
+
from pydantic import BaseModel
|
| 8 |
+
from rag import rag_answer, rag_answer_stream
|
| 9 |
+
from typing import Optional
|
| 10 |
+
|
| 11 |
+
app = FastAPI()
|
| 12 |
+
|
| 13 |
+
app.add_middleware(
|
| 14 |
+
CORSMiddleware,
|
| 15 |
+
allow_origins=["*"],
|
| 16 |
+
allow_credentials=True,
|
| 17 |
+
allow_methods=["*"],
|
| 18 |
+
allow_headers=["*"],
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
class Question(BaseModel):
|
| 22 |
+
question: str
|
| 23 |
+
conversation_id: Optional[int] = None
|
| 24 |
+
|
| 25 |
+
@app.post("/ask")
|
| 26 |
+
async def ask_question(data: Question):
|
| 27 |
+
"""
|
| 28 |
+
Handle question, get RAG answer, and return it.
|
| 29 |
+
"""
|
| 30 |
+
try:
|
| 31 |
+
# 1. Get answer from RAG
|
| 32 |
+
answer = rag_answer(data.question)
|
| 33 |
+
|
| 34 |
+
return {
|
| 35 |
+
"answer": answer,
|
| 36 |
+
"conversation_id": data.conversation_id
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
except Exception as e:
|
| 40 |
+
print(f"Error in ask_question: {e}")
|
| 41 |
+
return {
|
| 42 |
+
"answer": "Error processing your question.",
|
| 43 |
+
"conversation_id": data.conversation_id,
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
@app.post("/ask_stream")
|
| 47 |
+
async def ask_question_stream(data: Question):
|
| 48 |
+
"""Stream the answer from RAG."""
|
| 49 |
+
return StreamingResponse(rag_answer_stream(data.question), media_type="text/plain")
|
embedding.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dotenv import load_dotenv
|
| 2 |
+
import os
|
| 3 |
+
import re
|
| 4 |
+
import uuid
|
| 5 |
+
import time
|
| 6 |
+
from sentence_transformers import SentenceTransformer
|
| 7 |
+
from qdrant_client import QdrantClient
|
| 8 |
+
from qdrant_client.models import VectorParams, PointStruct
|
| 9 |
+
|
| 10 |
+
# === Load ENV ===
|
| 11 |
+
load_dotenv()
|
| 12 |
+
QDRANT_URL = os.getenv("QDRANT_URL")
|
| 13 |
+
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
|
| 14 |
+
|
| 15 |
+
# === Paths ===
|
| 16 |
+
BASE_PATH = os.getcwd()
|
| 17 |
+
CHUNKS_FOLDER = os.path.join(BASE_PATH, "data", "chunks")
|
| 18 |
+
COLLECTION_NAME = "student_materials"
|
| 19 |
+
|
| 20 |
+
# === Load embedding model ===
|
| 21 |
+
print("Loading E5-Large model...")
|
| 22 |
+
model = SentenceTransformer("intfloat/e5-large")
|
| 23 |
+
|
| 24 |
+
# === Connect to Qdrant ===
|
| 25 |
+
client = QdrantClient(
|
| 26 |
+
url=QDRANT_URL,
|
| 27 |
+
api_key=QDRANT_API_KEY,
|
| 28 |
+
timeout=60 # مهم عشان يمنع فصل الاتصال
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
from qdrant_client.models import Distance
|
| 32 |
+
|
| 33 |
+
if not client.collection_exists(COLLECTION_NAME):
|
| 34 |
+
client.create_collection(
|
| 35 |
+
collection_name=COLLECTION_NAME,
|
| 36 |
+
vectors_config=VectorParams(
|
| 37 |
+
size=1024,
|
| 38 |
+
distance=Distance.COSINE
|
| 39 |
+
)
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
# ======================================================
|
| 44 |
+
# Extract metadata from filename
|
| 45 |
+
# ======================================================
|
| 46 |
+
def extract_metadata(filename):
|
| 47 |
+
name = filename.replace(".txt", "")
|
| 48 |
+
match = re.search(r"(\d+)", name)
|
| 49 |
+
sheet_number = int(match.group(1)) if match else None
|
| 50 |
+
course_name = name[:match.start()].strip() if match else name
|
| 51 |
+
return course_name, sheet_number
|
| 52 |
+
|
| 53 |
+
# ======================================================
|
| 54 |
+
# Read chunks
|
| 55 |
+
# ======================================================
|
| 56 |
+
def read_chunks_from_file(path):
|
| 57 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 58 |
+
content = f.read()
|
| 59 |
+
|
| 60 |
+
raw_chunks = content.split("---CHUNK---")
|
| 61 |
+
cleaned_chunks = [c.strip() for c in raw_chunks if len(c.strip()) > 20]
|
| 62 |
+
return cleaned_chunks
|
| 63 |
+
|
| 64 |
+
# ======================================================
|
| 65 |
+
# Process single file (NEW - للملفات الجديدة)
|
| 66 |
+
# ======================================================
|
| 67 |
+
def embed_single_file(chunk_filename, batch_size=10, retry_times=5):
|
| 68 |
+
"""
|
| 69 |
+
معالجة ملف واحد محدد بدلاً من كل الملفات
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
chunk_filename: اسم الملف فقط (مثل: Mathematics1.txt)
|
| 73 |
+
batch_size: حجم الـ batch
|
| 74 |
+
retry_times: عدد المحاولات
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
dict: {'success': bool, 'total_chunks': int}
|
| 78 |
+
"""
|
| 79 |
+
filepath = os.path.join(CHUNKS_FOLDER, chunk_filename)
|
| 80 |
+
|
| 81 |
+
if not os.path.exists(filepath):
|
| 82 |
+
print(f"❌ File not found: {filepath}")
|
| 83 |
+
return {'success': False, 'total_chunks': 0}
|
| 84 |
+
|
| 85 |
+
chunks = read_chunks_from_file(filepath)
|
| 86 |
+
course_name, sheet_number = extract_metadata(chunk_filename)
|
| 87 |
+
|
| 88 |
+
print(f"\n📌 File: {chunk_filename} | Chunks: {len(chunks)}")
|
| 89 |
+
print(f" Course: {course_name} | Sheet: {sheet_number}")
|
| 90 |
+
|
| 91 |
+
uploaded_count = 0
|
| 92 |
+
|
| 93 |
+
# تقسيم إلى batches
|
| 94 |
+
for i in range(0, len(chunks), batch_size):
|
| 95 |
+
batch = chunks[i:i+batch_size]
|
| 96 |
+
|
| 97 |
+
# Embed
|
| 98 |
+
vectors = model.encode(batch).tolist()
|
| 99 |
+
|
| 100 |
+
# Prepare points
|
| 101 |
+
points = []
|
| 102 |
+
for vec, chunk in zip(vectors, batch):
|
| 103 |
+
points.append(
|
| 104 |
+
PointStruct(
|
| 105 |
+
id=str(uuid.uuid4()),
|
| 106 |
+
vector=vec,
|
| 107 |
+
payload={
|
| 108 |
+
"text": chunk,
|
| 109 |
+
"filename": chunk_filename,
|
| 110 |
+
"course": course_name,
|
| 111 |
+
"sheet_number": sheet_number
|
| 112 |
+
}
|
| 113 |
+
)
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
# Upsert with retry
|
| 117 |
+
for attempt in range(retry_times):
|
| 118 |
+
try:
|
| 119 |
+
client.upsert(
|
| 120 |
+
collection_name=COLLECTION_NAME,
|
| 121 |
+
points=points
|
| 122 |
+
)
|
| 123 |
+
uploaded_count += len(batch)
|
| 124 |
+
print(f" → Uploaded batch {i//batch_size + 1}")
|
| 125 |
+
break
|
| 126 |
+
|
| 127 |
+
except Exception as e:
|
| 128 |
+
print(f"⚠ خطأ في الاتصال! محاولة {attempt+1}/{retry_times}")
|
| 129 |
+
print(e)
|
| 130 |
+
time.sleep(3)
|
| 131 |
+
|
| 132 |
+
if attempt == retry_times - 1:
|
| 133 |
+
print("❌ فشل نهائي في رفع هذا batch")
|
| 134 |
+
return {'success': False, 'total_chunks': uploaded_count}
|
| 135 |
+
|
| 136 |
+
time.sleep(0.5)
|
| 137 |
+
|
| 138 |
+
print(f"\n🔥 Uploaded {uploaded_count} chunks successfully!")
|
| 139 |
+
|
| 140 |
+
return {'success': True, 'total_chunks': uploaded_count}
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
# ======================================================
|
| 144 |
+
# Batched embedding + retries (الدالة الأصلية لكل الملفات)
|
| 145 |
+
# ======================================================
|
| 146 |
+
def embed_chunks_and_upload(batch_size=10, retry_times=5):
|
| 147 |
+
files = [f for f in os.listdir(CHUNKS_FOLDER) if f.endswith(".txt")]
|
| 148 |
+
print(f"Found {len(files)} chunk files.\n")
|
| 149 |
+
|
| 150 |
+
for filename in files:
|
| 151 |
+
|
| 152 |
+
filepath = os.path.join(CHUNKS_FOLDER, filename)
|
| 153 |
+
chunks = read_chunks_from_file(filepath)
|
| 154 |
+
course_name, sheet_number = extract_metadata(filename)
|
| 155 |
+
|
| 156 |
+
print(f"\n📌 File: {filename} | Chunks: {len(chunks)}")
|
| 157 |
+
print(f" Course: {course_name} | Sheet: {sheet_number}")
|
| 158 |
+
|
| 159 |
+
# تقسيم الـ chunks إلى batches
|
| 160 |
+
for i in range(0, len(chunks), batch_size):
|
| 161 |
+
batch = chunks[i:i+batch_size]
|
| 162 |
+
|
| 163 |
+
# Embed batch
|
| 164 |
+
vectors = model.encode(batch).tolist()
|
| 165 |
+
|
| 166 |
+
# Prepare points
|
| 167 |
+
points = []
|
| 168 |
+
for vec, chunk in zip(vectors, batch):
|
| 169 |
+
points.append(
|
| 170 |
+
PointStruct(
|
| 171 |
+
id=str(uuid.uuid4()),
|
| 172 |
+
vector=vec,
|
| 173 |
+
payload={
|
| 174 |
+
"text": chunk,
|
| 175 |
+
"filename": filename,
|
| 176 |
+
"course": course_name,
|
| 177 |
+
"sheet_number": sheet_number
|
| 178 |
+
}
|
| 179 |
+
)
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
# Upsert with retry handling
|
| 183 |
+
for attempt in range(retry_times):
|
| 184 |
+
try:
|
| 185 |
+
client.upsert(
|
| 186 |
+
collection_name=COLLECTION_NAME,
|
| 187 |
+
points=points
|
| 188 |
+
)
|
| 189 |
+
print(f" → Uploaded batch {i//batch_size + 1}")
|
| 190 |
+
break
|
| 191 |
+
|
| 192 |
+
except Exception as e:
|
| 193 |
+
print(f"⚠ خطأ في الاتصال! محاولة {attempt+1}/{retry_times}")
|
| 194 |
+
print(e)
|
| 195 |
+
|
| 196 |
+
time.sleep(3)
|
| 197 |
+
|
| 198 |
+
if attempt == retry_times - 1:
|
| 199 |
+
print("❌ فشل نهائي في رفع هذا batch، بنتخطّاه...")
|
| 200 |
+
|
| 201 |
+
time.sleep(0.5) # منع الضغط على السيرفر
|
| 202 |
+
|
| 203 |
+
print("\n🔥 All chunks uploaded successfully with batching + retry!")
|
| 204 |
+
|
| 205 |
+
# ======================================================
|
| 206 |
+
# Run
|
| 207 |
+
# ======================================================
|
| 208 |
+
if __name__ == "__main__":
|
| 209 |
+
embed_chunks_and_upload()
|
| 210 |
+
print("\n🎉 Done!")
|
forgot-password.html
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--forget pass -->
|
| 2 |
+
<!DOCTYPE html>
|
| 3 |
+
<html lang="en" dir="ltr">
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Forget the password- University AI</title>
|
| 8 |
+
<style>
|
| 9 |
+
* {
|
| 10 |
+
margin: 0;
|
| 11 |
+
padding: 0;
|
| 12 |
+
box-sizing: border-box;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
:root {
|
| 16 |
+
--olive-light: #3A662A;
|
| 17 |
+
--bg-light: #FFFFFF;
|
| 18 |
+
--text-light: #2C2C2C;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
body {
|
| 22 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 23 |
+
min-height: 100vh;
|
| 24 |
+
display: flex;
|
| 25 |
+
align-items: center;
|
| 26 |
+
justify-content: center;
|
| 27 |
+
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
| 28 |
+
color: var(--text-light);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.container {
|
| 32 |
+
width: 100%;
|
| 33 |
+
max-width: 450px;
|
| 34 |
+
padding: 20px;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.card {
|
| 38 |
+
background: white;
|
| 39 |
+
padding: 40px;
|
| 40 |
+
border-radius: 15px;
|
| 41 |
+
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.header {
|
| 45 |
+
text-align: center;
|
| 46 |
+
margin-bottom: 30px;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.logo {
|
| 50 |
+
font-size: 48px;
|
| 51 |
+
margin-bottom: 10px;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
h1 {
|
| 55 |
+
font-size: 28px;
|
| 56 |
+
margin-bottom: 10px;
|
| 57 |
+
color: var(--olive-light);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.subtitle {
|
| 61 |
+
opacity: 0.7;
|
| 62 |
+
font-size: 14px;
|
| 63 |
+
line-height: 1.6;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.form-group {
|
| 67 |
+
margin-bottom: 20px;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
label {
|
| 71 |
+
display: block;
|
| 72 |
+
margin-bottom: 8px;
|
| 73 |
+
font-weight: 500;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
input {
|
| 77 |
+
width: 100%;
|
| 78 |
+
padding: 12px 15px;
|
| 79 |
+
border-radius: 8px;
|
| 80 |
+
border: 2px solid #e0e0e0;
|
| 81 |
+
font-size: 16px;
|
| 82 |
+
transition: all 0.3s ease;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
input:focus {
|
| 86 |
+
outline: none;
|
| 87 |
+
border-color: var(--olive-light);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.btn {
|
| 91 |
+
width: 100%;
|
| 92 |
+
padding: 14px;
|
| 93 |
+
border: none;
|
| 94 |
+
border-radius: 8px;
|
| 95 |
+
font-size: 16px;
|
| 96 |
+
font-weight: 600;
|
| 97 |
+
cursor: pointer;
|
| 98 |
+
transition: all 0.3s ease;
|
| 99 |
+
background: var(--olive-light);
|
| 100 |
+
color: white;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.btn:hover {
|
| 104 |
+
transform: translateY(-2px);
|
| 105 |
+
box-shadow: 0 5px 15px rgba(58, 102, 42, 0.4);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.btn:disabled {
|
| 109 |
+
opacity: 0.6;
|
| 110 |
+
cursor: not-allowed;
|
| 111 |
+
transform: none;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.alert {
|
| 115 |
+
padding: 12px;
|
| 116 |
+
border-radius: 8px;
|
| 117 |
+
margin-bottom: 20px;
|
| 118 |
+
display: none;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.alert.error {
|
| 122 |
+
background: #fee;
|
| 123 |
+
color: #c33;
|
| 124 |
+
border: 1px solid #fcc;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.alert.success {
|
| 128 |
+
background: #efe;
|
| 129 |
+
color: #3c3;
|
| 130 |
+
border: 1px solid #cfc;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.alert.show {
|
| 134 |
+
display: block;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.links {
|
| 138 |
+
text-align: center;
|
| 139 |
+
margin-top: 20px;
|
| 140 |
+
font-size: 14px;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.links a {
|
| 144 |
+
color: var(--olive-light);
|
| 145 |
+
text-decoration: none;
|
| 146 |
+
font-weight: 500;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.links a:hover {
|
| 150 |
+
text-decoration: underline;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.divider {
|
| 154 |
+
margin: 20px 0;
|
| 155 |
+
text-align: center;
|
| 156 |
+
opacity: 0.5;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.loading {
|
| 160 |
+
display: none;
|
| 161 |
+
text-align: center;
|
| 162 |
+
margin-top: 10px;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.loading.show {
|
| 166 |
+
display: block;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.back-home {
|
| 170 |
+
position: absolute;
|
| 171 |
+
top: 20px;
|
| 172 |
+
right: 20px;
|
| 173 |
+
padding: 10px 20px;
|
| 174 |
+
background: var(--olive-light);
|
| 175 |
+
color: white;
|
| 176 |
+
text-decoration: none;
|
| 177 |
+
border-radius: 8px;
|
| 178 |
+
font-size: 14px;
|
| 179 |
+
transition: all 0.3s ease;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.back-home:hover {
|
| 183 |
+
transform: translateY(-2px);
|
| 184 |
+
box-shadow: 0 5px 15px rgba(58, 102, 42, 0.4);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
@media (max-width: 768px) {
|
| 188 |
+
.back-home {
|
| 189 |
+
position: relative;
|
| 190 |
+
top: 0;
|
| 191 |
+
right: 0;
|
| 192 |
+
margin: 10px auto;
|
| 193 |
+
display: block;
|
| 194 |
+
text-align: center;
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
</style>
|
| 198 |
+
</head>
|
| 199 |
+
<body>
|
| 200 |
+
<a href="index.html" class="back-home">Home 🏠 </a>
|
| 201 |
+
|
| 202 |
+
<div class="container">
|
| 203 |
+
<div class="card">
|
| 204 |
+
<div class="header">
|
| 205 |
+
<div class="logo">🔑</div>
|
| 206 |
+
<h1>Forgot Password</h1>
|
| 207 |
+
<p class="subtitle">Don't worry! Enter your email and we will send you a code to reset your password
|
| 208 |
+
</p>
|
| 209 |
+
</div>
|
| 210 |
+
|
| 211 |
+
<div id="alert" class="alert"></div>
|
| 212 |
+
|
| 213 |
+
<!-- ✅ FIXED: Changed form to div -->
|
| 214 |
+
<div id="forgotForm">
|
| 215 |
+
<div class="form-group">
|
| 216 |
+
<label for="email">Email </label>
|
| 217 |
+
<input
|
| 218 |
+
type="email"
|
| 219 |
+
id="email"
|
| 220 |
+
name="email"
|
| 221 |
+
required
|
| 222 |
+
placeholder="example@university.edu"
|
| 223 |
+
>
|
| 224 |
+
</div>
|
| 225 |
+
|
| 226 |
+
<!-- ✅ FIXED: Changed type to button and added onclick -->
|
| 227 |
+
<button type="button" class="btn" id="sendBtn" onclick="handleForgot()">
|
| 228 |
+
Send Reset Code
|
| 229 |
+
</button>
|
| 230 |
+
|
| 231 |
+
<div class="loading" id="loading">
|
| 232 |
+
<p>Sending...</p>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
|
| 236 |
+
<div class="divider">───────</div>
|
| 237 |
+
|
| 238 |
+
<div class="links">
|
| 239 |
+
<p>Remember your passwprd ? <a href="login.html">Login </a></p>
|
| 240 |
+
<p style="margin-top: 10px;">
|
| 241 |
+
Don't have account ?<a href="register.html">Create account</a>
|
| 242 |
+
</p>
|
| 243 |
+
</div>
|
| 244 |
+
</div>
|
| 245 |
+
</div>
|
| 246 |
+
|
| 247 |
+
<script>
|
| 248 |
+
const API_URL = '';
|
| 249 |
+
|
| 250 |
+
// عرض رسالة
|
| 251 |
+
function showAlert(message, type = 'success') {
|
| 252 |
+
const alert = document.getElementById('alert');
|
| 253 |
+
alert.textContent = message;
|
| 254 |
+
alert.className = `alert ${type} show`;
|
| 255 |
+
|
| 256 |
+
setTimeout(() => {
|
| 257 |
+
alert.classList.remove('show');
|
| 258 |
+
}, 8000);
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
// معالجة الطلب
|
| 262 |
+
// ✅ FIXED: Replaced event listener with direct function
|
| 263 |
+
async function handleForgot() {
|
| 264 |
+
|
| 265 |
+
const email = document.getElementById('email').value;
|
| 266 |
+
const sendBtn = document.getElementById('sendBtn');
|
| 267 |
+
const loading = document.getElementById('loading');
|
| 268 |
+
|
| 269 |
+
sendBtn.disabled = true;
|
| 270 |
+
loading.classList.add('show');
|
| 271 |
+
|
| 272 |
+
try {
|
| 273 |
+
const response = await fetch(`${API_URL}/auth/forgot-password`, {
|
| 274 |
+
method: 'POST',
|
| 275 |
+
headers: {
|
| 276 |
+
'Content-Type': 'application/json',
|
| 277 |
+
},
|
| 278 |
+
body: JSON.stringify({
|
| 279 |
+
email: email
|
| 280 |
+
})
|
| 281 |
+
});
|
| 282 |
+
|
| 283 |
+
const data = await response.json();
|
| 284 |
+
|
| 285 |
+
if (response.ok) {
|
| 286 |
+
showAlert(
|
| 287 |
+
'✅ If the email is registered, a code has been sent. Redirecting...',
|
| 288 |
+
'success'
|
| 289 |
+
);
|
| 290 |
+
setTimeout(() => {
|
| 291 |
+
window.location.href = `reset-password.html?email=${encodeURIComponent(email)}`;
|
| 292 |
+
}, 1500);
|
| 293 |
+
} else {
|
| 294 |
+
// To prevent email enumeration, we show a success message even on failure.
|
| 295 |
+
showAlert('✅ If the email is registered, a code has been sent. Redirecting...', 'success');
|
| 296 |
+
setTimeout(() => {
|
| 297 |
+
window.location.href = `reset-password.html?email=${encodeURIComponent(email)}`;
|
| 298 |
+
}, 1500);
|
| 299 |
+
}
|
| 300 |
+
} catch (error) {
|
| 301 |
+
console.error('Forgot password error:', error);
|
| 302 |
+
showAlert('There was an error connecting to the server. Please try again..', 'error');
|
| 303 |
+
} finally {
|
| 304 |
+
sendBtn.disabled = false;
|
| 305 |
+
loading.classList.remove('show');
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
</script>
|
| 309 |
+
</body>
|
| 310 |
+
</html>
|
index.html
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" dir="ltr">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>University AI Chatbot - Home</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
:root {
|
| 15 |
+
--olive-light: #3A662A;
|
| 16 |
+
--olive-dark: #5C6E4A;
|
| 17 |
+
--bg-light: #FFFFFF;
|
| 18 |
+
--bg-dark: #1A1A1A;
|
| 19 |
+
--text-light: #2C2C2C;
|
| 20 |
+
--text-dark: #F5F5F5;
|
| 21 |
+
--card-light: #F8F9FA;
|
| 22 |
+
--card-dark: #2D2D2D;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
body {
|
| 26 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 27 |
+
transition: all 0.3s ease;
|
| 28 |
+
min-height: 100vh;
|
| 29 |
+
display: flex;
|
| 30 |
+
flex-direction: column;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
body.light-mode {
|
| 34 |
+
background: var(--bg-light);
|
| 35 |
+
color: var(--text-light);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
body.dark-mode {
|
| 39 |
+
background: var(--bg-dark);
|
| 40 |
+
color: var(--text-dark);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/* Header */
|
| 44 |
+
.header {
|
| 45 |
+
padding: 20px 50px;
|
| 46 |
+
display: flex;
|
| 47 |
+
justify-content: space-between;
|
| 48 |
+
align-items: center;
|
| 49 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 50 |
+
position: relative;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.light-mode .header {
|
| 54 |
+
background: var(--bg-light);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.dark-mode .header {
|
| 58 |
+
background: var(--card-dark);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.logo {
|
| 62 |
+
font-size: 24px;
|
| 63 |
+
font-weight: bold;
|
| 64 |
+
color: var(--olive-light);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.header-buttons {
|
| 68 |
+
display: flex;
|
| 69 |
+
gap: 15px;
|
| 70 |
+
align-items: center;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.btn {
|
| 74 |
+
padding: 10px 25px;
|
| 75 |
+
border: none;
|
| 76 |
+
border-radius: 8px;
|
| 77 |
+
cursor: pointer;
|
| 78 |
+
font-size: 16px;
|
| 79 |
+
transition: all 0.3s ease;
|
| 80 |
+
text-decoration: none;
|
| 81 |
+
display: inline-block;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.btn-primary {
|
| 85 |
+
background: var(--olive-light);
|
| 86 |
+
color: white;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.btn-primary:hover {
|
| 90 |
+
transform: translateY(-2px);
|
| 91 |
+
box-shadow: 0 5px 15px rgba(156, 175, 136, 0.4);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.btn-secondary {
|
| 95 |
+
background: transparent;
|
| 96 |
+
border: 2px solid var(--olive-light);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.light-mode .btn-secondary {
|
| 100 |
+
color: var(--text-light);
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.dark-mode .btn-secondary {
|
| 104 |
+
color: var(--text-dark);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.btn-secondary:hover {
|
| 108 |
+
background: var(--olive-light);
|
| 109 |
+
color: white;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
/* Theme Toggle - Fixed Position */
|
| 113 |
+
.theme-toggle {
|
| 114 |
+
width: 50px;
|
| 115 |
+
height: 26px;
|
| 116 |
+
background: var(--olive-light);
|
| 117 |
+
border-radius: 13px;
|
| 118 |
+
position: relative;
|
| 119 |
+
cursor: pointer;
|
| 120 |
+
transition: all 0.3s ease;
|
| 121 |
+
flex-shrink: 0;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.theme-toggle::after {
|
| 125 |
+
content: '☀️';
|
| 126 |
+
position: absolute;
|
| 127 |
+
top: 3px;
|
| 128 |
+
left: 3px;
|
| 129 |
+
width: 20px;
|
| 130 |
+
height: 20px;
|
| 131 |
+
background: white;
|
| 132 |
+
border-radius: 50%;
|
| 133 |
+
transition: all 0.3s ease;
|
| 134 |
+
display: flex;
|
| 135 |
+
align-items: center;
|
| 136 |
+
justify-content: center;
|
| 137 |
+
font-size: 12px;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.dark-mode .theme-toggle::after {
|
| 141 |
+
content: '🌙';
|
| 142 |
+
left: 27px;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/* Hero Section */
|
| 146 |
+
.hero {
|
| 147 |
+
flex: 1;
|
| 148 |
+
display: flex;
|
| 149 |
+
align-items: center;
|
| 150 |
+
justify-content: center;
|
| 151 |
+
padding: 80px 50px;
|
| 152 |
+
text-align: center;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.hero-content h1 {
|
| 156 |
+
font-size: 48px;
|
| 157 |
+
margin-bottom: 20px;
|
| 158 |
+
background: linear-gradient(135deg, var(--olive-light), var(--olive-dark));
|
| 159 |
+
background-clip: text;
|
| 160 |
+
-webkit-background-clip: text;
|
| 161 |
+
color: transparent;
|
| 162 |
+
-webkit-text-fill-color: transparent;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.hero-content p {
|
| 166 |
+
font-size: 20px;
|
| 167 |
+
margin-bottom: 40px;
|
| 168 |
+
opacity: 0.8;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.cta-buttons {
|
| 172 |
+
display: flex;
|
| 173 |
+
gap: 20px;
|
| 174 |
+
justify-content: center;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
/* Features */
|
| 178 |
+
.features {
|
| 179 |
+
padding: 80px 50px;
|
| 180 |
+
display: grid;
|
| 181 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 182 |
+
gap: 30px;
|
| 183 |
+
max-width: 1200px;
|
| 184 |
+
margin: 0 auto;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.feature-card {
|
| 188 |
+
padding: 30px;
|
| 189 |
+
border-radius: 12px;
|
| 190 |
+
text-align: center;
|
| 191 |
+
transition: all 0.3s ease;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.light-mode .feature-card {
|
| 195 |
+
background: var(--card-light);
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.dark-mode .feature-card {
|
| 199 |
+
background: var(--card-dark);
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.feature-card:hover {
|
| 203 |
+
transform: translateY(-5px);
|
| 204 |
+
box-shadow: 0 10px 30px rgba(156, 175, 136, 0.3);
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.feature-icon {
|
| 208 |
+
font-size: 48px;
|
| 209 |
+
margin-bottom: 20px;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.feature-card h3 {
|
| 213 |
+
font-size: 22px;
|
| 214 |
+
margin-bottom: 15px;
|
| 215 |
+
color: var(--olive-light);
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
/* Footer */
|
| 219 |
+
.footer {
|
| 220 |
+
padding: 30px 50px;
|
| 221 |
+
text-align: center;
|
| 222 |
+
border-top: 1px solid var(--olive-light);
|
| 223 |
+
margin-top: 50px;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
/* Responsive */
|
| 227 |
+
@media (max-width: 768px) {
|
| 228 |
+
.header {
|
| 229 |
+
padding: 15px 20px;
|
| 230 |
+
flex-wrap: wrap;
|
| 231 |
+
gap: 15px;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.logo {
|
| 235 |
+
order: 1;
|
| 236 |
+
flex: 1;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.theme-toggle {
|
| 240 |
+
order: 2;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.header-buttons {
|
| 244 |
+
order: 3;
|
| 245 |
+
width: 100%;
|
| 246 |
+
justify-content: center;
|
| 247 |
+
flex-wrap: wrap;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.hero {
|
| 251 |
+
padding: 40px 20px;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.hero-content h1 {
|
| 255 |
+
font-size: 28px;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.hero-content p {
|
| 259 |
+
font-size: 16px;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.cta-buttons {
|
| 263 |
+
flex-direction: column;
|
| 264 |
+
align-items: center;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.features {
|
| 268 |
+
padding: 40px 20px;
|
| 269 |
+
grid-template-columns: 1fr;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.footer {
|
| 273 |
+
padding: 20px;
|
| 274 |
+
}
|
| 275 |
+
}
|
| 276 |
+
</style>
|
| 277 |
+
</head>
|
| 278 |
+
<body class="light-mode">
|
| 279 |
+
<!-- Header -->
|
| 280 |
+
<div class="header">
|
| 281 |
+
<div class="logo">🎓 University AI</div>
|
| 282 |
+
<div class="theme-toggle" onclick="toggleTheme()"></div>
|
| 283 |
+
<div class="header-buttons">
|
| 284 |
+
<a href="login.html" class="btn btn-secondary">Login</a>
|
| 285 |
+
<a href="register.html" class="btn btn-primary">Create Account</a>
|
| 286 |
+
</div>
|
| 287 |
+
</div>
|
| 288 |
+
|
| 289 |
+
<!-- Hero Section -->
|
| 290 |
+
<div class="hero">
|
| 291 |
+
<div class="hero-content">
|
| 292 |
+
<h1>Your Smart Assistant in Your University Journey</h1>
|
| 293 |
+
<p>Learn, research, and organize your academic information with advanced AI</p>
|
| 294 |
+
<div class="cta-buttons">
|
| 295 |
+
<a href="register.html" class="btn btn-primary">Start Now</a>
|
| 296 |
+
<a href="#features" class="btn btn-secondary">Explore Features</a>
|
| 297 |
+
</div>
|
| 298 |
+
</div>
|
| 299 |
+
</div>
|
| 300 |
+
|
| 301 |
+
<!-- Features Section -->
|
| 302 |
+
<div class="features" id="features">
|
| 303 |
+
<div class="feature-card">
|
| 304 |
+
<div class="feature-icon">💬</div>
|
| 305 |
+
<h3>Smart Conversations</h3>
|
| 306 |
+
<p>Get instant answers to your academic questions powered by AI</p>
|
| 307 |
+
</div>
|
| 308 |
+
<div class="feature-card">
|
| 309 |
+
<div class="feature-icon">📚</div>
|
| 310 |
+
<h3>Organized Lectures</h3>
|
| 311 |
+
<p>Access all your lectures and study materials in one place</p>
|
| 312 |
+
</div>
|
| 313 |
+
<div class="feature-card">
|
| 314 |
+
<div class="feature-icon">🔍</div>
|
| 315 |
+
<h3>Advanced Search</h3>
|
| 316 |
+
<p>Search your notes and lectures easily and quickly</p>
|
| 317 |
+
</div>
|
| 318 |
+
<div class="feature-card">
|
| 319 |
+
<div class="feature-icon">📁</div>
|
| 320 |
+
<h3>File Management</h3>
|
| 321 |
+
<p>Upload and organize your study files professionally</p>
|
| 322 |
+
</div>
|
| 323 |
+
</div>
|
| 324 |
+
|
| 325 |
+
<!-- Footer -->
|
| 326 |
+
<div class="footer">
|
| 327 |
+
<p>© 2025 University AI Chatbot. All rights reserved.</p>
|
| 328 |
+
</div>
|
| 329 |
+
|
| 330 |
+
<script>
|
| 331 |
+
// Theme Toggle
|
| 332 |
+
function toggleTheme() {
|
| 333 |
+
const body = document.body;
|
| 334 |
+
if (body.classList.contains('light-mode')) {
|
| 335 |
+
body.classList.remove('light-mode');
|
| 336 |
+
body.classList.add('dark-mode');
|
| 337 |
+
localStorage.setItem('theme', 'dark');
|
| 338 |
+
} else {
|
| 339 |
+
body.classList.remove('dark-mode');
|
| 340 |
+
body.classList.add('light-mode');
|
| 341 |
+
localStorage.setItem('theme', 'light');
|
| 342 |
+
}
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
// Load saved theme
|
| 346 |
+
document.addEventListener("DOMContentLoaded", () => {
|
| 347 |
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
| 348 |
+
document.body.classList.remove("light-mode", "dark-mode");
|
| 349 |
+
document.body.classList.add(savedTheme + "-mode");
|
| 350 |
+
});
|
| 351 |
+
|
| 352 |
+
// Smooth scroll
|
| 353 |
+
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
| 354 |
+
anchor.addEventListener('click', function (e) {
|
| 355 |
+
e.preventDefault();
|
| 356 |
+
const target = document.querySelector(this.getAttribute('href'));
|
| 357 |
+
if (target) {
|
| 358 |
+
target.scrollIntoView({ behavior: 'smooth' });
|
| 359 |
+
}
|
| 360 |
+
});
|
| 361 |
+
});
|
| 362 |
+
</script>
|
| 363 |
+
</body>
|
| 364 |
+
</html>
|
index_lectures.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dotenv import load_dotenv
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
load_dotenv()
|
| 5 |
+
|
| 6 |
+
QDRANT_URL = os.getenv("QDRANT_URL")
|
| 7 |
+
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
|
ingest.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dotenv import load_dotenv
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
load_dotenv()
|
| 5 |
+
|
| 6 |
+
QDRANT_URL = os.getenv("QDRANT_URL")
|
| 7 |
+
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
|
login.html
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" dir="ltr">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Login - University AI</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
:root {
|
| 15 |
+
--olive-light: #3A662A;
|
| 16 |
+
--olive-dark: #5C6E4A;
|
| 17 |
+
--bg-light: #FFFFFF;
|
| 18 |
+
--bg-dark: #1A1A1A;
|
| 19 |
+
--text-light: #2C2C2C;
|
| 20 |
+
--text-dark: #F5F5F5;
|
| 21 |
+
--card-light: #F8F9FA;
|
| 22 |
+
--card-dark: #2D2D2D;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
body {
|
| 26 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 27 |
+
min-height: 100vh;
|
| 28 |
+
display: flex;
|
| 29 |
+
align-items: center;
|
| 30 |
+
justify-content: center;
|
| 31 |
+
transition: all 0.3s ease;
|
| 32 |
+
padding: 20px;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
body.light-mode {
|
| 36 |
+
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
| 37 |
+
color: var(--text-light);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
body.dark-mode {
|
| 41 |
+
background: linear-gradient(135deg, #1a1a1a 0%, #2d3748 100%);
|
| 42 |
+
color: var(--text-dark);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.container {
|
| 46 |
+
width: 100%;
|
| 47 |
+
max-width: 450px;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.card {
|
| 51 |
+
padding: 40px;
|
| 52 |
+
border-radius: 15px;
|
| 53 |
+
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
| 54 |
+
transition: all 0.3s ease;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.light-mode .card {
|
| 58 |
+
background: var(--bg-light);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.dark-mode .card {
|
| 62 |
+
background: var(--card-dark);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.header {
|
| 66 |
+
text-align: center;
|
| 67 |
+
margin-bottom: 30px;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.logo {
|
| 71 |
+
font-size: 48px;
|
| 72 |
+
margin-bottom: 10px;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
h1 {
|
| 76 |
+
font-size: 28px;
|
| 77 |
+
margin-bottom: 10px;
|
| 78 |
+
color: var(--olive-light);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.subtitle {
|
| 82 |
+
opacity: 0.7;
|
| 83 |
+
font-size: 14px;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.form-group {
|
| 87 |
+
margin-bottom: 20px;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
label {
|
| 91 |
+
display: block;
|
| 92 |
+
margin-bottom: 8px;
|
| 93 |
+
font-weight: 500;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
input {
|
| 97 |
+
width: 100%;
|
| 98 |
+
padding: 12px 15px;
|
| 99 |
+
border-radius: 8px;
|
| 100 |
+
border: 2px solid transparent;
|
| 101 |
+
font-size: 16px;
|
| 102 |
+
transition: all 0.3s ease;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.light-mode input {
|
| 106 |
+
background: var(--card-light);
|
| 107 |
+
color: var(--text-light);
|
| 108 |
+
border-color: #e0e0e0;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.dark-mode input {
|
| 112 |
+
background: var(--bg-dark);
|
| 113 |
+
color: var(--text-dark);
|
| 114 |
+
border-color: #444;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
input:focus {
|
| 118 |
+
outline: none;
|
| 119 |
+
border-color: var(--olive-light);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.btn {
|
| 123 |
+
width: 100%;
|
| 124 |
+
padding: 14px;
|
| 125 |
+
border: none;
|
| 126 |
+
border-radius: 8px;
|
| 127 |
+
font-size: 16px;
|
| 128 |
+
font-weight: 600;
|
| 129 |
+
cursor: pointer;
|
| 130 |
+
transition: all 0.3s ease;
|
| 131 |
+
background: var(--olive-light);
|
| 132 |
+
color: white;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.btn:hover {
|
| 136 |
+
transform: translateY(-2px);
|
| 137 |
+
box-shadow: 0 5px 15px rgba(58, 102, 42, 0.4);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.btn:disabled {
|
| 141 |
+
opacity: 0.6;
|
| 142 |
+
cursor: not-allowed;
|
| 143 |
+
transform: none;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.links {
|
| 147 |
+
text-align: center;
|
| 148 |
+
margin-top: 20px;
|
| 149 |
+
font-size: 14px;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.links a {
|
| 153 |
+
color: var(--olive-light);
|
| 154 |
+
text-decoration: none;
|
| 155 |
+
font-weight: 500;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.links a:hover {
|
| 159 |
+
text-decoration: underline;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.divider {
|
| 163 |
+
margin: 20px 0;
|
| 164 |
+
text-align: center;
|
| 165 |
+
opacity: 0.5;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.alert {
|
| 169 |
+
padding: 12px;
|
| 170 |
+
border-radius: 8px;
|
| 171 |
+
margin-bottom: 20px;
|
| 172 |
+
display: none;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.alert.error {
|
| 176 |
+
background: #fee;
|
| 177 |
+
color: #c33;
|
| 178 |
+
border: 1px solid #fcc;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.alert.success {
|
| 182 |
+
background: #efe;
|
| 183 |
+
color: #3c3;
|
| 184 |
+
border: 1px solid #cfc;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.alert.show {
|
| 188 |
+
display: block;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
/* Top Bar with Theme Toggle */
|
| 192 |
+
.top-bar {
|
| 193 |
+
position: fixed;
|
| 194 |
+
top: 0;
|
| 195 |
+
left: 0;
|
| 196 |
+
right: 0;
|
| 197 |
+
padding: 15px 20px;
|
| 198 |
+
display: flex;
|
| 199 |
+
justify-content: space-between;
|
| 200 |
+
align-items: center;
|
| 201 |
+
z-index: 1000;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.light-mode .top-bar {
|
| 205 |
+
background: rgba(255,255,255,0.9);
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.dark-mode .top-bar {
|
| 209 |
+
background: rgba(45,45,45,0.9);
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.theme-toggle {
|
| 213 |
+
width: 50px;
|
| 214 |
+
height: 26px;
|
| 215 |
+
background: var(--olive-light);
|
| 216 |
+
border-radius: 13px;
|
| 217 |
+
position: relative;
|
| 218 |
+
cursor: pointer;
|
| 219 |
+
transition: all 0.3s ease;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.theme-toggle::after {
|
| 223 |
+
content: '☀️';
|
| 224 |
+
position: absolute;
|
| 225 |
+
top: 3px;
|
| 226 |
+
left: 3px;
|
| 227 |
+
width: 20px;
|
| 228 |
+
height: 20px;
|
| 229 |
+
background: white;
|
| 230 |
+
border-radius: 50%;
|
| 231 |
+
transition: all 0.3s ease;
|
| 232 |
+
display: flex;
|
| 233 |
+
align-items: center;
|
| 234 |
+
justify-content: center;
|
| 235 |
+
font-size: 12px;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.dark-mode .theme-toggle::after {
|
| 239 |
+
content: '🌙';
|
| 240 |
+
left: 27px;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.back-home {
|
| 244 |
+
padding: 10px 20px;
|
| 245 |
+
background: var(--olive-light);
|
| 246 |
+
color: white;
|
| 247 |
+
text-decoration: none;
|
| 248 |
+
border-radius: 8px;
|
| 249 |
+
font-size: 14px;
|
| 250 |
+
transition: all 0.3s ease;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.back-home:hover {
|
| 254 |
+
transform: translateY(-2px);
|
| 255 |
+
box-shadow: 0 5px 15px rgba(58, 102, 42, 0.4);
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.loading {
|
| 259 |
+
display: none;
|
| 260 |
+
text-align: center;
|
| 261 |
+
margin-top: 10px;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.loading.show {
|
| 265 |
+
display: block;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
@media (max-width: 768px) {
|
| 269 |
+
body {
|
| 270 |
+
padding-top: 80px;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
.card {
|
| 274 |
+
padding: 25px;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.top-bar {
|
| 278 |
+
padding: 10px 15px;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.back-home {
|
| 282 |
+
padding: 8px 15px;
|
| 283 |
+
font-size: 12px;
|
| 284 |
+
}
|
| 285 |
+
}
|
| 286 |
+
</style>
|
| 287 |
+
</head>
|
| 288 |
+
<body class="light-mode">
|
| 289 |
+
<!-- Top Bar -->
|
| 290 |
+
<div class="top-bar">
|
| 291 |
+
<a href="index.html" class="back-home">🏠 Home</a>
|
| 292 |
+
<div class="theme-toggle" onclick="toggleTheme()"></div>
|
| 293 |
+
</div>
|
| 294 |
+
|
| 295 |
+
<div class="container">
|
| 296 |
+
<div class="card">
|
| 297 |
+
<div class="header">
|
| 298 |
+
<div class="logo">🎓</div>
|
| 299 |
+
<h1>Login</h1>
|
| 300 |
+
<p class="subtitle">Welcome back! Sign in to continue</p>
|
| 301 |
+
</div>
|
| 302 |
+
|
| 303 |
+
<div id="alert" class="alert"></div>
|
| 304 |
+
|
| 305 |
+
<!-- ✅ FIXED: Changed form to div to prevent race conditions -->
|
| 306 |
+
<div id="loginForm">
|
| 307 |
+
<div class="form-group">
|
| 308 |
+
<label for="email">Email Address</label>
|
| 309 |
+
<input
|
| 310 |
+
type="email"
|
| 311 |
+
id="email"
|
| 312 |
+
name="email"
|
| 313 |
+
required
|
| 314 |
+
placeholder="example@university.edu"
|
| 315 |
+
>
|
| 316 |
+
</div>
|
| 317 |
+
|
| 318 |
+
<div class="form-group">
|
| 319 |
+
<label for="password">Password</label>
|
| 320 |
+
<input
|
| 321 |
+
type="password"
|
| 322 |
+
id="password"
|
| 323 |
+
name="password"
|
| 324 |
+
required
|
| 325 |
+
placeholder="••••••••"
|
| 326 |
+
>
|
| 327 |
+
</div>
|
| 328 |
+
|
| 329 |
+
<!-- ✅ FIXED: Changed type to button and added onclick -->
|
| 330 |
+
<button type="button" class="btn" id="loginBtn" onclick="handleLogin()">
|
| 331 |
+
Login
|
| 332 |
+
</button>
|
| 333 |
+
|
| 334 |
+
<div class="loading" id="loading">
|
| 335 |
+
<p>Signing in...</p>
|
| 336 |
+
</div>
|
| 337 |
+
</div>
|
| 338 |
+
|
| 339 |
+
<div class="divider">───────</div>
|
| 340 |
+
|
| 341 |
+
<div class="links">
|
| 342 |
+
<p>Don't have an account? <a href="register.html">Create Account</a></p>
|
| 343 |
+
<p style="margin-top: 10px;">
|
| 344 |
+
<a href="forgot-password.html">Forgot Password?</a>
|
| 345 |
+
</p>
|
| 346 |
+
</div>
|
| 347 |
+
</div>
|
| 348 |
+
</div>
|
| 349 |
+
|
| 350 |
+
<script>
|
| 351 |
+
const API_URL = '';
|
| 352 |
+
|
| 353 |
+
// Theme Toggle
|
| 354 |
+
function toggleTheme() {
|
| 355 |
+
const body = document.body;
|
| 356 |
+
if (body.classList.contains('light-mode')) {
|
| 357 |
+
body.classList.remove('light-mode');
|
| 358 |
+
body.classList.add('dark-mode');
|
| 359 |
+
localStorage.setItem('theme', 'dark');
|
| 360 |
+
} else {
|
| 361 |
+
body.classList.remove('dark-mode');
|
| 362 |
+
body.classList.add('light-mode');
|
| 363 |
+
localStorage.setItem('theme', 'light');
|
| 364 |
+
}
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
// Load saved theme
|
| 368 |
+
document.addEventListener("DOMContentLoaded", () => {
|
| 369 |
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
| 370 |
+
document.body.classList.remove("light-mode", "dark-mode");
|
| 371 |
+
document.body.classList.add(savedTheme + "-mode");
|
| 372 |
+
});
|
| 373 |
+
|
| 374 |
+
// Show alert message
|
| 375 |
+
function showAlert(message, type = 'error') {
|
| 376 |
+
const alert = document.getElementById('alert');
|
| 377 |
+
alert.textContent = message;
|
| 378 |
+
alert.className = `alert ${type} show`;
|
| 379 |
+
|
| 380 |
+
setTimeout(() => {
|
| 381 |
+
alert.classList.remove('show');
|
| 382 |
+
}, 5000);
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
// Handle login
|
| 386 |
+
// ✅ FIXED: Replaced event listener with direct function
|
| 387 |
+
async function handleLogin() {
|
| 388 |
+
|
| 389 |
+
const email = document.getElementById('email').value;
|
| 390 |
+
const password = document.getElementById('password').value;
|
| 391 |
+
const loginBtn = document.getElementById('loginBtn');
|
| 392 |
+
const loading = document.getElementById('loading');
|
| 393 |
+
|
| 394 |
+
loginBtn.disabled = true;
|
| 395 |
+
loading.classList.add('show');
|
| 396 |
+
|
| 397 |
+
try {
|
| 398 |
+
const response = await fetch(`${API_URL}/auth/login`, {
|
| 399 |
+
method: 'POST',
|
| 400 |
+
headers: {
|
| 401 |
+
'Content-Type': 'application/json',
|
| 402 |
+
},
|
| 403 |
+
body: JSON.stringify({
|
| 404 |
+
email: email,
|
| 405 |
+
password: password
|
| 406 |
+
})
|
| 407 |
+
});
|
| 408 |
+
|
| 409 |
+
const data = await response.json();
|
| 410 |
+
|
| 411 |
+
if (response.ok) {
|
| 412 |
+
localStorage.setItem('token', data.access_token);
|
| 413 |
+
localStorage.setItem('role', data.role);
|
| 414 |
+
|
| 415 |
+
if (data.role === 'student') {
|
| 416 |
+
localStorage.setItem('show_welcome_alert', 'true');
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
showAlert('Login successful! Redirecting...', 'success');
|
| 420 |
+
|
| 421 |
+
setTimeout(() => {
|
| 422 |
+
if (data.role === 'admin') {
|
| 423 |
+
window.location.href = 'admin-dashboard.html';
|
| 424 |
+
} else {
|
| 425 |
+
window.location.href = 'chat.html';
|
| 426 |
+
}
|
| 427 |
+
}, 800); // ✅ FIXED: Reduced delay to 800ms
|
| 428 |
+
} else {
|
| 429 |
+
showAlert(data.detail || 'Login failed. Please check your credentials.');
|
| 430 |
+
}
|
| 431 |
+
} catch (error) {
|
| 432 |
+
console.error('Login error:', error);
|
| 433 |
+
showAlert('Connection error. Please make sure the API is running.');
|
| 434 |
+
} finally {
|
| 435 |
+
loginBtn.disabled = false;
|
| 436 |
+
loading.classList.remove('show');
|
| 437 |
+
}
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
// Check if user is already logged in
|
| 441 |
+
window.addEventListener('DOMContentLoaded', async () => {
|
| 442 |
+
const token = localStorage.getItem('token');
|
| 443 |
+
const role = localStorage.getItem('role');
|
| 444 |
+
|
| 445 |
+
if (token && role) {
|
| 446 |
+
try {
|
| 447 |
+
const response = await fetch(`${API_URL}/user/me`, {
|
| 448 |
+
headers: {
|
| 449 |
+
'Authorization': `Bearer ${token}`
|
| 450 |
+
}
|
| 451 |
+
});
|
| 452 |
+
|
| 453 |
+
if (response.ok) {
|
| 454 |
+
console.log('✅ User already logged in, redirecting...');
|
| 455 |
+
if (role === 'admin') {
|
| 456 |
+
window.location.href = 'admin-dashboard.html';
|
| 457 |
+
} else {
|
| 458 |
+
window.location.href = 'chat.html';
|
| 459 |
+
}
|
| 460 |
+
} else {
|
| 461 |
+
localStorage.clear();
|
| 462 |
+
}
|
| 463 |
+
} catch (error) {
|
| 464 |
+
console.error('Token validation error:', error);
|
| 465 |
+
localStorage.clear();
|
| 466 |
+
}
|
| 467 |
+
}
|
| 468 |
+
});
|
| 469 |
+
</script>
|
| 470 |
+
</body>
|
| 471 |
+
</html>
|
main.py
ADDED
|
@@ -0,0 +1,1395 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# main.py - Updated Version with All Fixes
|
| 2 |
+
from fastapi import FastAPI, Depends, HTTPException, status, UploadFile, File, Form, Header
|
| 3 |
+
from fastapi.responses import FileResponse, StreamingResponse
|
| 4 |
+
from fastapi.staticfiles import StaticFiles
|
| 5 |
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
| 6 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 7 |
+
from pydantic import BaseModel, EmailStr
|
| 8 |
+
from typing import Optional, List
|
| 9 |
+
from datetime import datetime, timedelta, timezone
|
| 10 |
+
import jwt
|
| 11 |
+
import bcrypt
|
| 12 |
+
import sqlite3
|
| 13 |
+
import os
|
| 14 |
+
import uuid
|
| 15 |
+
import smtplib
|
| 16 |
+
from email.mime.text import MIMEText
|
| 17 |
+
from email.mime.multipart import MIMEMultipart
|
| 18 |
+
import sys
|
| 19 |
+
from dotenv import load_dotenv
|
| 20 |
+
import asyncio
|
| 21 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 22 |
+
import secrets
|
| 23 |
+
import httpx
|
| 24 |
+
|
| 25 |
+
# In-memory storage for OTPs (for MVP)
|
| 26 |
+
otp_storage = {}
|
| 27 |
+
|
| 28 |
+
# Setup paths
|
| 29 |
+
from process_pdf import process_new_pdf
|
| 30 |
+
|
| 31 |
+
# Thread pool for Qdrant operations
|
| 32 |
+
executor = ThreadPoolExecutor(max_workers=3)
|
| 33 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 34 |
+
|
| 35 |
+
# Load environment variables
|
| 36 |
+
load_dotenv()
|
| 37 |
+
|
| 38 |
+
# =======================
|
| 39 |
+
# Configuration
|
| 40 |
+
# =======================
|
| 41 |
+
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-in-production")
|
| 42 |
+
ALGORITHM = "HS256"
|
| 43 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 # 24 hours
|
| 44 |
+
UPLOAD_DIR = "uploads"
|
| 45 |
+
LECTURES_DIR = "lectures"
|
| 46 |
+
DB_PATH = os.path.join("data", "university_chatbot.db")
|
| 47 |
+
|
| 48 |
+
# Email Configuration
|
| 49 |
+
GMAIL_USER = "universityai.com@gmail.com"
|
| 50 |
+
GMAIL_PASSWORD = "megg neiq boli dhzt"
|
| 51 |
+
EMAIL_HOST = "smtp.gmail.com"
|
| 52 |
+
EMAIL_PORT = 587
|
| 53 |
+
SENDER_EMAIL = GMAIL_USER
|
| 54 |
+
|
| 55 |
+
app = FastAPI(title="University AI Chatbot API with Courses")
|
| 56 |
+
|
| 57 |
+
# OAuth2 Scheme
|
| 58 |
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login", auto_error=False)
|
| 59 |
+
|
| 60 |
+
# CORS
|
| 61 |
+
app.add_middleware(
|
| 62 |
+
CORSMiddleware,
|
| 63 |
+
allow_origins=["*"],
|
| 64 |
+
allow_credentials=True,
|
| 65 |
+
allow_methods=["*"],
|
| 66 |
+
allow_headers=["*"],
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
# Serve only the chat image securely instead of the whole backend folder
|
| 70 |
+
@app.get("/static/ch.png")
|
| 71 |
+
async def get_chat_image():
|
| 72 |
+
return FileResponse("ch.png")
|
| 73 |
+
|
| 74 |
+
# =======================
|
| 75 |
+
# Database Setup with Auto-Migration
|
| 76 |
+
# =======================
|
| 77 |
+
def init_db():
|
| 78 |
+
# Ensure data directory exists to prevent crash
|
| 79 |
+
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
| 80 |
+
|
| 81 |
+
conn = sqlite3.connect(DB_PATH)
|
| 82 |
+
c = conn.cursor()
|
| 83 |
+
|
| 84 |
+
print("\n" + "="*60)
|
| 85 |
+
print("🔄 Initializing Database...")
|
| 86 |
+
print("="*60 + "\n")
|
| 87 |
+
|
| 88 |
+
# Users table
|
| 89 |
+
c.execute('''CREATE TABLE IF NOT EXISTS users (
|
| 90 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 91 |
+
email TEXT NOT NULL UNIQUE,
|
| 92 |
+
password_hash TEXT NOT NULL,
|
| 93 |
+
role TEXT NOT NULL,
|
| 94 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 95 |
+
)''')
|
| 96 |
+
|
| 97 |
+
# Courses table
|
| 98 |
+
c.execute('''CREATE TABLE IF NOT EXISTS courses (
|
| 99 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 100 |
+
name TEXT UNIQUE NOT NULL,
|
| 101 |
+
description TEXT,
|
| 102 |
+
admin_id INTEGER NOT NULL,
|
| 103 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 104 |
+
FOREIGN KEY (admin_id) REFERENCES users(id)
|
| 105 |
+
)''')
|
| 106 |
+
|
| 107 |
+
# Conversations table
|
| 108 |
+
c.execute('''CREATE TABLE IF NOT EXISTS conversations (
|
| 109 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 110 |
+
user_id INTEGER NOT NULL,
|
| 111 |
+
title TEXT NOT NULL,
|
| 112 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 113 |
+
is_deleted INTEGER DEFAULT 0,
|
| 114 |
+
FOREIGN KEY (user_id) REFERENCES users(id)
|
| 115 |
+
)''')
|
| 116 |
+
|
| 117 |
+
# Messages table - NOW WITH BOTH sender AND role
|
| 118 |
+
c.execute('''CREATE TABLE IF NOT EXISTS messages (
|
| 119 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 120 |
+
conversation_id INTEGER NOT NULL,
|
| 121 |
+
sender TEXT NOT NULL,
|
| 122 |
+
role TEXT NOT NULL,
|
| 123 |
+
content TEXT NOT NULL,
|
| 124 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 125 |
+
FOREIGN KEY (conversation_id) REFERENCES conversations(id)
|
| 126 |
+
)''')
|
| 127 |
+
|
| 128 |
+
# Feedbacks table
|
| 129 |
+
c.execute('''CREATE TABLE IF NOT EXISTS feedbacks (
|
| 130 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 131 |
+
message_id INTEGER NOT NULL,
|
| 132 |
+
user_id INTEGER NOT NULL,
|
| 133 |
+
feedback_type TEXT NOT NULL,
|
| 134 |
+
comment TEXT,
|
| 135 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 136 |
+
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE,
|
| 137 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
| 138 |
+
UNIQUE(message_id, user_id)
|
| 139 |
+
)''')
|
| 140 |
+
|
| 141 |
+
# Files table
|
| 142 |
+
c.execute('''CREATE TABLE IF NOT EXISTS files (
|
| 143 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 144 |
+
user_id INTEGER NOT NULL,
|
| 145 |
+
filename TEXT NOT NULL,
|
| 146 |
+
filepath TEXT NOT NULL,
|
| 147 |
+
file_type TEXT NOT NULL,
|
| 148 |
+
subject TEXT,
|
| 149 |
+
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 150 |
+
FOREIGN KEY (user_id) REFERENCES users(id)
|
| 151 |
+
)''')
|
| 152 |
+
|
| 153 |
+
# Lectures table
|
| 154 |
+
c.execute('''CREATE TABLE IF NOT EXISTS lectures (
|
| 155 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 156 |
+
admin_id INTEGER NOT NULL,
|
| 157 |
+
filename TEXT NOT NULL,
|
| 158 |
+
filepath TEXT NOT NULL,
|
| 159 |
+
subject TEXT,
|
| 160 |
+
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 161 |
+
processing_status TEXT DEFAULT 'pending',
|
| 162 |
+
total_chunks INTEGER DEFAULT 0,
|
| 163 |
+
total_characters INTEGER DEFAULT 0,
|
| 164 |
+
error_message TEXT,
|
| 165 |
+
FOREIGN KEY (admin_id) REFERENCES users(id)
|
| 166 |
+
)''')
|
| 167 |
+
|
| 168 |
+
# Password reset tokens table
|
| 169 |
+
c.execute('''CREATE TABLE IF NOT EXISTS password_reset_tokens (
|
| 170 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 171 |
+
user_id INTEGER NOT NULL,
|
| 172 |
+
token TEXT UNIQUE NOT NULL,
|
| 173 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 174 |
+
expires_at TEXT NOT NULL,
|
| 175 |
+
used INTEGER DEFAULT 0,
|
| 176 |
+
FOREIGN KEY (user_id) REFERENCES users(id)
|
| 177 |
+
)''')
|
| 178 |
+
|
| 179 |
+
conn.commit()
|
| 180 |
+
|
| 181 |
+
# 🔧 AUTO-MIGRATION - Add missing columns
|
| 182 |
+
print("🔍 Checking for missing columns...")
|
| 183 |
+
|
| 184 |
+
# 🔥 CRITICAL FIX: Add 'role' column to messages table
|
| 185 |
+
c.execute("PRAGMA table_info(messages)")
|
| 186 |
+
message_columns = {col[1] for col in c.fetchall()}
|
| 187 |
+
|
| 188 |
+
if 'role' not in message_columns:
|
| 189 |
+
try:
|
| 190 |
+
print(" 🔧 Adding 'role' column to messages table...")
|
| 191 |
+
c.execute("ALTER TABLE messages ADD COLUMN role TEXT DEFAULT 'user'")
|
| 192 |
+
|
| 193 |
+
# Migrate existing data: map sender -> role
|
| 194 |
+
c.execute("UPDATE messages SET role = CASE WHEN sender = 'ai' THEN 'assistant' ELSE 'user' END")
|
| 195 |
+
|
| 196 |
+
conn.commit()
|
| 197 |
+
print(" ✅ Added column: role to messages (migrated existing data)")
|
| 198 |
+
except sqlite3.OperationalError as e:
|
| 199 |
+
if "duplicate column" not in str(e).lower():
|
| 200 |
+
print(f" ⚠️ Error adding role: {e}")
|
| 201 |
+
else:
|
| 202 |
+
# Ensure existing role data is correct
|
| 203 |
+
try:
|
| 204 |
+
c.execute("UPDATE messages SET role = CASE WHEN sender = 'ai' THEN 'assistant' ELSE 'user' END WHERE role IS NULL OR role = ''")
|
| 205 |
+
conn.commit()
|
| 206 |
+
print(" ✅ Role column exists and data verified")
|
| 207 |
+
except Exception as e:
|
| 208 |
+
print(f" ⚠️ Error verifying role data: {e}")
|
| 209 |
+
|
| 210 |
+
# Check lectures table columns
|
| 211 |
+
c.execute("PRAGMA table_info(lectures)")
|
| 212 |
+
existing_columns = {col[1] for col in c.fetchall()}
|
| 213 |
+
|
| 214 |
+
required_columns = {
|
| 215 |
+
'processing_status': "TEXT DEFAULT 'pending'",
|
| 216 |
+
'total_chunks': "INTEGER DEFAULT 0",
|
| 217 |
+
'total_characters': "INTEGER DEFAULT 0",
|
| 218 |
+
'error_message': "TEXT"
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
for col_name, col_type in required_columns.items():
|
| 222 |
+
if col_name not in existing_columns:
|
| 223 |
+
try:
|
| 224 |
+
c.execute(f"ALTER TABLE lectures ADD COLUMN {col_name} {col_type}")
|
| 225 |
+
conn.commit()
|
| 226 |
+
print(f" ✅ Added column: {col_name}")
|
| 227 |
+
except sqlite3.OperationalError as e:
|
| 228 |
+
if "duplicate column" not in str(e).lower():
|
| 229 |
+
print(f" ⚠️ Error adding {col_name}: {e}")
|
| 230 |
+
|
| 231 |
+
# Check conversations table for is_deleted
|
| 232 |
+
c.execute("PRAGMA table_info(conversations)")
|
| 233 |
+
conv_columns = {col[1] for col in c.fetchall()}
|
| 234 |
+
if 'is_deleted' not in conv_columns:
|
| 235 |
+
try:
|
| 236 |
+
c.execute("ALTER TABLE conversations ADD COLUMN is_deleted INTEGER DEFAULT 0")
|
| 237 |
+
conn.commit()
|
| 238 |
+
print(" ✅ Added column: is_deleted to conversations")
|
| 239 |
+
except Exception as e:
|
| 240 |
+
print(f" ⚠️ Error adding is_deleted: {e}")
|
| 241 |
+
|
| 242 |
+
# Seed admin user
|
| 243 |
+
admin_email = "admin@university.edu"
|
| 244 |
+
admin_password = "Admin123"
|
| 245 |
+
hashed = bcrypt.hashpw(admin_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
| 246 |
+
|
| 247 |
+
try:
|
| 248 |
+
c.execute("SELECT id FROM users WHERE email = ?", (admin_email,))
|
| 249 |
+
existing_admin = c.fetchone()
|
| 250 |
+
|
| 251 |
+
if not existing_admin:
|
| 252 |
+
c.execute("INSERT INTO users (email, password_hash, role) VALUES (?, ?, ?)",
|
| 253 |
+
(admin_email, hashed, 'admin'))
|
| 254 |
+
conn.commit()
|
| 255 |
+
admin_id = c.lastrowid
|
| 256 |
+
print(f"\n✅ Admin user created: {admin_email}")
|
| 257 |
+
print(f" Password: {admin_password}")
|
| 258 |
+
else:
|
| 259 |
+
admin_id = existing_admin[0]
|
| 260 |
+
print(f"\nℹ️ Admin user already exists: {admin_email}")
|
| 261 |
+
|
| 262 |
+
# Seed 12 Fixed Courses
|
| 263 |
+
fixed_courses = [
|
| 264 |
+
("Android Development", "Basics of CS and programming"),
|
| 265 |
+
("Computer Networks", "Fundamental data structures and algorithms"),
|
| 266 |
+
("Information Security", "SQL, NoSQL, and database design"),
|
| 267 |
+
("Operating Systems", "Process management, memory, and concurrency"),
|
| 268 |
+
("Theory of Computation", "OSI model, TCP/IP, and network security"),
|
| 269 |
+
("Algorithms Design and Analysis", "SDLC, agile, and design patterns"),
|
| 270 |
+
("Computer Architecture", "Search, logic, and probabilistic reasoning"),
|
| 271 |
+
("Machine Learning", "Supervised and unsupervised learning"),
|
| 272 |
+
("Compiler Design", "HTML, CSS, JavaScript, and backend frameworks"),
|
| 273 |
+
("Computer Graphics", "Network security, cryptography, and ethical hacking"),
|
| 274 |
+
|
| 275 |
+
("Human Computer Interaction", "AWS, Azure, and cloud architecture")
|
| 276 |
+
]
|
| 277 |
+
|
| 278 |
+
print("\n🌱 Seeding fixed courses...")
|
| 279 |
+
for name, desc in fixed_courses:
|
| 280 |
+
c.execute("SELECT id FROM courses WHERE name = ?", (name,))
|
| 281 |
+
if not c.fetchone():
|
| 282 |
+
c.execute("INSERT INTO courses (name, description, admin_id) VALUES (?, ?, ?)",
|
| 283 |
+
(name, desc, admin_id))
|
| 284 |
+
print(f" ✅ Added course: {name}")
|
| 285 |
+
|
| 286 |
+
conn.commit()
|
| 287 |
+
|
| 288 |
+
except Exception as e:
|
| 289 |
+
print(f"❌ Error seeding data: {e}")
|
| 290 |
+
|
| 291 |
+
conn.close()
|
| 292 |
+
|
| 293 |
+
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 294 |
+
os.makedirs(LECTURES_DIR, exist_ok=True)
|
| 295 |
+
|
| 296 |
+
print("\n" + "="*60)
|
| 297 |
+
print("✅ Database initialization completed!")
|
| 298 |
+
print("="*60 + "\n")
|
| 299 |
+
|
| 300 |
+
init_db()
|
| 301 |
+
|
| 302 |
+
# =======================
|
| 303 |
+
# Helper Functions
|
| 304 |
+
# =======================
|
| 305 |
+
def get_db():
|
| 306 |
+
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
| 307 |
+
conn.row_factory = sqlite3.Row
|
| 308 |
+
return conn
|
| 309 |
+
|
| 310 |
+
def create_access_token(data: dict) -> str:
|
| 311 |
+
to_encode = data.copy()
|
| 312 |
+
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 313 |
+
to_encode.update({"exp": expire})
|
| 314 |
+
token = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 315 |
+
return token
|
| 316 |
+
|
| 317 |
+
def verify_token(token: str):
|
| 318 |
+
try:
|
| 319 |
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
| 320 |
+
return payload
|
| 321 |
+
except jwt.ExpiredSignatureError:
|
| 322 |
+
raise HTTPException(status_code=401, detail="Token expired")
|
| 323 |
+
except jwt.InvalidTokenError:
|
| 324 |
+
raise HTTPException(status_code=401, detail="Invalid token")
|
| 325 |
+
|
| 326 |
+
def get_current_user(
|
| 327 |
+
authorization: Optional[str] = Header(None),
|
| 328 |
+
token: Optional[str] = Depends(oauth2_scheme)
|
| 329 |
+
):
|
| 330 |
+
raw_token = None
|
| 331 |
+
|
| 332 |
+
if authorization and authorization.startswith("Bearer "):
|
| 333 |
+
raw_token = authorization.split(" ")[1]
|
| 334 |
+
elif token:
|
| 335 |
+
raw_token = token
|
| 336 |
+
|
| 337 |
+
if not raw_token:
|
| 338 |
+
raise HTTPException(status_code=401, detail="Authentication required")
|
| 339 |
+
|
| 340 |
+
payload = verify_token(raw_token)
|
| 341 |
+
user_id = payload.get("user_id")
|
| 342 |
+
role = payload.get("role")
|
| 343 |
+
|
| 344 |
+
if not user_id:
|
| 345 |
+
raise HTTPException(status_code=401, detail="Invalid token")
|
| 346 |
+
|
| 347 |
+
return {"user_id": user_id, "role": role}
|
| 348 |
+
|
| 349 |
+
def send_email(to_email: str, subject: str, html_content: str):
|
| 350 |
+
"""Sends an email using the configured SMTP server."""
|
| 351 |
+
if not all([GMAIL_USER, GMAIL_PASSWORD]):
|
| 352 |
+
print("⚠️ Email configuration missing. Skipping email.")
|
| 353 |
+
return
|
| 354 |
+
|
| 355 |
+
message = MIMEMultipart("alternative")
|
| 356 |
+
message["Subject"] = subject
|
| 357 |
+
message["From"] = SENDER_EMAIL
|
| 358 |
+
message["To"] = to_email
|
| 359 |
+
|
| 360 |
+
text_content = "Please enable HTML to view this email."
|
| 361 |
+
part1 = MIMEText(text_content, "plain")
|
| 362 |
+
part2 = MIMEText(html_content, "html")
|
| 363 |
+
|
| 364 |
+
message.attach(part1)
|
| 365 |
+
message.attach(part2)
|
| 366 |
+
|
| 367 |
+
try:
|
| 368 |
+
with smtplib.SMTP(EMAIL_HOST, EMAIL_PORT) as server:
|
| 369 |
+
server.starttls()
|
| 370 |
+
server.login(GMAIL_USER, GMAIL_PASSWORD)
|
| 371 |
+
server.sendmail(SENDER_EMAIL, to_email, message.as_string())
|
| 372 |
+
print(f"✅ Email sent to {to_email}")
|
| 373 |
+
except Exception as e:
|
| 374 |
+
print(f"❌ Failed to send email to {to_email}: {e}")
|
| 375 |
+
|
| 376 |
+
# =======================
|
| 377 |
+
# Pydantic Models
|
| 378 |
+
# =======================
|
| 379 |
+
class UserRegister(BaseModel):
|
| 380 |
+
email: EmailStr
|
| 381 |
+
password: str
|
| 382 |
+
|
| 383 |
+
class ForgotPasswordRequest(BaseModel):
|
| 384 |
+
email: EmailStr
|
| 385 |
+
|
| 386 |
+
class ResetPasswordRequest(BaseModel):
|
| 387 |
+
email: EmailStr
|
| 388 |
+
token: str
|
| 389 |
+
new_password: str
|
| 390 |
+
|
| 391 |
+
class Token(BaseModel):
|
| 392 |
+
access_token: str
|
| 393 |
+
token_type: str
|
| 394 |
+
role: str
|
| 395 |
+
|
| 396 |
+
class ChatMessage(BaseModel):
|
| 397 |
+
conversation_id: Optional[int] = None
|
| 398 |
+
message: str
|
| 399 |
+
|
| 400 |
+
class FeedbackRequest(BaseModel):
|
| 401 |
+
message_id: int
|
| 402 |
+
feedback_type: str
|
| 403 |
+
comment: Optional[str] = None
|
| 404 |
+
|
| 405 |
+
class CourseCreate(BaseModel):
|
| 406 |
+
name: str
|
| 407 |
+
description: Optional[str] = None
|
| 408 |
+
|
| 409 |
+
# =======================
|
| 410 |
+
# Root Endpoint
|
| 411 |
+
# =======================
|
| 412 |
+
# Serve the Login page by default
|
| 413 |
+
@app.get("/")
|
| 414 |
+
async def root():
|
| 415 |
+
return FileResponse("login.html")
|
| 416 |
+
|
| 417 |
+
# Serve HTML Pages
|
| 418 |
+
@app.get("/login.html")
|
| 419 |
+
async def login_page():
|
| 420 |
+
return FileResponse("login.html")
|
| 421 |
+
|
| 422 |
+
@app.get("/chat.html")
|
| 423 |
+
async def chat_page():
|
| 424 |
+
return FileResponse("chat.html")
|
| 425 |
+
|
| 426 |
+
@app.get("/Admin-Dashboard.html")
|
| 427 |
+
async def admin_page():
|
| 428 |
+
return FileResponse("Admin-Dashboard.html")
|
| 429 |
+
|
| 430 |
+
@app.get("/register.html")
|
| 431 |
+
async def register_page():
|
| 432 |
+
return FileResponse("register.html")
|
| 433 |
+
|
| 434 |
+
@app.get("/forgot-password.html")
|
| 435 |
+
async def forgot_password_page():
|
| 436 |
+
return FileResponse("forgot-password.html")
|
| 437 |
+
|
| 438 |
+
@app.get("/reset-password.html")
|
| 439 |
+
async def reset_password_page():
|
| 440 |
+
return FileResponse("reset-password.html")
|
| 441 |
+
|
| 442 |
+
@app.get("/verify-email.html")
|
| 443 |
+
async def verify_email_page():
|
| 444 |
+
return FileResponse("verify-email.html")
|
| 445 |
+
|
| 446 |
+
# =======================
|
| 447 |
+
# Auth Endpoints
|
| 448 |
+
# =======================
|
| 449 |
+
@app.post("/auth/register")
|
| 450 |
+
async def register(email: str = Form(...), password: str = Form(...)):
|
| 451 |
+
conn = get_db()
|
| 452 |
+
c = conn.cursor()
|
| 453 |
+
|
| 454 |
+
c.execute("SELECT id FROM users WHERE email = ?", (email,))
|
| 455 |
+
if c.fetchone():
|
| 456 |
+
conn.close()
|
| 457 |
+
raise HTTPException(status_code=400, detail="Email already registered")
|
| 458 |
+
|
| 459 |
+
hashed_pw = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
|
| 460 |
+
c.execute("INSERT INTO users (email, password_hash, role) VALUES (?, ?, ?)",
|
| 461 |
+
(email, hashed_pw, "student"))
|
| 462 |
+
conn.commit()
|
| 463 |
+
conn.close()
|
| 464 |
+
|
| 465 |
+
# Send Welcome Email
|
| 466 |
+
subject = "Welcome to University AI! 🎓"
|
| 467 |
+
html_content = f"""
|
| 468 |
+
<html><body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
| 469 |
+
<div style="max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 10px;">
|
| 470 |
+
<h2 style="color: #3A662A; text-align: center;">Welcome to University AI! 🎓</h2>
|
| 471 |
+
<p>Hi there,</p>
|
| 472 |
+
<p>Thank you for joining University AI. We are excited to have you on board!</p>
|
| 473 |
+
<p>You can now log in and start chatting with our AI assistant.</p>
|
| 474 |
+
<hr style="border: none; border-top: 1px solid #ddd; margin: 20px 0;">
|
| 475 |
+
<p style="font-size: 12px; color: #999; text-align: center;">University AI Team<br>Your Learning Partner</p>
|
| 476 |
+
</div>
|
| 477 |
+
</body></html>"""
|
| 478 |
+
|
| 479 |
+
send_email(email, subject, html_content)
|
| 480 |
+
|
| 481 |
+
return {
|
| 482 |
+
"message": "Account created successfully. Redirecting to login...",
|
| 483 |
+
"email": email
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
@app.post("/auth/login", response_model=Token)
|
| 487 |
+
async def login(data: UserRegister):
|
| 488 |
+
conn = get_db()
|
| 489 |
+
c = conn.cursor()
|
| 490 |
+
|
| 491 |
+
c.execute("SELECT * FROM users WHERE email = ?", (data.email,))
|
| 492 |
+
user = c.fetchone()
|
| 493 |
+
conn.close()
|
| 494 |
+
|
| 495 |
+
if not user:
|
| 496 |
+
raise HTTPException(status_code=401, detail="Invalid email or password")
|
| 497 |
+
|
| 498 |
+
if not bcrypt.checkpw(data.password.encode(), user["password_hash"].encode()):
|
| 499 |
+
raise HTTPException(status_code=401, detail="Invalid email or password")
|
| 500 |
+
|
| 501 |
+
access = create_access_token({
|
| 502 |
+
"user_id": user["id"],
|
| 503 |
+
"role": user["role"]
|
| 504 |
+
})
|
| 505 |
+
|
| 506 |
+
print(f"✅ User logged in: {data.email} ({user['role']})")
|
| 507 |
+
|
| 508 |
+
return {
|
| 509 |
+
"access_token": access,
|
| 510 |
+
"token_type": "bearer",
|
| 511 |
+
"role": user["role"]
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
@app.post("/auth/forgot-password")
|
| 515 |
+
async def forgot_password(request: ForgotPasswordRequest):
|
| 516 |
+
"""Handles forgot password request"""
|
| 517 |
+
conn = get_db()
|
| 518 |
+
c = conn.cursor()
|
| 519 |
+
|
| 520 |
+
c.execute("SELECT id FROM users WHERE email = ?", (request.email,))
|
| 521 |
+
user = c.fetchone()
|
| 522 |
+
conn.close()
|
| 523 |
+
|
| 524 |
+
if user:
|
| 525 |
+
otp = f"{secrets.randbelow(1000000):06d}"
|
| 526 |
+
otp_storage[request.email] = {
|
| 527 |
+
"code": otp,
|
| 528 |
+
"timestamp": datetime.now(timezone.utc)
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
subject = "Reset Your Password - University AI 🔑"
|
| 532 |
+
html_content = f"""
|
| 533 |
+
<html><body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
| 534 |
+
<div style="max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 10px;">
|
| 535 |
+
<h2 style="color: #3A662A; text-align: center;">Password Reset Request</h2>
|
| 536 |
+
<p>Hi,</p>
|
| 537 |
+
<p>We received a request to reset your password. Use this code:</p>
|
| 538 |
+
<div style="background: #f5f5f5; padding: 20px; text-align: center; border-radius: 8px; margin: 20px 0;">
|
| 539 |
+
<h1 style="letter-spacing: 8px; font-size: 36px; margin: 10px 0; color: #3A662A;">{otp}</h1>
|
| 540 |
+
<p style="margin: 10px 0 0 0; font-size: 12px; color: #999;">Expires in 5 minutes</p>
|
| 541 |
+
</div>
|
| 542 |
+
<p style="font-size: 14px; color: #666;">If you didn't request this, ignore this email.</p>
|
| 543 |
+
<p style="font-size: 12px; color: #999; text-align: center;">University AI Security Team</p>
|
| 544 |
+
</div>
|
| 545 |
+
</body></html>"""
|
| 546 |
+
|
| 547 |
+
send_email(request.email, subject, html_content)
|
| 548 |
+
print(f"🔐 Password reset code sent to: {request.email}")
|
| 549 |
+
|
| 550 |
+
return {"message": "If account exists, password reset code has been sent."}
|
| 551 |
+
|
| 552 |
+
@app.post("/auth/reset-password")
|
| 553 |
+
async def reset_password(request: ResetPasswordRequest):
|
| 554 |
+
"""Resets user password"""
|
| 555 |
+
conn = get_db()
|
| 556 |
+
c = conn.cursor()
|
| 557 |
+
|
| 558 |
+
# Verify OTP
|
| 559 |
+
stored_otp = otp_storage.get(request.email)
|
| 560 |
+
if not stored_otp or stored_otp["code"] != request.token:
|
| 561 |
+
conn.close()
|
| 562 |
+
raise HTTPException(status_code=400, detail="Invalid or incorrect code.")
|
| 563 |
+
|
| 564 |
+
# Check TTL
|
| 565 |
+
if datetime.now(timezone.utc) - stored_otp["timestamp"] > timedelta(minutes=5):
|
| 566 |
+
del otp_storage[request.email]
|
| 567 |
+
conn.close()
|
| 568 |
+
raise HTTPException(status_code=400, detail="Code has expired.")
|
| 569 |
+
|
| 570 |
+
# Burn OTP
|
| 571 |
+
del otp_storage[request.email]
|
| 572 |
+
|
| 573 |
+
c.execute("SELECT id FROM users WHERE email = ?", (request.email,))
|
| 574 |
+
user = c.fetchone()
|
| 575 |
+
if not user:
|
| 576 |
+
conn.close()
|
| 577 |
+
raise HTTPException(status_code=404, detail="User not found.")
|
| 578 |
+
|
| 579 |
+
new_hashed_pw = bcrypt.hashpw(request.new_password.encode(), bcrypt.gensalt()).decode()
|
| 580 |
+
c.execute("UPDATE users SET password_hash = ? WHERE id = ?", (new_hashed_pw, user["id"]))
|
| 581 |
+
conn.commit()
|
| 582 |
+
conn.close()
|
| 583 |
+
|
| 584 |
+
print(f"🔐 Password reset successful for: {request.email}")
|
| 585 |
+
|
| 586 |
+
return {"message": "Password reset successful. You can now login with your new password."}
|
| 587 |
+
|
| 588 |
+
# =======================
|
| 589 |
+
# Student Endpoints - FIXED
|
| 590 |
+
# =======================
|
| 591 |
+
@app.get("/student/conversations")
|
| 592 |
+
async def get_conversations(current_user: dict = Depends(get_current_user)):
|
| 593 |
+
"""Get all conversations with first message for title generation"""
|
| 594 |
+
if current_user['role'] != 'student':
|
| 595 |
+
raise HTTPException(status_code=403, detail="Access denied")
|
| 596 |
+
|
| 597 |
+
conn = get_db()
|
| 598 |
+
c = conn.cursor()
|
| 599 |
+
|
| 600 |
+
try:
|
| 601 |
+
c.execute("""
|
| 602 |
+
SELECT
|
| 603 |
+
c.id,
|
| 604 |
+
c.title,
|
| 605 |
+
c.created_at,
|
| 606 |
+
(SELECT content
|
| 607 |
+
FROM messages m
|
| 608 |
+
WHERE m.conversation_id = c.id
|
| 609 |
+
AND (m.role = 'user' OR m.sender = 'user')
|
| 610 |
+
ORDER BY m.created_at ASC
|
| 611 |
+
LIMIT 1) as first_message
|
| 612 |
+
FROM conversations c
|
| 613 |
+
WHERE c.user_id = ? AND c.is_deleted = 0
|
| 614 |
+
ORDER BY c.created_at DESC
|
| 615 |
+
""", (current_user['user_id'],))
|
| 616 |
+
|
| 617 |
+
conversations = []
|
| 618 |
+
for row in c.fetchall():
|
| 619 |
+
conv_dict = dict(row)
|
| 620 |
+
conversations.append(conv_dict)
|
| 621 |
+
|
| 622 |
+
conn.close()
|
| 623 |
+
return {"conversations": conversations}
|
| 624 |
+
|
| 625 |
+
except Exception as e:
|
| 626 |
+
conn.close()
|
| 627 |
+
print(f"❌ Error loading conversations: {e}")
|
| 628 |
+
raise HTTPException(status_code=500, detail=f"Error loading conversations: {str(e)}")
|
| 629 |
+
|
| 630 |
+
@app.get("/student/conversation/{conversation_id}")
|
| 631 |
+
async def get_conversation(conversation_id: int, current_user: dict = Depends(get_current_user)):
|
| 632 |
+
"""Get conversation with all messages - handles both old and new format"""
|
| 633 |
+
if current_user['role'] != 'student':
|
| 634 |
+
raise HTTPException(status_code=403, detail="Access denied")
|
| 635 |
+
|
| 636 |
+
conn = get_db()
|
| 637 |
+
c = conn.cursor()
|
| 638 |
+
|
| 639 |
+
try:
|
| 640 |
+
# Verify conversation belongs to user
|
| 641 |
+
c.execute("SELECT * FROM conversations WHERE id = ? AND user_id = ? AND is_deleted = 0",
|
| 642 |
+
(conversation_id, current_user['user_id']))
|
| 643 |
+
conversation = c.fetchone()
|
| 644 |
+
|
| 645 |
+
if not conversation:
|
| 646 |
+
conn.close()
|
| 647 |
+
raise HTTPException(status_code=404, detail="Conversation not found")
|
| 648 |
+
|
| 649 |
+
# Get all messages with both sender and role
|
| 650 |
+
c.execute("""
|
| 651 |
+
SELECT id, conversation_id, sender, role, content, created_at
|
| 652 |
+
FROM messages
|
| 653 |
+
WHERE conversation_id = ?
|
| 654 |
+
ORDER BY created_at ASC
|
| 655 |
+
""", (conversation_id,))
|
| 656 |
+
|
| 657 |
+
messages = []
|
| 658 |
+
for row in c.fetchall():
|
| 659 |
+
msg = dict(row)
|
| 660 |
+
|
| 661 |
+
# Ensure role is set correctly (backward compatibility)
|
| 662 |
+
if not msg.get('role') or msg['role'] == '':
|
| 663 |
+
if msg['sender'] == 'ai':
|
| 664 |
+
msg['role'] = 'assistant'
|
| 665 |
+
else:
|
| 666 |
+
msg['role'] = 'user'
|
| 667 |
+
|
| 668 |
+
messages.append(msg)
|
| 669 |
+
|
| 670 |
+
conn.close()
|
| 671 |
+
|
| 672 |
+
print(f"✅ Loaded conversation {conversation_id} with {len(messages)} messages")
|
| 673 |
+
|
| 674 |
+
return {
|
| 675 |
+
"conversation": dict(conversation),
|
| 676 |
+
"messages": messages
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
except HTTPException:
|
| 680 |
+
raise
|
| 681 |
+
except Exception as e:
|
| 682 |
+
conn.close()
|
| 683 |
+
print(f"❌ Error loading conversation: {e}")
|
| 684 |
+
raise HTTPException(status_code=500, detail=f"Error loading conversation: {str(e)}")
|
| 685 |
+
|
| 686 |
+
@app.delete("/student/conversations/{conversation_id}")
|
| 687 |
+
async def delete_conversation(
|
| 688 |
+
conversation_id: int,
|
| 689 |
+
current_user: dict = Depends(get_current_user)
|
| 690 |
+
):
|
| 691 |
+
"""Delete a conversation and its messages"""
|
| 692 |
+
if current_user['role'] != 'student':
|
| 693 |
+
raise HTTPException(status_code=403, detail="Access denied")
|
| 694 |
+
|
| 695 |
+
conn = get_db()
|
| 696 |
+
c = conn.cursor()
|
| 697 |
+
|
| 698 |
+
try:
|
| 699 |
+
# Verify ownership
|
| 700 |
+
c.execute("SELECT id FROM conversations WHERE id = ? AND user_id = ?",
|
| 701 |
+
(conversation_id, current_user['user_id']))
|
| 702 |
+
conversation = c.fetchone()
|
| 703 |
+
|
| 704 |
+
if not conversation:
|
| 705 |
+
conn.close()
|
| 706 |
+
raise HTTPException(status_code=404, detail="Conversation not found")
|
| 707 |
+
|
| 708 |
+
# Soft delete conversation (Hide from user, keep for admin/feedback)
|
| 709 |
+
c.execute("UPDATE conversations SET is_deleted = 1 WHERE id = ?", (conversation_id,))
|
| 710 |
+
|
| 711 |
+
conn.commit()
|
| 712 |
+
conn.close()
|
| 713 |
+
|
| 714 |
+
print(f"🗑️ Deleted conversation {conversation_id}")
|
| 715 |
+
|
| 716 |
+
return {
|
| 717 |
+
"success": True,
|
| 718 |
+
"message": "Conversation deleted successfully"
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
except HTTPException:
|
| 722 |
+
raise
|
| 723 |
+
except Exception as e:
|
| 724 |
+
conn.close()
|
| 725 |
+
print(f"❌ Error deleting conversation: {e}")
|
| 726 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 727 |
+
|
| 728 |
+
@app.post("/student/chat")
|
| 729 |
+
async def chat(data: ChatMessage, current_user: dict = Depends(get_current_user)):
|
| 730 |
+
"""Send message and get AI response - NOW SAVES BOTH sender AND role"""
|
| 731 |
+
if current_user['role'] != 'student':
|
| 732 |
+
raise HTTPException(status_code=403, detail="Access denied")
|
| 733 |
+
|
| 734 |
+
conn = get_db()
|
| 735 |
+
c = conn.cursor()
|
| 736 |
+
|
| 737 |
+
try:
|
| 738 |
+
# Create new conversation if needed
|
| 739 |
+
if data.conversation_id is None:
|
| 740 |
+
# Generate title from first 3 words
|
| 741 |
+
title_words = data.message.split()[:3]
|
| 742 |
+
title = " ".join(title_words)
|
| 743 |
+
if len(data.message.split()) > 3:
|
| 744 |
+
title += "..."
|
| 745 |
+
|
| 746 |
+
c.execute("INSERT INTO conversations (user_id, title) VALUES (?, ?)",
|
| 747 |
+
(current_user['user_id'], title))
|
| 748 |
+
conversation_id = c.lastrowid
|
| 749 |
+
print(f"✅ Created new conversation: {conversation_id} - '{title}'")
|
| 750 |
+
else:
|
| 751 |
+
conversation_id = data.conversation_id
|
| 752 |
+
|
| 753 |
+
# 🔥 CRITICAL: Save user message with BOTH sender AND role
|
| 754 |
+
c.execute(
|
| 755 |
+
"INSERT INTO messages (conversation_id, sender, role, content) VALUES (?, ?, ?, ?)",
|
| 756 |
+
(conversation_id, 'user', 'user', data.message)
|
| 757 |
+
)
|
| 758 |
+
user_message_id = c.lastrowid
|
| 759 |
+
conn.commit()
|
| 760 |
+
|
| 761 |
+
print(f"💬 User message saved (ID: {user_message_id})")
|
| 762 |
+
|
| 763 |
+
# 1. Pre-create AI message with empty content to get an ID
|
| 764 |
+
c.execute(
|
| 765 |
+
"INSERT INTO messages (conversation_id, sender, role, content) VALUES (?, ?, ?, ?)",
|
| 766 |
+
(conversation_id, 'ai', 'assistant', '')
|
| 767 |
+
)
|
| 768 |
+
ai_message_id = c.lastrowid
|
| 769 |
+
conn.commit()
|
| 770 |
+
conn.close() # Close main connection, we will open a new one for update
|
| 771 |
+
|
| 772 |
+
# 2. Define Generator for Streaming
|
| 773 |
+
async def response_generator():
|
| 774 |
+
full_response = ""
|
| 775 |
+
RAG_URL = "http://127.0.0.1:8001/ask_stream"
|
| 776 |
+
|
| 777 |
+
try:
|
| 778 |
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
| 779 |
+
async with client.stream("POST", RAG_URL, json={"question": data.message, "conversation_id": conversation_id}) as r:
|
| 780 |
+
async for chunk in r.aiter_text():
|
| 781 |
+
full_response += chunk
|
| 782 |
+
yield chunk
|
| 783 |
+
except Exception as e:
|
| 784 |
+
error_msg = f"Error: {str(e)}"
|
| 785 |
+
full_response += error_msg
|
| 786 |
+
yield error_msg
|
| 787 |
+
|
| 788 |
+
# 3. Update DB with full response after stream ends
|
| 789 |
+
try:
|
| 790 |
+
# Must create new connection in async generator
|
| 791 |
+
update_conn = sqlite3.connect(DB_PATH)
|
| 792 |
+
update_c = update_conn.cursor()
|
| 793 |
+
update_c.execute("UPDATE messages SET content = ? WHERE id = ?", (full_response, ai_message_id))
|
| 794 |
+
update_conn.commit()
|
| 795 |
+
update_conn.close()
|
| 796 |
+
print(f"🤖 AI message updated (ID: {ai_message_id})")
|
| 797 |
+
except Exception as e:
|
| 798 |
+
print(f"❌ Error updating DB: {e}")
|
| 799 |
+
|
| 800 |
+
# 4. Return Streaming Response with IDs in headers
|
| 801 |
+
return StreamingResponse(
|
| 802 |
+
response_generator(),
|
| 803 |
+
media_type="text/plain",
|
| 804 |
+
headers={
|
| 805 |
+
"X-Conversation-Id": str(conversation_id),
|
| 806 |
+
"X-Message-Id": str(ai_message_id)
|
| 807 |
+
}
|
| 808 |
+
)
|
| 809 |
+
|
| 810 |
+
except Exception as e:
|
| 811 |
+
conn.rollback()
|
| 812 |
+
print(f"❌ Error in chat endpoint: {e}")
|
| 813 |
+
raise HTTPException(status_code=500, detail=f"Chat error: {str(e)}")
|
| 814 |
+
|
| 815 |
+
# =======================
|
| 816 |
+
# Feedback Endpoints
|
| 817 |
+
# =======================
|
| 818 |
+
@app.post("/feedback")
|
| 819 |
+
async def submit_feedback(
|
| 820 |
+
feedback: FeedbackRequest,
|
| 821 |
+
current_user: dict = Depends(get_current_user)
|
| 822 |
+
):
|
| 823 |
+
"""Submit or update feedback for a message"""
|
| 824 |
+
conn = get_db()
|
| 825 |
+
c = conn.cursor()
|
| 826 |
+
|
| 827 |
+
try:
|
| 828 |
+
# Check if feedback exists
|
| 829 |
+
c.execute(
|
| 830 |
+
"SELECT id FROM feedbacks WHERE message_id = ? AND user_id = ?",
|
| 831 |
+
(feedback.message_id, current_user['user_id'])
|
| 832 |
+
)
|
| 833 |
+
existing = c.fetchone()
|
| 834 |
+
|
| 835 |
+
if existing:
|
| 836 |
+
# Update existing
|
| 837 |
+
c.execute(
|
| 838 |
+
"""UPDATE feedbacks
|
| 839 |
+
SET feedback_type = ?, comment = ?, created_at = CURRENT_TIMESTAMP
|
| 840 |
+
WHERE id = ?""",
|
| 841 |
+
(feedback.feedback_type, feedback.comment, existing['id'])
|
| 842 |
+
)
|
| 843 |
+
print(f"✅ Updated feedback for message {feedback.message_id}")
|
| 844 |
+
else:
|
| 845 |
+
# Create new
|
| 846 |
+
c.execute(
|
| 847 |
+
"""INSERT INTO feedbacks (message_id, user_id, feedback_type, comment)
|
| 848 |
+
VALUES (?, ?, ?, ?)""",
|
| 849 |
+
(feedback.message_id, current_user['user_id'],
|
| 850 |
+
feedback.feedback_type, feedback.comment)
|
| 851 |
+
)
|
| 852 |
+
print(f"✅ Created feedback for message {feedback.message_id}")
|
| 853 |
+
|
| 854 |
+
conn.commit()
|
| 855 |
+
conn.close()
|
| 856 |
+
|
| 857 |
+
return {
|
| 858 |
+
"success": True,
|
| 859 |
+
"message": "Feedback submitted successfully"
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
except Exception as e:
|
| 863 |
+
conn.close()
|
| 864 |
+
print(f"❌ Error submitting feedback: {e}")
|
| 865 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 866 |
+
|
| 867 |
+
@app.get("/admin/get-feedbacks")
|
| 868 |
+
async def get_all_feedbacks(current_user: dict = Depends(get_current_user)):
|
| 869 |
+
"""Get all feedback with details"""
|
| 870 |
+
if current_user["role"] != "admin":
|
| 871 |
+
raise HTTPException(status_code=403, detail="Admin access required")
|
| 872 |
+
|
| 873 |
+
conn = get_db()
|
| 874 |
+
c = conn.cursor()
|
| 875 |
+
|
| 876 |
+
try:
|
| 877 |
+
c.execute("""
|
| 878 |
+
SELECT
|
| 879 |
+
f.id,
|
| 880 |
+
f.message_id,
|
| 881 |
+
f.feedback_type,
|
| 882 |
+
f.comment,
|
| 883 |
+
f.created_at,
|
| 884 |
+
m.content as message_content,
|
| 885 |
+
m.sender as message_sender,
|
| 886 |
+
m.role as message_role,
|
| 887 |
+
u.email as user_email,
|
| 888 |
+
c.title as conversation_title,
|
| 889 |
+
c.id as conversation_id
|
| 890 |
+
FROM feedbacks f
|
| 891 |
+
JOIN messages m ON f.message_id = m.id
|
| 892 |
+
JOIN users u ON f.user_id = u.id
|
| 893 |
+
JOIN conversations c ON m.conversation_id = c.id
|
| 894 |
+
ORDER BY f.created_at DESC
|
| 895 |
+
""")
|
| 896 |
+
|
| 897 |
+
feedbacks = [dict(row) for row in c.fetchall()]
|
| 898 |
+
conn.close()
|
| 899 |
+
|
| 900 |
+
return {"feedbacks": feedbacks}
|
| 901 |
+
|
| 902 |
+
except Exception as e:
|
| 903 |
+
conn.close()
|
| 904 |
+
print(f"❌ Error fetching feedbacks: {e}")
|
| 905 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 906 |
+
|
| 907 |
+
# =======================
|
| 908 |
+
# Admin Endpoints - Courses
|
| 909 |
+
# =======================
|
| 910 |
+
@app.post("/admin/create-course")
|
| 911 |
+
async def create_course(
|
| 912 |
+
course: CourseCreate,
|
| 913 |
+
current_user: dict = Depends(get_current_user)
|
| 914 |
+
):
|
| 915 |
+
"""Create a new course"""
|
| 916 |
+
if current_user["role"] != "admin":
|
| 917 |
+
raise HTTPException(status_code=403, detail="Admin access required")
|
| 918 |
+
|
| 919 |
+
conn = get_db()
|
| 920 |
+
c = conn.cursor()
|
| 921 |
+
|
| 922 |
+
try:
|
| 923 |
+
# Check if exists
|
| 924 |
+
c.execute("SELECT id FROM courses WHERE name = ?", (course.name,))
|
| 925 |
+
if c.fetchone():
|
| 926 |
+
conn.close()
|
| 927 |
+
raise HTTPException(status_code=400, detail="Course already exists")
|
| 928 |
+
|
| 929 |
+
# Insert
|
| 930 |
+
c.execute(
|
| 931 |
+
"INSERT INTO courses (name, description, admin_id) VALUES (?, ?, ?)",
|
| 932 |
+
(course.name, course.description, current_user['user_id'])
|
| 933 |
+
)
|
| 934 |
+
course_id = c.lastrowid
|
| 935 |
+
conn.commit()
|
| 936 |
+
conn.close()
|
| 937 |
+
|
| 938 |
+
print(f"✅ Course created: {course.name} (ID: {course_id})")
|
| 939 |
+
|
| 940 |
+
return {
|
| 941 |
+
"success": True,
|
| 942 |
+
"message": "Course created successfully",
|
| 943 |
+
"course_id": course_id,
|
| 944 |
+
"name": course.name
|
| 945 |
+
}
|
| 946 |
+
|
| 947 |
+
except HTTPException:
|
| 948 |
+
raise
|
| 949 |
+
except Exception as e:
|
| 950 |
+
conn.close()
|
| 951 |
+
print(f"❌ Error creating course: {e}")
|
| 952 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 953 |
+
|
| 954 |
+
@app.get("/admin/get-courses")
|
| 955 |
+
async def get_courses(current_user: dict = Depends(get_current_user)):
|
| 956 |
+
"""Get all courses with lecture counts"""
|
| 957 |
+
if current_user["role"] != "admin":
|
| 958 |
+
raise HTTPException(status_code=403, detail="Admin access required")
|
| 959 |
+
|
| 960 |
+
conn = get_db()
|
| 961 |
+
c = conn.cursor()
|
| 962 |
+
|
| 963 |
+
try:
|
| 964 |
+
c.execute("""
|
| 965 |
+
SELECT
|
| 966 |
+
c.id,
|
| 967 |
+
c.name,
|
| 968 |
+
c.description,
|
| 969 |
+
c.created_at,
|
| 970 |
+
COUNT(l.id) as lecture_count
|
| 971 |
+
FROM courses c
|
| 972 |
+
LEFT JOIN lectures l ON c.name = l.subject
|
| 973 |
+
GROUP BY c.id
|
| 974 |
+
ORDER BY c.created_at DESC
|
| 975 |
+
""")
|
| 976 |
+
|
| 977 |
+
courses = []
|
| 978 |
+
for row in c.fetchall():
|
| 979 |
+
courses.append({
|
| 980 |
+
"id": row[0],
|
| 981 |
+
"name": row[1],
|
| 982 |
+
"description": row[2],
|
| 983 |
+
"created_at": row[3],
|
| 984 |
+
"lecture_count": row[4]
|
| 985 |
+
})
|
| 986 |
+
|
| 987 |
+
conn.close()
|
| 988 |
+
return {"courses": courses}
|
| 989 |
+
|
| 990 |
+
except Exception as e:
|
| 991 |
+
conn.close()
|
| 992 |
+
print(f"❌ Error fetching courses: {e}")
|
| 993 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 994 |
+
|
| 995 |
+
@app.get("/admin/course/{course_name}/lectures")
|
| 996 |
+
async def get_course_lectures(
|
| 997 |
+
course_name: str,
|
| 998 |
+
current_user: dict = Depends(get_current_user)
|
| 999 |
+
):
|
| 1000 |
+
"""Get lectures for a course"""
|
| 1001 |
+
if current_user["role"] != "admin":
|
| 1002 |
+
raise HTTPException(status_code=403, detail="Admin access required")
|
| 1003 |
+
|
| 1004 |
+
conn = get_db()
|
| 1005 |
+
c = conn.cursor()
|
| 1006 |
+
|
| 1007 |
+
try:
|
| 1008 |
+
c.execute("""
|
| 1009 |
+
SELECT id, filename, filepath, subject, uploaded_at,
|
| 1010 |
+
processing_status, total_chunks, total_characters
|
| 1011 |
+
FROM lectures
|
| 1012 |
+
WHERE subject = ?
|
| 1013 |
+
ORDER BY uploaded_at DESC
|
| 1014 |
+
""", (course_name,))
|
| 1015 |
+
|
| 1016 |
+
lectures = [dict(row) for row in c.fetchall()]
|
| 1017 |
+
conn.close()
|
| 1018 |
+
|
| 1019 |
+
return {
|
| 1020 |
+
"course_name": course_name,
|
| 1021 |
+
"lectures": lectures
|
| 1022 |
+
}
|
| 1023 |
+
|
| 1024 |
+
except Exception as e:
|
| 1025 |
+
conn.close()
|
| 1026 |
+
print(f"❌ Error fetching course lectures: {e}")
|
| 1027 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 1028 |
+
|
| 1029 |
+
@app.delete("/admin/delete-course/{course_id}")
|
| 1030 |
+
async def delete_course(
|
| 1031 |
+
course_id: int,
|
| 1032 |
+
current_user: dict = Depends(get_current_user)
|
| 1033 |
+
):
|
| 1034 |
+
"""Delete a course"""
|
| 1035 |
+
if current_user["role"] != "admin":
|
| 1036 |
+
raise HTTPException(status_code=403, detail="Admin access required")
|
| 1037 |
+
|
| 1038 |
+
conn = get_db()
|
| 1039 |
+
c = conn.cursor()
|
| 1040 |
+
|
| 1041 |
+
try:
|
| 1042 |
+
c.execute("SELECT name FROM courses WHERE id = ?", (course_id,))
|
| 1043 |
+
course = c.fetchone()
|
| 1044 |
+
|
| 1045 |
+
if not course:
|
| 1046 |
+
conn.close()
|
| 1047 |
+
raise HTTPException(status_code=404, detail="Course not found")
|
| 1048 |
+
|
| 1049 |
+
course_name = course[0]
|
| 1050 |
+
c.execute("DELETE FROM courses WHERE id = ?", (course_id,))
|
| 1051 |
+
conn.commit()
|
| 1052 |
+
conn.close()
|
| 1053 |
+
|
| 1054 |
+
print(f"🗑️ Course deleted: {course_name}")
|
| 1055 |
+
|
| 1056 |
+
return {
|
| 1057 |
+
"success": True,
|
| 1058 |
+
"message": f"Course '{course_name}' deleted",
|
| 1059 |
+
"course_id": course_id
|
| 1060 |
+
}
|
| 1061 |
+
|
| 1062 |
+
except HTTPException:
|
| 1063 |
+
raise
|
| 1064 |
+
except Exception as e:
|
| 1065 |
+
conn.close()
|
| 1066 |
+
print(f"❌ Error deleting course: {e}")
|
| 1067 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 1068 |
+
|
| 1069 |
+
# =======================
|
| 1070 |
+
# Admin Endpoints - Lectures & Stats
|
| 1071 |
+
# =======================
|
| 1072 |
+
@app.get("/admin/get-users")
|
| 1073 |
+
async def get_users(current_user: dict = Depends(get_current_user)):
|
| 1074 |
+
"""Get all users"""
|
| 1075 |
+
if current_user["role"] != "admin":
|
| 1076 |
+
raise HTTPException(status_code=403, detail="Admin access required")
|
| 1077 |
+
|
| 1078 |
+
conn = get_db()
|
| 1079 |
+
c = conn.cursor()
|
| 1080 |
+
c.execute("SELECT id, email, role, created_at FROM users ORDER BY created_at DESC")
|
| 1081 |
+
users = [dict(row) for row in c.fetchall()]
|
| 1082 |
+
conn.close()
|
| 1083 |
+
|
| 1084 |
+
return {"users": users}
|
| 1085 |
+
|
| 1086 |
+
@app.get("/admin/get-lectures")
|
| 1087 |
+
async def get_lectures(current_user: dict = Depends(get_current_user)):
|
| 1088 |
+
"""Get all lectures"""
|
| 1089 |
+
if current_user["role"] != "admin":
|
| 1090 |
+
raise HTTPException(status_code=403, detail="Admin access required")
|
| 1091 |
+
|
| 1092 |
+
conn = get_db()
|
| 1093 |
+
c = conn.cursor()
|
| 1094 |
+
|
| 1095 |
+
try:
|
| 1096 |
+
c.execute("""
|
| 1097 |
+
SELECT id, filename, subject, uploaded_at,
|
| 1098 |
+
processing_status, total_chunks, total_characters, error_message
|
| 1099 |
+
FROM lectures
|
| 1100 |
+
ORDER BY uploaded_at DESC
|
| 1101 |
+
""")
|
| 1102 |
+
|
| 1103 |
+
lectures = [dict(row) for row in c.fetchall()]
|
| 1104 |
+
conn.close()
|
| 1105 |
+
|
| 1106 |
+
return {"lectures": lectures}
|
| 1107 |
+
|
| 1108 |
+
except Exception as e:
|
| 1109 |
+
conn.close()
|
| 1110 |
+
print(f"❌ Error in get_lectures: {e}")
|
| 1111 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 1112 |
+
|
| 1113 |
+
@app.post("/admin/upload-lecture")
|
| 1114 |
+
async def upload_lecture(
|
| 1115 |
+
file: UploadFile = File(...),
|
| 1116 |
+
subject: str = Form(...),
|
| 1117 |
+
current_user: dict = Depends(get_current_user)
|
| 1118 |
+
):
|
| 1119 |
+
"""Upload and process lecture"""
|
| 1120 |
+
|
| 1121 |
+
print(f"\n{'='*60}")
|
| 1122 |
+
print(f"📤 Upload Request")
|
| 1123 |
+
print(f" File: {file.filename}")
|
| 1124 |
+
print(f" Course: {subject}")
|
| 1125 |
+
print(f" User: {current_user['user_id']}")
|
| 1126 |
+
print(f"{'='*60}\n")
|
| 1127 |
+
|
| 1128 |
+
if current_user['role'] != 'admin':
|
| 1129 |
+
raise HTTPException(status_code=403, detail="Access denied")
|
| 1130 |
+
|
| 1131 |
+
file_ext = os.path.splitext(file.filename)[1].lower()
|
| 1132 |
+
if file_ext != '.pdf':
|
| 1133 |
+
raise HTTPException(status_code=400, detail="Only PDF files allowed")
|
| 1134 |
+
|
| 1135 |
+
if not subject or subject.strip() == "":
|
| 1136 |
+
raise HTTPException(status_code=400, detail="Course name required")
|
| 1137 |
+
|
| 1138 |
+
unique_filename = f"{uuid.uuid4()}{file_ext}"
|
| 1139 |
+
filepath = os.path.join(LECTURES_DIR, unique_filename)
|
| 1140 |
+
|
| 1141 |
+
try:
|
| 1142 |
+
with open(filepath, "wb") as f:
|
| 1143 |
+
content = await file.read()
|
| 1144 |
+
f.write(content)
|
| 1145 |
+
print(f"✅ File saved: {filepath}")
|
| 1146 |
+
except Exception as e:
|
| 1147 |
+
print(f"❌ Failed to save: {e}")
|
| 1148 |
+
raise HTTPException(status_code=500, detail=f"Save error: {str(e)}")
|
| 1149 |
+
|
| 1150 |
+
lecture_id = None
|
| 1151 |
+
|
| 1152 |
+
try:
|
| 1153 |
+
conn = get_db()
|
| 1154 |
+
c = conn.cursor()
|
| 1155 |
+
|
| 1156 |
+
c.execute(
|
| 1157 |
+
"""INSERT INTO lectures
|
| 1158 |
+
(admin_id, filename, filepath, subject, uploaded_at, processing_status)
|
| 1159 |
+
VALUES (?, ?, ?, ?, datetime('now'), ?)""",
|
| 1160 |
+
(current_user['user_id'], file.filename, filepath, subject.strip(), 'processing')
|
| 1161 |
+
)
|
| 1162 |
+
lecture_id = c.lastrowid
|
| 1163 |
+
conn.commit()
|
| 1164 |
+
conn.close()
|
| 1165 |
+
|
| 1166 |
+
print(f"✅ Lecture saved to DB: {lecture_id}")
|
| 1167 |
+
|
| 1168 |
+
# Process PDF (uncomment when ready)
|
| 1169 |
+
loop = asyncio.get_event_loop()
|
| 1170 |
+
result = await loop.run_in_executor(
|
| 1171 |
+
executor,
|
| 1172 |
+
process_new_pdf,
|
| 1173 |
+
filepath,
|
| 1174 |
+
subject.strip()
|
| 1175 |
+
)
|
| 1176 |
+
|
| 1177 |
+
if not result['success']:
|
| 1178 |
+
raise Exception(result.get('error', 'Processing failed'))
|
| 1179 |
+
|
| 1180 |
+
conn = get_db()
|
| 1181 |
+
c = conn.cursor()
|
| 1182 |
+
c.execute(
|
| 1183 |
+
"""UPDATE lectures
|
| 1184 |
+
SET processing_status = 'completed',
|
| 1185 |
+
total_chunks = ?,
|
| 1186 |
+
total_characters = ?
|
| 1187 |
+
WHERE id = ?""",
|
| 1188 |
+
(result['total_chunks'], result['total_characters'], lecture_id)
|
| 1189 |
+
)
|
| 1190 |
+
conn.commit()
|
| 1191 |
+
conn.close()
|
| 1192 |
+
|
| 1193 |
+
print(f"✅ Processing completed")
|
| 1194 |
+
|
| 1195 |
+
return {
|
| 1196 |
+
"success": True,
|
| 1197 |
+
"message": "Lecture uploaded successfully",
|
| 1198 |
+
"lecture_id": lecture_id,
|
| 1199 |
+
"filename": file.filename,
|
| 1200 |
+
"subject": subject,
|
| 1201 |
+
"status": "completed",
|
| 1202 |
+
"stats": {
|
| 1203 |
+
"total_chunks": result['total_chunks'],
|
| 1204 |
+
"total_characters": result['total_characters']
|
| 1205 |
+
}
|
| 1206 |
+
}
|
| 1207 |
+
|
| 1208 |
+
except Exception as e:
|
| 1209 |
+
error_msg = str(e)
|
| 1210 |
+
print(f"❌ Error: {error_msg}")
|
| 1211 |
+
|
| 1212 |
+
if lecture_id:
|
| 1213 |
+
try:
|
| 1214 |
+
conn = get_db()
|
| 1215 |
+
c = conn.cursor()
|
| 1216 |
+
c.execute(
|
| 1217 |
+
"""UPDATE lectures
|
| 1218 |
+
SET processing_status = 'failed', error_message = ?
|
| 1219 |
+
WHERE id = ?""",
|
| 1220 |
+
(error_msg, lecture_id)
|
| 1221 |
+
)
|
| 1222 |
+
conn.commit()
|
| 1223 |
+
conn.close()
|
| 1224 |
+
except Exception as db_error:
|
| 1225 |
+
print(f"⚠️ Failed to update error: {db_error}")
|
| 1226 |
+
|
| 1227 |
+
if os.path.exists(filepath):
|
| 1228 |
+
try:
|
| 1229 |
+
os.remove(filepath)
|
| 1230 |
+
except:
|
| 1231 |
+
pass
|
| 1232 |
+
|
| 1233 |
+
raise HTTPException(status_code=500, detail=f"Processing error: {error_msg}")
|
| 1234 |
+
|
| 1235 |
+
@app.delete("/admin/delete-lecture/{lecture_id}")
|
| 1236 |
+
async def delete_lecture(
|
| 1237 |
+
lecture_id: int,
|
| 1238 |
+
current_user: dict = Depends(get_current_user)
|
| 1239 |
+
):
|
| 1240 |
+
"""Delete a lecture"""
|
| 1241 |
+
if current_user["role"] != "admin":
|
| 1242 |
+
raise HTTPException(status_code=403, detail="Admin access required")
|
| 1243 |
+
|
| 1244 |
+
conn = get_db()
|
| 1245 |
+
c = conn.cursor()
|
| 1246 |
+
|
| 1247 |
+
c.execute("SELECT filepath FROM lectures WHERE id = ?", (lecture_id,))
|
| 1248 |
+
lecture = c.fetchone()
|
| 1249 |
+
|
| 1250 |
+
if not lecture:
|
| 1251 |
+
conn.close()
|
| 1252 |
+
raise HTTPException(status_code=404, detail="Lecture not found")
|
| 1253 |
+
|
| 1254 |
+
filepath = lecture[0]
|
| 1255 |
+
|
| 1256 |
+
c.execute("DELETE FROM lectures WHERE id = ?", (lecture_id,))
|
| 1257 |
+
conn.commit()
|
| 1258 |
+
conn.close()
|
| 1259 |
+
|
| 1260 |
+
if os.path.exists(filepath):
|
| 1261 |
+
try:
|
| 1262 |
+
os.remove(filepath)
|
| 1263 |
+
print(f"🗑️ Deleted file: {filepath}")
|
| 1264 |
+
except Exception as e:
|
| 1265 |
+
print(f"⚠️ Could not delete file: {e}")
|
| 1266 |
+
|
| 1267 |
+
return {
|
| 1268 |
+
"success": True,
|
| 1269 |
+
"message": "Lecture deleted",
|
| 1270 |
+
"lecture_id": lecture_id
|
| 1271 |
+
}
|
| 1272 |
+
|
| 1273 |
+
@app.get("/admin/get-stats")
|
| 1274 |
+
async def get_stats(current_user: dict = Depends(get_current_user)):
|
| 1275 |
+
"""Get comprehensive statistics"""
|
| 1276 |
+
if current_user["role"] != "admin":
|
| 1277 |
+
raise HTTPException(status_code=403, detail="Admin access required")
|
| 1278 |
+
|
| 1279 |
+
conn = get_db()
|
| 1280 |
+
c = conn.cursor()
|
| 1281 |
+
|
| 1282 |
+
try:
|
| 1283 |
+
# Users
|
| 1284 |
+
c.execute("SELECT COUNT(*) FROM users WHERE role = 'student'")
|
| 1285 |
+
total_students = c.fetchone()[0]
|
| 1286 |
+
|
| 1287 |
+
# Lectures
|
| 1288 |
+
c.execute("SELECT COUNT(*) FROM lectures")
|
| 1289 |
+
total_lectures = c.fetchone()[0]
|
| 1290 |
+
|
| 1291 |
+
c.execute("SELECT COUNT(*) FROM lectures WHERE processing_status = 'completed'")
|
| 1292 |
+
completed_lectures = c.fetchone()[0]
|
| 1293 |
+
|
| 1294 |
+
c.execute("SELECT COUNT(*) FROM lectures WHERE processing_status = 'failed'")
|
| 1295 |
+
failed_lectures = c.fetchone()[0]
|
| 1296 |
+
|
| 1297 |
+
# Courses
|
| 1298 |
+
c.execute("SELECT COUNT(*) FROM courses")
|
| 1299 |
+
total_courses = c.fetchone()[0]
|
| 1300 |
+
|
| 1301 |
+
# Activity
|
| 1302 |
+
c.execute("SELECT COUNT(*) FROM conversations")
|
| 1303 |
+
total_conversations = c.fetchone()[0]
|
| 1304 |
+
|
| 1305 |
+
c.execute("SELECT COUNT(*) FROM messages")
|
| 1306 |
+
total_messages = c.fetchone()[0]
|
| 1307 |
+
|
| 1308 |
+
# Feedback
|
| 1309 |
+
c.execute("SELECT COUNT(*) FROM feedbacks WHERE feedback_type = 'positive'")
|
| 1310 |
+
positive_feedbacks = c.fetchone()[0]
|
| 1311 |
+
|
| 1312 |
+
c.execute("SELECT COUNT(*) FROM feedbacks WHERE feedback_type = 'negative'")
|
| 1313 |
+
negative_feedbacks = c.fetchone()[0]
|
| 1314 |
+
|
| 1315 |
+
# Content stats
|
| 1316 |
+
c.execute("SELECT SUM(total_chunks) FROM lectures WHERE processing_status = 'completed'")
|
| 1317 |
+
result = c.fetchone()[0]
|
| 1318 |
+
total_chunks = result if result else 0
|
| 1319 |
+
|
| 1320 |
+
c.execute("SELECT SUM(total_characters) FROM lectures WHERE processing_status = 'completed'")
|
| 1321 |
+
result = c.fetchone()[0]
|
| 1322 |
+
total_characters = result if result else 0
|
| 1323 |
+
|
| 1324 |
+
conn.close()
|
| 1325 |
+
|
| 1326 |
+
return {
|
| 1327 |
+
"stats": {
|
| 1328 |
+
"users": {
|
| 1329 |
+
"total_students": total_students
|
| 1330 |
+
},
|
| 1331 |
+
"courses": {
|
| 1332 |
+
"total": total_courses
|
| 1333 |
+
},
|
| 1334 |
+
"lectures": {
|
| 1335 |
+
"total": total_lectures,
|
| 1336 |
+
"completed": completed_lectures,
|
| 1337 |
+
"failed": failed_lectures,
|
| 1338 |
+
"processing": total_lectures - completed_lectures - failed_lectures
|
| 1339 |
+
},
|
| 1340 |
+
"content": {
|
| 1341 |
+
"total_chunks": total_chunks,
|
| 1342 |
+
"total_characters": total_characters
|
| 1343 |
+
},
|
| 1344 |
+
"activity": {
|
| 1345 |
+
"total_conversations": total_conversations,
|
| 1346 |
+
"total_messages": total_messages
|
| 1347 |
+
},
|
| 1348 |
+
"feedback": {
|
| 1349 |
+
"positive": positive_feedbacks,
|
| 1350 |
+
"negative": negative_feedbacks,
|
| 1351 |
+
"total": positive_feedbacks + negative_feedbacks
|
| 1352 |
+
}
|
| 1353 |
+
}
|
| 1354 |
+
}
|
| 1355 |
+
|
| 1356 |
+
except Exception as e:
|
| 1357 |
+
conn.close()
|
| 1358 |
+
print(f"❌ Error in get_stats: {e}")
|
| 1359 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 1360 |
+
|
| 1361 |
+
# =======================
|
| 1362 |
+
# User Info
|
| 1363 |
+
# =======================
|
| 1364 |
+
@app.get("/user/me")
|
| 1365 |
+
def get_me(current_user: dict = Depends(get_current_user)):
|
| 1366 |
+
"""Get current user info"""
|
| 1367 |
+
return current_user
|
| 1368 |
+
|
| 1369 |
+
# =======================
|
| 1370 |
+
# Health Check
|
| 1371 |
+
# =======================
|
| 1372 |
+
@app.get("/health")
|
| 1373 |
+
async def health_check():
|
| 1374 |
+
"""Health check endpoint"""
|
| 1375 |
+
return {
|
| 1376 |
+
"status": "healthy",
|
| 1377 |
+
"database": "connected",
|
| 1378 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
| 1379 |
+
"version": "3.1.0"
|
| 1380 |
+
}
|
| 1381 |
+
|
| 1382 |
+
# =======================
|
| 1383 |
+
# Run Server
|
| 1384 |
+
# =======================
|
| 1385 |
+
if __name__ == "__main__":
|
| 1386 |
+
import uvicorn
|
| 1387 |
+
print("\n" + "="*60)
|
| 1388 |
+
print("🚀 Starting University AI Chatbot API")
|
| 1389 |
+
print("="*60)
|
| 1390 |
+
print(f"🌐 API URL: http://localhost:8080")
|
| 1391 |
+
print(f"📖 Docs: http://localhost:8080/docs")
|
| 1392 |
+
print(f"👤 Admin: admin@university.edu / Admin123")
|
| 1393 |
+
print("="*60 + "\n")
|
| 1394 |
+
|
| 1395 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
process_pdf.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# scr/process_pdf.py
|
| 2 |
+
"""
|
| 3 |
+
معالج PDF يستخدم الدوال الموجودة
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
import os
|
| 8 |
+
import PyPDF2
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import traceback
|
| 11 |
+
import re
|
| 12 |
+
|
| 13 |
+
# Load environment
|
| 14 |
+
load_dotenv()
|
| 15 |
+
|
| 16 |
+
QDRANT_URL = os.getenv("QDRANT_URL")
|
| 17 |
+
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
|
| 18 |
+
|
| 19 |
+
# ✅ Import الدوال الصحيحة بالـ parameters الصح
|
| 20 |
+
try:
|
| 21 |
+
from clean_text import clean_text
|
| 22 |
+
print("✅ Imported clean_text")
|
| 23 |
+
except Exception as e:
|
| 24 |
+
print(f"⚠️ Could not import clean_text: {e}")
|
| 25 |
+
# Fallback implementation
|
| 26 |
+
def clean_text(text):
|
| 27 |
+
text = text.encode("utf-8", "ignore").decode("utf-8", "ignore")
|
| 28 |
+
text = re.sub(r"\s+", " ", text)
|
| 29 |
+
text = re.sub(r"[^\w\s.,?!\-–—/\n]+", "", text)
|
| 30 |
+
text = re.sub(r"\n+", "\n", text)
|
| 31 |
+
return text.strip()
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
from chunk_text import chunk_text
|
| 35 |
+
print("✅ Imported chunk_text")
|
| 36 |
+
except Exception as e:
|
| 37 |
+
print(f"⚠️ Could not import chunk_text: {e}")
|
| 38 |
+
# Fallback implementation
|
| 39 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 40 |
+
|
| 41 |
+
def chunk_text(text):
|
| 42 |
+
splitter = RecursiveCharacterTextSplitter(
|
| 43 |
+
chunk_size=500,
|
| 44 |
+
chunk_overlap=50,
|
| 45 |
+
length_function=len
|
| 46 |
+
)
|
| 47 |
+
return splitter.split_text(text)
|
| 48 |
+
|
| 49 |
+
try:
|
| 50 |
+
from embedding import embed_single_file
|
| 51 |
+
print("✅ Imported embed_single_file")
|
| 52 |
+
except Exception as e:
|
| 53 |
+
print(f"⚠️ Could not import embed_single_file: {e}")
|
| 54 |
+
print(f"⚠️ Make sure embedding.py has the embed_single_file function!")
|
| 55 |
+
raise Exception("embed_single_file function is required but not found")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
# ======================================================
|
| 59 |
+
# Extract text from PDF
|
| 60 |
+
# ======================================================
|
| 61 |
+
def extract_pdf_text(pdf_path):
|
| 62 |
+
"""استخراج النص من PDF"""
|
| 63 |
+
try:
|
| 64 |
+
text = ""
|
| 65 |
+
with open(pdf_path, "rb") as file:
|
| 66 |
+
reader = PyPDF2.PdfReader(file)
|
| 67 |
+
|
| 68 |
+
# Check if encrypted
|
| 69 |
+
if reader.is_encrypted:
|
| 70 |
+
try:
|
| 71 |
+
reader.decrypt('')
|
| 72 |
+
except:
|
| 73 |
+
raise Exception("PDF is encrypted")
|
| 74 |
+
|
| 75 |
+
# Extract from all pages
|
| 76 |
+
total_pages = len(reader.pages)
|
| 77 |
+
print(f" 📄 Total pages: {total_pages}")
|
| 78 |
+
|
| 79 |
+
for page_num, page in enumerate(reader.pages):
|
| 80 |
+
try:
|
| 81 |
+
page_text = page.extract_text()
|
| 82 |
+
if page_text:
|
| 83 |
+
text += page_text + "\n"
|
| 84 |
+
except Exception as e:
|
| 85 |
+
print(f" ⚠️ Error on page {page_num + 1}: {e}")
|
| 86 |
+
continue
|
| 87 |
+
|
| 88 |
+
return text
|
| 89 |
+
|
| 90 |
+
except Exception as e:
|
| 91 |
+
print(f"❌ Error extracting PDF: {e}")
|
| 92 |
+
raise
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
# ======================================================
|
| 96 |
+
# Save chunks to file
|
| 97 |
+
# ======================================================
|
| 98 |
+
def save_chunks_to_file(chunks, pdf_filename, subject_name):
|
| 99 |
+
"""
|
| 100 |
+
حفظ الـ chunks في ملف بنفس صيغة الملفات الموجودة
|
| 101 |
+
"""
|
| 102 |
+
BASE_PATH = os.getcwd()
|
| 103 |
+
CHUNKS_FOLDER = os.path.join(BASE_PATH, "data", "chunks")
|
| 104 |
+
|
| 105 |
+
# Create folder if not exists
|
| 106 |
+
os.makedirs(CHUNKS_FOLDER, exist_ok=True)
|
| 107 |
+
|
| 108 |
+
# Create filename: SubjectName1.txt (same format as existing files)
|
| 109 |
+
pdf_name = Path(pdf_filename).stem
|
| 110 |
+
match = re.search(r"(\d+)", pdf_name)
|
| 111 |
+
number = match.group(1) if match else "1"
|
| 112 |
+
|
| 113 |
+
chunk_filename = f"{subject_name}{number}.txt"
|
| 114 |
+
chunk_filepath = os.path.join(CHUNKS_FOLDER, chunk_filename)
|
| 115 |
+
|
| 116 |
+
# Save chunks with separator ---CHUNK---
|
| 117 |
+
with open(chunk_filepath, "w", encoding="utf-8") as f:
|
| 118 |
+
f.write("---CHUNK---\n".join(chunks))
|
| 119 |
+
|
| 120 |
+
print(f" 💾 Saved to: {chunk_filepath}")
|
| 121 |
+
|
| 122 |
+
return chunk_filename # نرجع اسم الملف فقط
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
# ======================================================
|
| 126 |
+
# Main Process Function
|
| 127 |
+
# ======================================================
|
| 128 |
+
def process_new_pdf(pdf_path, subject_name):
|
| 129 |
+
"""
|
| 130 |
+
معالجة PDF كامل باستخدام الدوال الموجودة
|
| 131 |
+
|
| 132 |
+
Args:
|
| 133 |
+
pdf_path: المسار الكامل للـ PDF
|
| 134 |
+
subject_name: اسم المادة
|
| 135 |
+
|
| 136 |
+
Returns:
|
| 137 |
+
dict: {
|
| 138 |
+
'success': bool,
|
| 139 |
+
'total_chunks': int,
|
| 140 |
+
'total_characters': int,
|
| 141 |
+
'error': str (optional)
|
| 142 |
+
}
|
| 143 |
+
"""
|
| 144 |
+
|
| 145 |
+
try:
|
| 146 |
+
filename = Path(pdf_path).name
|
| 147 |
+
print(f"\n{'='*60}")
|
| 148 |
+
print(f"🚀 Processing PDF")
|
| 149 |
+
print(f"{'='*60}")
|
| 150 |
+
print(f"📄 File: {filename}")
|
| 151 |
+
print(f"📚 Subject: {subject_name}")
|
| 152 |
+
print(f"📂 Path: {pdf_path}")
|
| 153 |
+
print(f"{'='*60}\n")
|
| 154 |
+
|
| 155 |
+
# Validate file
|
| 156 |
+
if not os.path.exists(pdf_path):
|
| 157 |
+
raise Exception(f"File not found: {pdf_path}")
|
| 158 |
+
|
| 159 |
+
file_size = os.path.getsize(pdf_path)
|
| 160 |
+
print(f"📦 File size: {file_size / 1024:.2f} KB")
|
| 161 |
+
|
| 162 |
+
if file_size == 0:
|
| 163 |
+
raise Exception("File is empty")
|
| 164 |
+
|
| 165 |
+
# Step 1: Extract text from PDF
|
| 166 |
+
print("📄 Extracting text from PDF...")
|
| 167 |
+
raw_text = extract_pdf_text(pdf_path)
|
| 168 |
+
|
| 169 |
+
if not raw_text or len(raw_text.strip()) < 50:
|
| 170 |
+
raise Exception("No readable text found in PDF")
|
| 171 |
+
|
| 172 |
+
print(f" ✓ Extracted {len(raw_text)} characters")
|
| 173 |
+
|
| 174 |
+
# Step 2: Clean text using clean_text(text)
|
| 175 |
+
print("\n🧹 Cleaning text...")
|
| 176 |
+
cleaned_text = clean_text(raw_text) # ← بتاخد text parameter واحد بس
|
| 177 |
+
print(f" ✓ Cleaned: {len(cleaned_text)} characters")
|
| 178 |
+
|
| 179 |
+
if len(cleaned_text) < 50:
|
| 180 |
+
raise Exception("Cleaned text too short")
|
| 181 |
+
|
| 182 |
+
# Step 3: Chunk text using chunk_text(text)
|
| 183 |
+
print("\n✂️ Chunking text...")
|
| 184 |
+
chunks = chunk_text(cleaned_text) # ← بتاخد text parameter واحد بس
|
| 185 |
+
print(f" ✓ Created {len(chunks)} chunks")
|
| 186 |
+
|
| 187 |
+
if not chunks or len(chunks) == 0:
|
| 188 |
+
raise Exception("No chunks created")
|
| 189 |
+
|
| 190 |
+
# Preview first chunk
|
| 191 |
+
if chunks:
|
| 192 |
+
preview = chunks[0][:100] + "..." if len(chunks[0]) > 100 else chunks[0]
|
| 193 |
+
print(f" 📝 First chunk preview: {preview}")
|
| 194 |
+
|
| 195 |
+
# Step 4: Save chunks to file
|
| 196 |
+
print("\n💾 Saving chunks to file...")
|
| 197 |
+
chunk_filename = save_chunks_to_file(chunks, filename, subject_name)
|
| 198 |
+
|
| 199 |
+
# Step 5: Embed and upload using embed_single_file(chunk_filename)
|
| 200 |
+
print("\n🔼 Creating embeddings and uploading to Qdrant...")
|
| 201 |
+
result = embed_single_file(chunk_filename) # ← بتاخد filename parameter واحد بس
|
| 202 |
+
|
| 203 |
+
if not result or not result.get('success'):
|
| 204 |
+
raise Exception(result.get('error', 'Upload failed'))
|
| 205 |
+
|
| 206 |
+
print(f"\n{'='*60}")
|
| 207 |
+
print(f"✅ Successfully processed {filename}")
|
| 208 |
+
print(f"{'='*60}")
|
| 209 |
+
print(f"📊 Total chunks: {result['total_chunks']}")
|
| 210 |
+
print(f"📏 Total characters: {len(cleaned_text)}")
|
| 211 |
+
print(f"{'='*60}\n")
|
| 212 |
+
|
| 213 |
+
return {
|
| 214 |
+
'success': True,
|
| 215 |
+
'total_chunks': result['total_chunks'],
|
| 216 |
+
'total_characters': len(cleaned_text)
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
except Exception as e:
|
| 220 |
+
error_msg = str(e)
|
| 221 |
+
print(f"\n{'='*60}")
|
| 222 |
+
print(f"❌ ERROR PROCESSING PDF")
|
| 223 |
+
print(f"{'='*60}")
|
| 224 |
+
print(f"Error: {error_msg}")
|
| 225 |
+
print(f"{'='*60}\n")
|
| 226 |
+
|
| 227 |
+
traceback.print_exc()
|
| 228 |
+
|
| 229 |
+
return {
|
| 230 |
+
'success': False,
|
| 231 |
+
'error': error_msg,
|
| 232 |
+
'total_chunks': 0,
|
| 233 |
+
'total_characters': 0
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
# ======================================================
|
| 238 |
+
# Test
|
| 239 |
+
# ======================================================
|
| 240 |
+
if __name__ == "__main__":
|
| 241 |
+
import sys
|
| 242 |
+
|
| 243 |
+
if len(sys.argv) > 1:
|
| 244 |
+
test_pdf = sys.argv[1]
|
| 245 |
+
test_subject = sys.argv[2] if len(sys.argv) > 2 else "Test"
|
| 246 |
+
else:
|
| 247 |
+
test_pdf = r"C:\Users\DOWN TOWN H\project\lectures\test.pdf"
|
| 248 |
+
test_subject = "Mathematics"
|
| 249 |
+
|
| 250 |
+
if os.path.exists(test_pdf):
|
| 251 |
+
result = process_new_pdf(test_pdf, test_subject)
|
| 252 |
+
print(f"\n📊 Final Result: {result}")
|
| 253 |
+
else:
|
| 254 |
+
print(f"❌ File not found: {test_pdf}")
|
| 255 |
+
print(f"\nUsage: python scr/process_pdf.py <pdf_path> [subject]")
|
rag.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dotenv import load_dotenv
|
| 2 |
+
import os
|
| 3 |
+
from qdrant_client import QdrantClient
|
| 4 |
+
from sentence_transformers import SentenceTransformer
|
| 5 |
+
from groq import Groq
|
| 6 |
+
|
| 7 |
+
# Load environment variables
|
| 8 |
+
load_dotenv()
|
| 9 |
+
QDRANT_URL = os.getenv("QDRANT_URL")
|
| 10 |
+
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
|
| 11 |
+
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
|
| 12 |
+
|
| 13 |
+
COLLECTION_NAME = "student_materials"
|
| 14 |
+
|
| 15 |
+
# Connect to Qdrant
|
| 16 |
+
client = QdrantClient(
|
| 17 |
+
url=QDRANT_URL,
|
| 18 |
+
api_key=QDRANT_API_KEY,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
# Initialize Groq client
|
| 22 |
+
groq_client = Groq(api_key=GROQ_API_KEY)
|
| 23 |
+
|
| 24 |
+
# Embedding model
|
| 25 |
+
embedder = SentenceTransformer("intfloat/e5-large")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def format_payload(p):
|
| 29 |
+
"""Make context clearer and reduce mixing between sheets/courses."""
|
| 30 |
+
text = p.payload.get("text", "")
|
| 31 |
+
course = p.payload.get("course", "Unknown Course")
|
| 32 |
+
sheet = p.payload.get("sheet_number", "Unknown Sheet")
|
| 33 |
+
|
| 34 |
+
return f"[COURSE: {course} | SHEET: {sheet}]\n{text}"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def search_qdrant(query):
|
| 38 |
+
"""Search Qdrant and return best chunks."""
|
| 39 |
+
# تحسين الـ query عشان الـ embedding يفهمه أحسن
|
| 40 |
+
enhanced_query = f"query: {query}"
|
| 41 |
+
vec = embedder.encode(enhanced_query).tolist()
|
| 42 |
+
|
| 43 |
+
results = client.query_points(
|
| 44 |
+
collection_name=COLLECTION_NAME,
|
| 45 |
+
query=vec,
|
| 46 |
+
limit=5, # زودت العدد عشان نجيب نتائج أكتر
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
chunks = []
|
| 50 |
+
print(f"📊 Found {len(results.points)} relevant chunks:")
|
| 51 |
+
for i, p in enumerate(results.points, 1):
|
| 52 |
+
print(f" {i}. Score: {p.score:.4f} | Course: {p.payload.get('course', 'N/A')} | Sheet: {p.payload.get('sheet_number', 'N/A')}")
|
| 53 |
+
chunks.append(format_payload(p))
|
| 54 |
+
|
| 55 |
+
return "\n\n---\n\n".join(chunks)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def rag_answer(question):
|
| 59 |
+
"""Generate answer using Groq's LLM with RAG context."""
|
| 60 |
+
print("\n🔍 Searching Qdrant...")
|
| 61 |
+
context = search_qdrant(question)
|
| 62 |
+
|
| 63 |
+
if not context:
|
| 64 |
+
context = "No relevant context found."
|
| 65 |
+
|
| 66 |
+
print("🤖 Generating answer using Groq...\n")
|
| 67 |
+
|
| 68 |
+
instructional_prompt = f"""
|
| 69 |
+
You are an academic AI assistant.
|
| 70 |
+
|
| 71 |
+
Use the retrieved context below to answer the question.
|
| 72 |
+
If the answer exists in the context, extract it directly.
|
| 73 |
+
If the context does NOT contain enough information, you may use your own general knowledge — but keep the answer accurate and concise.
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
Context:
|
| 77 |
+
{context}
|
| 78 |
+
|
| 79 |
+
Question:
|
| 80 |
+
{question}
|
| 81 |
+
|
| 82 |
+
Answer:
|
| 83 |
+
"""
|
| 84 |
+
|
| 85 |
+
# Groq chat completion
|
| 86 |
+
chat_completion = groq_client.chat.completions.create(
|
| 87 |
+
messages=[
|
| 88 |
+
{
|
| 89 |
+
"role": "user",
|
| 90 |
+
"content": instructional_prompt
|
| 91 |
+
}
|
| 92 |
+
],
|
| 93 |
+
model="llama-3.3-70b-versatile", # أو "mixtral-8x7b-32768" للسرعة الأعلى
|
| 94 |
+
temperature=0.1, # خليتها أقل عشان يلتزم بالـ context
|
| 95 |
+
max_tokens=1024,
|
| 96 |
+
top_p=1,
|
| 97 |
+
stream=False
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
return chat_completion.choices[0].message.content
|
| 101 |
+
|
| 102 |
+
def rag_answer_stream(question):
|
| 103 |
+
"""Generate answer using Groq's LLM with RAG context (Streaming)."""
|
| 104 |
+
print("\n🔍 Searching Qdrant...")
|
| 105 |
+
context = search_qdrant(question)
|
| 106 |
+
|
| 107 |
+
if not context:
|
| 108 |
+
context = "No relevant context found."
|
| 109 |
+
|
| 110 |
+
print("🤖 Generating answer using Groq (Streaming)...\n")
|
| 111 |
+
|
| 112 |
+
instructional_prompt = f"""
|
| 113 |
+
You are an academic AI assistant.
|
| 114 |
+
|
| 115 |
+
Use the retrieved context below to answer the question.
|
| 116 |
+
If the answer exists in the context, extract it directly.
|
| 117 |
+
If the context does NOT contain enough information, you may use your own general knowledge — but keep the answer accurate and concise.
|
| 118 |
+
|
| 119 |
+
Context:
|
| 120 |
+
{context}
|
| 121 |
+
|
| 122 |
+
Question:
|
| 123 |
+
{question}
|
| 124 |
+
|
| 125 |
+
Answer:
|
| 126 |
+
"""
|
| 127 |
+
|
| 128 |
+
stream = groq_client.chat.completions.create(
|
| 129 |
+
messages=[{"role": "user", "content": instructional_prompt}],
|
| 130 |
+
model="llama-3.3-70b-versatile",
|
| 131 |
+
temperature=0.1,
|
| 132 |
+
max_tokens=1024,
|
| 133 |
+
top_p=1,
|
| 134 |
+
stream=True
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
for chunk in stream:
|
| 138 |
+
content = chunk.choices[0].delta.content
|
| 139 |
+
if content:
|
| 140 |
+
yield content
|
| 141 |
+
|
| 142 |
+
if __name__ == "__main__":
|
| 143 |
+
print("=" * 60)
|
| 144 |
+
print("📚 Academic RAG System (Powered by Groq)")
|
| 145 |
+
print("=" * 60)
|
| 146 |
+
|
| 147 |
+
user_q = input("\n💬 Enter your question: ")
|
| 148 |
+
|
| 149 |
+
try:
|
| 150 |
+
answer = rag_answer(user_q)
|
| 151 |
+
print("\n" + "=" * 60)
|
| 152 |
+
print("✅ AI Response:")
|
| 153 |
+
print("=" * 60)
|
| 154 |
+
print(answer)
|
| 155 |
+
print("=" * 60)
|
| 156 |
+
except Exception as e:
|
| 157 |
+
print(f"\n❌ Error: {e}")
|
register.html
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" dir="ltr">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Create Account - University AI</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
:root {
|
| 15 |
+
--olive-light: #3A662A;
|
| 16 |
+
--olive-dark: #5C6E4A;
|
| 17 |
+
--bg-light: #FFFFFF;
|
| 18 |
+
--bg-dark: #1A1A1A;
|
| 19 |
+
--text-light: #2C2C2C;
|
| 20 |
+
--text-dark: #F5F5F5;
|
| 21 |
+
--card-light: #F8F9FA;
|
| 22 |
+
--card-dark: #2D2D2D;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
body {
|
| 26 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 27 |
+
min-height: 100vh;
|
| 28 |
+
display: flex;
|
| 29 |
+
align-items: center;
|
| 30 |
+
justify-content: center;
|
| 31 |
+
transition: all 0.3s ease;
|
| 32 |
+
padding: 20px;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
body.light-mode {
|
| 36 |
+
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
| 37 |
+
color: var(--text-light);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
body.dark-mode {
|
| 41 |
+
background: linear-gradient(135deg, #1a1a1a 0%, #2d3748 100%);
|
| 42 |
+
color: var(--text-dark);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.container {
|
| 46 |
+
width: 100%;
|
| 47 |
+
max-width: 450px;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.card {
|
| 51 |
+
padding: 40px;
|
| 52 |
+
border-radius: 15px;
|
| 53 |
+
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
| 54 |
+
transition: all 0.3s ease;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.light-mode .card {
|
| 58 |
+
background: var(--bg-light);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.dark-mode .card {
|
| 62 |
+
background: var(--card-dark);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.header {
|
| 66 |
+
text-align: center;
|
| 67 |
+
margin-bottom: 30px;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.logo {
|
| 71 |
+
font-size: 48px;
|
| 72 |
+
margin-bottom: 10px;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
h1 {
|
| 76 |
+
font-size: 28px;
|
| 77 |
+
margin-bottom: 10px;
|
| 78 |
+
color: var(--olive-light);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.subtitle {
|
| 82 |
+
opacity: 0.7;
|
| 83 |
+
font-size: 14px;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.form-group {
|
| 87 |
+
margin-bottom: 20px;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
label {
|
| 91 |
+
display: block;
|
| 92 |
+
margin-bottom: 8px;
|
| 93 |
+
font-weight: 500;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
input {
|
| 97 |
+
width: 100%;
|
| 98 |
+
padding: 12px 15px;
|
| 99 |
+
border-radius: 8px;
|
| 100 |
+
border: 2px solid transparent;
|
| 101 |
+
font-size: 16px;
|
| 102 |
+
transition: all 0.3s ease;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.light-mode input {
|
| 106 |
+
background: var(--card-light);
|
| 107 |
+
color: var(--text-light);
|
| 108 |
+
border-color: #e0e0e0;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.dark-mode input {
|
| 112 |
+
background: var(--bg-dark);
|
| 113 |
+
color: var(--text-dark);
|
| 114 |
+
border-color: #444;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
input:focus {
|
| 118 |
+
outline: none;
|
| 119 |
+
border-color: var(--olive-light);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.btn {
|
| 123 |
+
width: 100%;
|
| 124 |
+
padding: 14px;
|
| 125 |
+
border: none;
|
| 126 |
+
border-radius: 8px;
|
| 127 |
+
font-size: 16px;
|
| 128 |
+
font-weight: 600;
|
| 129 |
+
cursor: pointer;
|
| 130 |
+
transition: all 0.3s ease;
|
| 131 |
+
background: var(--olive-light);
|
| 132 |
+
color: white;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.btn:hover {
|
| 136 |
+
transform: translateY(-2px);
|
| 137 |
+
box-shadow: 0 5px 15px rgba(58, 102, 42, 0.4);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.btn:disabled {
|
| 141 |
+
opacity: 0.6;
|
| 142 |
+
cursor: not-allowed;
|
| 143 |
+
transform: none;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.links {
|
| 147 |
+
text-align: center;
|
| 148 |
+
margin-top: 20px;
|
| 149 |
+
font-size: 14px;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.links a {
|
| 153 |
+
color: var(--olive-light);
|
| 154 |
+
text-decoration: none;
|
| 155 |
+
font-weight: 500;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.links a:hover {
|
| 159 |
+
text-decoration: underline;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.divider {
|
| 163 |
+
margin: 20px 0;
|
| 164 |
+
text-align: center;
|
| 165 |
+
opacity: 0.5;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.alert {
|
| 169 |
+
padding: 12px;
|
| 170 |
+
border-radius: 8px;
|
| 171 |
+
margin-bottom: 20px;
|
| 172 |
+
display: none;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.alert.error {
|
| 176 |
+
background: #fee;
|
| 177 |
+
color: #c33;
|
| 178 |
+
border: 1px solid #fcc;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.alert.success {
|
| 182 |
+
background: #efe;
|
| 183 |
+
color: #3c3;
|
| 184 |
+
border: 1px solid #cfc;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.alert.show {
|
| 188 |
+
display: block;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.top-bar {
|
| 192 |
+
position: fixed;
|
| 193 |
+
top: 0;
|
| 194 |
+
left: 0;
|
| 195 |
+
right: 0;
|
| 196 |
+
padding: 15px 20px;
|
| 197 |
+
display: flex;
|
| 198 |
+
justify-content: space-between;
|
| 199 |
+
align-items: center;
|
| 200 |
+
z-index: 1000;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.light-mode .top-bar {
|
| 204 |
+
background: rgba(255,255,255,0.9);
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.dark-mode .top-bar {
|
| 208 |
+
background: rgba(45,45,45,0.9);
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.theme-toggle {
|
| 212 |
+
width: 50px;
|
| 213 |
+
height: 26px;
|
| 214 |
+
background: var(--olive-light);
|
| 215 |
+
border-radius: 13px;
|
| 216 |
+
position: relative;
|
| 217 |
+
cursor: pointer;
|
| 218 |
+
transition: all 0.3s ease;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.theme-toggle::after {
|
| 222 |
+
content: '☀️';
|
| 223 |
+
position: absolute;
|
| 224 |
+
top: 3px;
|
| 225 |
+
left: 3px;
|
| 226 |
+
width: 20px;
|
| 227 |
+
height: 20px;
|
| 228 |
+
background: white;
|
| 229 |
+
border-radius: 50%;
|
| 230 |
+
transition: all 0.3s ease;
|
| 231 |
+
display: flex;
|
| 232 |
+
align-items: center;
|
| 233 |
+
justify-content: center;
|
| 234 |
+
font-size: 12px;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.dark-mode .theme-toggle::after {
|
| 238 |
+
content: '🌙';
|
| 239 |
+
left: 27px;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.back-home {
|
| 243 |
+
padding: 10px 20px;
|
| 244 |
+
background: var(--olive-light);
|
| 245 |
+
color: white;
|
| 246 |
+
text-decoration: none;
|
| 247 |
+
border-radius: 8px;
|
| 248 |
+
font-size: 14px;
|
| 249 |
+
transition: all 0.3s ease;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.back-home:hover {
|
| 253 |
+
transform: translateY(-2px);
|
| 254 |
+
box-shadow: 0 5px 15px rgba(58, 102, 42, 0.4);
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.loading {
|
| 258 |
+
display: none;
|
| 259 |
+
text-align: center;
|
| 260 |
+
margin-top: 10px;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
.loading.show {
|
| 264 |
+
display: block;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.password-strength {
|
| 268 |
+
margin-top: 5px;
|
| 269 |
+
font-size: 12px;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.strength-weak { color: #c33; }
|
| 273 |
+
.strength-medium { color: #f90; }
|
| 274 |
+
.strength-strong { color: #3c3; }
|
| 275 |
+
|
| 276 |
+
.success-message {
|
| 277 |
+
text-align: center;
|
| 278 |
+
animation: slideUp 0.5s ease-out;
|
| 279 |
+
padding: 20px 0;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
.success-icon {
|
| 283 |
+
font-size: 60px;
|
| 284 |
+
margin-bottom: 20px;
|
| 285 |
+
display: inline-block;
|
| 286 |
+
animation: popIn 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.countdown {
|
| 290 |
+
margin-top: 15px;
|
| 291 |
+
font-size: 14px;
|
| 292 |
+
opacity: 0.7;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
@keyframes slideUp {
|
| 296 |
+
from { opacity: 0; transform: translateY(20px); }
|
| 297 |
+
to { opacity: 1; transform: translateY(0); }
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
@keyframes popIn {
|
| 301 |
+
0% { transform: scale(0); }
|
| 302 |
+
80% { transform: scale(1.1); }
|
| 303 |
+
100% { transform: scale(1); }
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
@media (max-width: 768px) {
|
| 307 |
+
body {
|
| 308 |
+
padding-top: 80px;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.card {
|
| 312 |
+
padding: 25px;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.top-bar {
|
| 316 |
+
padding: 10px 15px;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.back-home {
|
| 320 |
+
padding: 8px 15px;
|
| 321 |
+
font-size: 12px;
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
</style>
|
| 325 |
+
</head>
|
| 326 |
+
<body class="light-mode">
|
| 327 |
+
<div class="top-bar">
|
| 328 |
+
<a href="index.html" class="back-home">🏠 Home</a>
|
| 329 |
+
<div class="theme-toggle" onclick="toggleTheme()"></div>
|
| 330 |
+
</div>
|
| 331 |
+
|
| 332 |
+
<div class="container">
|
| 333 |
+
<div class="card">
|
| 334 |
+
<div class="header">
|
| 335 |
+
<div class="logo">🎓</div>
|
| 336 |
+
<h1>Create Account</h1>
|
| 337 |
+
<p class="subtitle">Join us and start your learning journey</p>
|
| 338 |
+
</div>
|
| 339 |
+
|
| 340 |
+
<div id="alert" class="alert"></div>
|
| 341 |
+
|
| 342 |
+
<div id="registerForm">
|
| 343 |
+
<div class="form-group">
|
| 344 |
+
<label for="email">University Email</label>
|
| 345 |
+
<input
|
| 346 |
+
type="email"
|
| 347 |
+
id="email"
|
| 348 |
+
name="email"
|
| 349 |
+
required
|
| 350 |
+
placeholder="example@university.edu"
|
| 351 |
+
>
|
| 352 |
+
</div>
|
| 353 |
+
|
| 354 |
+
<div class="form-group">
|
| 355 |
+
<label for="password">Password</label>
|
| 356 |
+
<input
|
| 357 |
+
type="password"
|
| 358 |
+
id="password"
|
| 359 |
+
name="password"
|
| 360 |
+
required
|
| 361 |
+
placeholder="••••••••"
|
| 362 |
+
minlength="6"
|
| 363 |
+
>
|
| 364 |
+
<div id="passwordStrength" class="password-strength"></div>
|
| 365 |
+
</div>
|
| 366 |
+
|
| 367 |
+
<div class="form-group">
|
| 368 |
+
<label for="confirmPassword">Confirm Password</label>
|
| 369 |
+
<input
|
| 370 |
+
type="password"
|
| 371 |
+
id="confirmPassword"
|
| 372 |
+
name="confirmPassword"
|
| 373 |
+
required
|
| 374 |
+
placeholder="••••••••"
|
| 375 |
+
>
|
| 376 |
+
</div>
|
| 377 |
+
|
| 378 |
+
<button type="button" class="btn" id="registerBtn" onclick="handleRegister()">
|
| 379 |
+
Create Account
|
| 380 |
+
</button>
|
| 381 |
+
|
| 382 |
+
<div class="loading" id="loading">
|
| 383 |
+
<p>Creating account...</p>
|
| 384 |
+
</div>
|
| 385 |
+
</div>
|
| 386 |
+
|
| 387 |
+
<div class="divider">───────</div>
|
| 388 |
+
|
| 389 |
+
<div class="links">
|
| 390 |
+
<p>Already have an account? <a href="login.html">Login</a></p>
|
| 391 |
+
</div>
|
| 392 |
+
</div>
|
| 393 |
+
</div>
|
| 394 |
+
|
| 395 |
+
<script>
|
| 396 |
+
const API_URL = '';
|
| 397 |
+
|
| 398 |
+
function toggleTheme() {
|
| 399 |
+
const body = document.body;
|
| 400 |
+
if (body.classList.contains('light-mode')) {
|
| 401 |
+
body.classList.remove('light-mode');
|
| 402 |
+
body.classList.add('dark-mode');
|
| 403 |
+
localStorage.setItem('theme', 'dark');
|
| 404 |
+
} else {
|
| 405 |
+
body.classList.remove('dark-mode');
|
| 406 |
+
body.classList.add('light-mode');
|
| 407 |
+
localStorage.setItem('theme', 'light');
|
| 408 |
+
}
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
document.addEventListener("DOMContentLoaded", async () => {
|
| 412 |
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
| 413 |
+
document.body.classList.remove("light-mode", "dark-mode");
|
| 414 |
+
document.body.classList.add(savedTheme + "-mode");
|
| 415 |
+
|
| 416 |
+
const token = localStorage.getItem('token');
|
| 417 |
+
const role = localStorage.getItem('role');
|
| 418 |
+
|
| 419 |
+
if (token && role) {
|
| 420 |
+
try {
|
| 421 |
+
const response = await fetch(`${API_URL}/user/me`, {
|
| 422 |
+
headers: {
|
| 423 |
+
'Authorization': `Bearer ${token}`
|
| 424 |
+
}
|
| 425 |
+
});
|
| 426 |
+
|
| 427 |
+
if (response.ok) {
|
| 428 |
+
console.log('✅ User already logged in, redirecting...');
|
| 429 |
+
if (role === 'admin') {
|
| 430 |
+
window.location.href = 'admin-dashboard.html';
|
| 431 |
+
} else {
|
| 432 |
+
window.location.href = 'chat.html';
|
| 433 |
+
}
|
| 434 |
+
} else {
|
| 435 |
+
localStorage.clear();
|
| 436 |
+
}
|
| 437 |
+
} catch (error) {
|
| 438 |
+
console.error('Token validation error:', error);
|
| 439 |
+
localStorage.clear();
|
| 440 |
+
}
|
| 441 |
+
}
|
| 442 |
+
});
|
| 443 |
+
|
| 444 |
+
function showAlert(message, type = 'error') {
|
| 445 |
+
const alert = document.getElementById('alert');
|
| 446 |
+
alert.textContent = message;
|
| 447 |
+
alert.className = `alert ${type} show`;
|
| 448 |
+
|
| 449 |
+
setTimeout(() => {
|
| 450 |
+
alert.classList.remove('show');
|
| 451 |
+
}, 1000);
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
document.getElementById('password').addEventListener('input', (e) => {
|
| 455 |
+
const password = e.target.value;
|
| 456 |
+
const strengthDiv = document.getElementById('passwordStrength');
|
| 457 |
+
|
| 458 |
+
let strength = 0;
|
| 459 |
+
if (password.length >= 8) strength++;
|
| 460 |
+
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
|
| 461 |
+
if (/\d/.test(password)) strength++;
|
| 462 |
+
if (/[^a-zA-Z\d]/.test(password)) strength++;
|
| 463 |
+
|
| 464 |
+
if (password.length === 0) {
|
| 465 |
+
strengthDiv.textContent = '';
|
| 466 |
+
} else if (strength <= 1) {
|
| 467 |
+
strengthDiv.textContent = '⚠️ Weak password';
|
| 468 |
+
strengthDiv.className = 'password-strength strength-weak';
|
| 469 |
+
} else if (strength === 2) {
|
| 470 |
+
strengthDiv.textContent = '⚡ Medium password';
|
| 471 |
+
strengthDiv.className = 'password-strength strength-medium';
|
| 472 |
+
} else {
|
| 473 |
+
strengthDiv.textContent = '✅ Strong password';
|
| 474 |
+
strengthDiv.className = 'password-strength strength-strong';
|
| 475 |
+
}
|
| 476 |
+
});
|
| 477 |
+
|
| 478 |
+
async function handleRegister() {
|
| 479 |
+
const emailInput = document.getElementById('email');
|
| 480 |
+
const passwordInput = document.getElementById('password');
|
| 481 |
+
const confirmInput = document.getElementById('confirmPassword');
|
| 482 |
+
const registerBtn = document.getElementById('registerBtn');
|
| 483 |
+
const loading = document.getElementById('loading');
|
| 484 |
+
|
| 485 |
+
if (!emailInput.reportValidity() || !passwordInput.reportValidity() || !confirmInput.reportValidity()) {
|
| 486 |
+
return;
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
const email = emailInput.value.trim();
|
| 490 |
+
const password = passwordInput.value;
|
| 491 |
+
const confirmPassword = confirmInput.value;
|
| 492 |
+
|
| 493 |
+
if (password !== confirmPassword) {
|
| 494 |
+
showAlert('Passwords do not match!');
|
| 495 |
+
return;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
if (password.length < 6) {
|
| 499 |
+
showAlert('Password must be at least 6 characters');
|
| 500 |
+
return;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
registerBtn.disabled = true;
|
| 504 |
+
loading.classList.add('show');
|
| 505 |
+
|
| 506 |
+
try {
|
| 507 |
+
const formData = new URLSearchParams();
|
| 508 |
+
formData.append('email', email);
|
| 509 |
+
formData.append('password', password);
|
| 510 |
+
|
| 511 |
+
const response = await fetch(`${API_URL}/auth/register`, {
|
| 512 |
+
method: 'POST',
|
| 513 |
+
headers: {
|
| 514 |
+
'Content-Type': 'application/x-www-form-urlencoded',
|
| 515 |
+
},
|
| 516 |
+
body: formData
|
| 517 |
+
});
|
| 518 |
+
|
| 519 |
+
let data = null;
|
| 520 |
+
const contentType = response.headers.get('content-type');
|
| 521 |
+
|
| 522 |
+
if (contentType && contentType.includes('application/json')) {
|
| 523 |
+
try {
|
| 524 |
+
data = await response.json();
|
| 525 |
+
} catch (e) {
|
| 526 |
+
console.log('No JSON response body');
|
| 527 |
+
}
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
if (response.ok) {
|
| 531 |
+
document.getElementById('registerForm').style.display = 'none';
|
| 532 |
+
document.querySelector('.divider').style.display = 'none';
|
| 533 |
+
document.querySelector('.links').style.display = 'none';
|
| 534 |
+
loading.classList.remove('show');
|
| 535 |
+
|
| 536 |
+
const successDiv = document.createElement('div');
|
| 537 |
+
successDiv.style.textAlign = 'center';
|
| 538 |
+
successDiv.style.marginTop = '30px';
|
| 539 |
+
successDiv.innerHTML = `
|
| 540 |
+
<div class="success-icon">🎉</div>
|
| 541 |
+
<h3 style="color: var(--olive-light); margin-bottom: 10px;">Account Created!</h3>
|
| 542 |
+
<p>Welcome to University AI.</p>
|
| 543 |
+
<p class="countdown">Redirecting to login...</p>
|
| 544 |
+
`;
|
| 545 |
+
|
| 546 |
+
document.querySelector('.card').appendChild(successDiv);
|
| 547 |
+
|
| 548 |
+
setTimeout(() => {
|
| 549 |
+
console.log('✅ Registration successful,now you can login');
|
| 550 |
+
window.location.replace('login.html');
|
| 551 |
+
}, 3000);
|
| 552 |
+
} else {
|
| 553 |
+
const errorMessage = data?.detail || data?.message || 'Registration failed. Please try again.';
|
| 554 |
+
showAlert(errorMessage);
|
| 555 |
+
console.error('Registration failed:', errorMessage);
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
} catch (error) {
|
| 559 |
+
console.error('Register error:', error);
|
| 560 |
+
showAlert('Connection error. Please make sure the API is running on ' + API_URL);
|
| 561 |
+
} finally {
|
| 562 |
+
registerBtn.disabled = false;
|
| 563 |
+
loading.classList.remove('show');
|
| 564 |
+
}
|
| 565 |
+
}
|
| 566 |
+
</script>
|
| 567 |
+
</body>
|
| 568 |
+
</html>
|
requierments.txt
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.104.1
|
| 2 |
+
uvicorn[standard]==0.24.0
|
| 3 |
+
python-jose[cryptography]==3.3.0
|
| 4 |
+
passlib[bcrypt]==1.7.4
|
| 5 |
+
python-multipart==0.0.6
|
| 6 |
+
python-dotenv==1.0.0
|
| 7 |
+
bcrypt==4.1.1
|
| 8 |
+
PyJWT==2.8.0
|
| 9 |
+
fastapi
|
| 10 |
+
uvicorn[standard]
|
| 11 |
+
python-dotenv
|
| 12 |
+
python-multipart
|
| 13 |
+
|
| 14 |
+
sentence-transformers
|
| 15 |
+
torch
|
| 16 |
+
|
| 17 |
+
qdrant-client
|
| 18 |
+
|
| 19 |
+
langchain
|
| 20 |
+
langchain-text-splitters
|
| 21 |
+
|
| 22 |
+
numpy
|
| 23 |
+
scikit-learn
|
| 24 |
+
|
| 25 |
+
python-jose[cryptography]
|
| 26 |
+
passlib[bcrypt]
|
| 27 |
+
bcrypt
|
| 28 |
+
PyJWT
|
requirements.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
uvicorn
|
| 2 |
+
fastapi
|
| 3 |
+
python-multipart
|
| 4 |
+
python-dotenv
|
| 5 |
+
httpx
|
| 6 |
+
pydantic
|
| 7 |
+
pydantic-settings
|
| 8 |
+
email-validator
|
| 9 |
+
pyjwt
|
| 10 |
+
bcrypt
|
| 11 |
+
qdrant-client
|
| 12 |
+
sentence-transformers
|
| 13 |
+
groq
|
| 14 |
+
PyPDF2
|
| 15 |
+
langchain-text-splitters
|
| 16 |
+
langchain-ollama
|
| 17 |
+
jinja2
|
| 18 |
+
# أضف أي مكتبات أخرى هنا (مثل torch, transformers, إلخ)
|
reset-password.html
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" dir="ltr">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Reset Password - University AI</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
:root {
|
| 15 |
+
--olive-light: #3A662A;
|
| 16 |
+
--bg-light: #FFFFFF;
|
| 17 |
+
--bg-dark: #1A1A1A;
|
| 18 |
+
--text-light: #2C2C2C;
|
| 19 |
+
--text-dark: #F5F5F5;
|
| 20 |
+
--card-light: #F8F9FA;
|
| 21 |
+
--card-dark: #2D2D2D;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
body {
|
| 25 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 26 |
+
min-height: 100vh;
|
| 27 |
+
display: flex;
|
| 28 |
+
align-items: center;
|
| 29 |
+
justify-content: center;
|
| 30 |
+
transition: all 0.3s ease;
|
| 31 |
+
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
| 32 |
+
color: var(--text-light);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.container {
|
| 36 |
+
width: 100%;
|
| 37 |
+
max-width: 450px;
|
| 38 |
+
padding: 20px;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.card {
|
| 42 |
+
background: white;
|
| 43 |
+
padding: 40px;
|
| 44 |
+
border-radius: 15px;
|
| 45 |
+
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.header {
|
| 49 |
+
text-align: center;
|
| 50 |
+
margin-bottom: 30px;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.logo {
|
| 54 |
+
font-size: 48px;
|
| 55 |
+
margin-bottom: 10px;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
h1 {
|
| 59 |
+
font-size: 28px;
|
| 60 |
+
margin-bottom: 10px;
|
| 61 |
+
color: var(--olive-light);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.subtitle {
|
| 65 |
+
opacity: 0.7;
|
| 66 |
+
font-size: 14px;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.form-group {
|
| 70 |
+
margin-bottom: 20px;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
label {
|
| 74 |
+
display: block;
|
| 75 |
+
margin-bottom: 8px;
|
| 76 |
+
font-weight: 500;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
input {
|
| 80 |
+
width: 100%;
|
| 81 |
+
padding: 12px 15px;
|
| 82 |
+
border-radius: 8px;
|
| 83 |
+
border: 2px solid #e0e0e0;
|
| 84 |
+
font-size: 16px;
|
| 85 |
+
transition: all 0.3s ease;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
input:focus {
|
| 89 |
+
outline: none;
|
| 90 |
+
border-color: var(--olive-light);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.btn {
|
| 94 |
+
width: 100%;
|
| 95 |
+
padding: 14px;
|
| 96 |
+
border: none;
|
| 97 |
+
border-radius: 8px;
|
| 98 |
+
font-size: 16px;
|
| 99 |
+
font-weight: 600;
|
| 100 |
+
cursor: pointer;
|
| 101 |
+
transition: all 0.3s ease;
|
| 102 |
+
background: var(--olive-light);
|
| 103 |
+
color: white;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.btn:hover {
|
| 107 |
+
transform: translateY(-2px);
|
| 108 |
+
box-shadow: 0 5px 15px rgba(58, 102, 42, 0.4);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.btn:disabled {
|
| 112 |
+
opacity: 0.6;
|
| 113 |
+
cursor: not-allowed;
|
| 114 |
+
transform: none;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.alert {
|
| 118 |
+
padding: 12px;
|
| 119 |
+
border-radius: 8px;
|
| 120 |
+
margin-bottom: 20px;
|
| 121 |
+
display: none;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.alert.error {
|
| 125 |
+
background: #fee;
|
| 126 |
+
color: #c33;
|
| 127 |
+
border: 1px solid #fcc;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.alert.success {
|
| 131 |
+
background: #efe;
|
| 132 |
+
color: #3c3;
|
| 133 |
+
border: 1px solid #cfc;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.alert.show {
|
| 137 |
+
display: block;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.links {
|
| 141 |
+
text-align: center;
|
| 142 |
+
margin-top: 20px;
|
| 143 |
+
font-size: 14px;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.links a {
|
| 147 |
+
color: var(--olive-light);
|
| 148 |
+
text-decoration: none;
|
| 149 |
+
font-weight: 500;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.links a:hover {
|
| 153 |
+
text-decoration: underline;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.loading {
|
| 157 |
+
display: none;
|
| 158 |
+
text-align: center;
|
| 159 |
+
margin-top: 10px;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.loading.show {
|
| 163 |
+
display: block;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.password-strength {
|
| 167 |
+
margin-top: 5px;
|
| 168 |
+
font-size: 12px;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.strength-weak { color: #c33; }
|
| 172 |
+
.strength-medium { color: #f90; }
|
| 173 |
+
.strength-strong { color: #3c3; }
|
| 174 |
+
</style>
|
| 175 |
+
</head>
|
| 176 |
+
<body>
|
| 177 |
+
<div class="container">
|
| 178 |
+
<div class="card">
|
| 179 |
+
<div class="header">
|
| 180 |
+
<div class="logo">🔐</div>
|
| 181 |
+
<h1>Reset Password</h1>
|
| 182 |
+
<p class="subtitle">Enter the code sent to your email</p>
|
| 183 |
+
</div>
|
| 184 |
+
|
| 185 |
+
<div id="alert" class="alert"></div>
|
| 186 |
+
|
| 187 |
+
<div id="resetForm">
|
| 188 |
+
<input type="hidden" id="email">
|
| 189 |
+
<div class="form-group">
|
| 190 |
+
<label for="code">Verification Code</label>
|
| 191 |
+
<input
|
| 192 |
+
type="text"
|
| 193 |
+
id="code"
|
| 194 |
+
required
|
| 195 |
+
maxlength="6"
|
| 196 |
+
placeholder="123456"
|
| 197 |
+
style="letter-spacing: 2px; font-weight: bold;"
|
| 198 |
+
>
|
| 199 |
+
<div style="text-align: right; margin-top: 8px; font-size: 14px;">
|
| 200 |
+
<a href="#" id="resendBtn" style="color: var(--olive-light); text-decoration: none;">Resend Code</a>
|
| 201 |
+
<span id="timerContainer" style="display: none; color: #666;">
|
| 202 |
+
(Wait <span id="timer">60</span>s)
|
| 203 |
+
</span>
|
| 204 |
+
</div>
|
| 205 |
+
</div>
|
| 206 |
+
|
| 207 |
+
<div class="form-group">
|
| 208 |
+
<label for="newPassword">New Password</label>
|
| 209 |
+
<input
|
| 210 |
+
type="password"
|
| 211 |
+
id="newPassword"
|
| 212 |
+
name="newPassword"
|
| 213 |
+
required
|
| 214 |
+
placeholder="••••••••"
|
| 215 |
+
minlength="6"
|
| 216 |
+
>
|
| 217 |
+
<div id="passwordStrength" class="password-strength"></div>
|
| 218 |
+
</div>
|
| 219 |
+
|
| 220 |
+
<div class="form-group">
|
| 221 |
+
<label for="confirmPassword">Confirm Password</label>
|
| 222 |
+
<input
|
| 223 |
+
type="password"
|
| 224 |
+
id="confirmPassword"
|
| 225 |
+
name="confirmPassword"
|
| 226 |
+
required
|
| 227 |
+
placeholder="••••••••"
|
| 228 |
+
>
|
| 229 |
+
</div>
|
| 230 |
+
|
| 231 |
+
<button type="button" class="btn" id="resetBtn" onclick="handleResetPassword()">
|
| 232 |
+
Reset Password
|
| 233 |
+
</button>
|
| 234 |
+
|
| 235 |
+
<div class="loading" id="loading">
|
| 236 |
+
<p>Resetting password...</p>
|
| 237 |
+
</div>
|
| 238 |
+
</div>
|
| 239 |
+
|
| 240 |
+
<div class="links">
|
| 241 |
+
<p>Remember your password? <a href="login.html">Sign In</a></p>
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
</div>
|
| 245 |
+
|
| 246 |
+
<script>
|
| 247 |
+
const API_URL = '';
|
| 248 |
+
|
| 249 |
+
// Get email from URL if present
|
| 250 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 251 |
+
const emailParam = urlParams.get('email');
|
| 252 |
+
if (emailParam) {
|
| 253 |
+
document.getElementById('email').value = emailParam;
|
| 254 |
+
} else {
|
| 255 |
+
showAlert('Email not found in URL. Please start the process again.', 'error');
|
| 256 |
+
}
|
| 257 |
+
// عرض رسالة
|
| 258 |
+
function showAlert(message, type = 'error') {
|
| 259 |
+
const alert = document.getElementById('alert');
|
| 260 |
+
alert.textContent = message;
|
| 261 |
+
alert.className = `alert ${type} show`;
|
| 262 |
+
|
| 263 |
+
setTimeout(() => {
|
| 264 |
+
alert.classList.remove('show');
|
| 265 |
+
}, 5000);
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
// فحص قوة كلمة المرور
|
| 269 |
+
document.getElementById('newPassword').addEventListener('input', (e) => {
|
| 270 |
+
const password = e.target.value;
|
| 271 |
+
const strengthDiv = document.getElementById('passwordStrength');
|
| 272 |
+
|
| 273 |
+
let strength = 0;
|
| 274 |
+
if (password.length >= 8) strength++;
|
| 275 |
+
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
|
| 276 |
+
if (/\d/.test(password)) strength++;
|
| 277 |
+
if (/[^a-zA-Z\d]/.test(password)) strength++;
|
| 278 |
+
|
| 279 |
+
if (password.length === 0) {
|
| 280 |
+
strengthDiv.textContent = '';
|
| 281 |
+
} else if (strength <= 1) {
|
| 282 |
+
strengthDiv.textContent = '⚠️ Weak password';
|
| 283 |
+
strengthDiv.className = 'password-strength strength-weak';
|
| 284 |
+
} else if (strength === 2) {
|
| 285 |
+
strengthDiv.textContent = '⚡ Medium password';
|
| 286 |
+
strengthDiv.className = 'password-strength strength-medium';
|
| 287 |
+
} else {
|
| 288 |
+
strengthDiv.textContent = '✅ Strong password';
|
| 289 |
+
strengthDiv.className = 'password-strength strength-strong';
|
| 290 |
+
}
|
| 291 |
+
});
|
| 292 |
+
|
| 293 |
+
// Restrict code input to numbers only
|
| 294 |
+
document.getElementById('code').addEventListener('input', function(e) {
|
| 295 |
+
this.value = this.value.replace(/[^0-9]/g, '');
|
| 296 |
+
});
|
| 297 |
+
|
| 298 |
+
// Timer Logic
|
| 299 |
+
function startResendTimer(duration = 60) {
|
| 300 |
+
const btn = document.getElementById('resendBtn');
|
| 301 |
+
const container = document.getElementById('timerContainer');
|
| 302 |
+
const timerSpan = document.getElementById('timer');
|
| 303 |
+
|
| 304 |
+
let timeLeft = duration;
|
| 305 |
+
|
| 306 |
+
// Disable button
|
| 307 |
+
btn.style.pointerEvents = 'none';
|
| 308 |
+
btn.style.opacity = '0.5';
|
| 309 |
+
btn.style.textDecoration = 'none';
|
| 310 |
+
|
| 311 |
+
// Show timer
|
| 312 |
+
container.style.display = 'inline';
|
| 313 |
+
timerSpan.textContent = timeLeft;
|
| 314 |
+
|
| 315 |
+
const interval = setInterval(() => {
|
| 316 |
+
timeLeft--;
|
| 317 |
+
timerSpan.textContent = timeLeft;
|
| 318 |
+
|
| 319 |
+
if (timeLeft <= 0) {
|
| 320 |
+
clearInterval(interval);
|
| 321 |
+
btn.style.pointerEvents = 'auto';
|
| 322 |
+
btn.style.opacity = '1';
|
| 323 |
+
btn.style.textDecoration = 'underline';
|
| 324 |
+
container.style.display = 'none';
|
| 325 |
+
}
|
| 326 |
+
}, 1000);
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
// Start timer on load
|
| 330 |
+
startResendTimer();
|
| 331 |
+
|
| 332 |
+
// Resend Logic
|
| 333 |
+
document.getElementById('resendBtn').addEventListener('click', async (e) => {
|
| 334 |
+
e.preventDefault();
|
| 335 |
+
const email = document.getElementById('email').value;
|
| 336 |
+
if(!email) return;
|
| 337 |
+
|
| 338 |
+
try {
|
| 339 |
+
// Reuse forgot-password endpoint to generate a new OTP
|
| 340 |
+
const response = await fetch(`${API_URL}/auth/forgot-password`, {
|
| 341 |
+
method: 'POST',
|
| 342 |
+
headers: { 'Content-Type': 'application/json' },
|
| 343 |
+
body: JSON.stringify({ email })
|
| 344 |
+
});
|
| 345 |
+
|
| 346 |
+
if (response.ok) {
|
| 347 |
+
showAlert('New code sent!', 'success');
|
| 348 |
+
startResendTimer();
|
| 349 |
+
} else {
|
| 350 |
+
const data = await response.json();
|
| 351 |
+
showAlert(data.detail || 'Failed to resend', 'error');
|
| 352 |
+
}
|
| 353 |
+
} catch (err) {
|
| 354 |
+
console.error('Resend error:', err);
|
| 355 |
+
showAlert('Connection error', 'error');
|
| 356 |
+
}
|
| 357 |
+
});
|
| 358 |
+
|
| 359 |
+
async function handleResetPassword() {
|
| 360 |
+
const email = document.getElementById('email').value;
|
| 361 |
+
const code = document.getElementById('code').value.trim();
|
| 362 |
+
const newPassword = document.getElementById('newPassword').value;
|
| 363 |
+
const confirmPassword = document.getElementById('confirmPassword').value;
|
| 364 |
+
const resetBtn = document.getElementById('resetBtn');
|
| 365 |
+
const loading = document.getElementById('loading');
|
| 366 |
+
|
| 367 |
+
// التحقق من تطابق كلمات المرور
|
| 368 |
+
if (newPassword !== confirmPassword) {
|
| 369 |
+
showAlert('Passwords do not match!');
|
| 370 |
+
return;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
if (!/^\d{6}$/.test(code)) {
|
| 374 |
+
showAlert('Code must be exactly 6 digits', 'error');
|
| 375 |
+
return;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
if (newPassword.length < 6) {
|
| 379 |
+
showAlert('Password must be at least 6 characters');
|
| 380 |
+
return;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
resetBtn.disabled = true;
|
| 384 |
+
loading.classList.add('show');
|
| 385 |
+
|
| 386 |
+
try {
|
| 387 |
+
const response = await fetch(`${API_URL}/auth/reset-password`, {
|
| 388 |
+
method: 'POST',
|
| 389 |
+
headers: { 'Content-Type': 'application/json' },
|
| 390 |
+
body: JSON.stringify({
|
| 391 |
+
email: email,
|
| 392 |
+
token: code,
|
| 393 |
+
new_password: newPassword
|
| 394 |
+
})
|
| 395 |
+
});
|
| 396 |
+
|
| 397 |
+
const data = await response.json();
|
| 398 |
+
|
| 399 |
+
if (response.ok) {
|
| 400 |
+
showAlert('✅ Password reset successfully! redirecting...', 'success');
|
| 401 |
+
setTimeout(() => {
|
| 402 |
+
window.location.replace('login.html');
|
| 403 |
+
}, 5000);
|
| 404 |
+
} else {
|
| 405 |
+
showAlert(data.detail || 'Invalid code or email.', 'error');
|
| 406 |
+
}
|
| 407 |
+
} catch (error) {
|
| 408 |
+
console.error('Reset error:', error);
|
| 409 |
+
showAlert('Connection error.', 'error');
|
| 410 |
+
} finally {
|
| 411 |
+
resetBtn.disabled = false;
|
| 412 |
+
loading.classList.remove('show');
|
| 413 |
+
}
|
| 414 |
+
}
|
| 415 |
+
</script>
|
| 416 |
+
</body>
|
| 417 |
+
</html>
|
search.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dotenv import load_dotenv
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
load_dotenv()
|
| 5 |
+
|
| 6 |
+
QDRANT_URL = os.getenv("QDRANT_URL")
|
| 7 |
+
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
|
| 8 |
+
from qdrant_client import QdrantClient
|
| 9 |
+
from sentence_transformers import SentenceTransformer
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
import os
|
| 12 |
+
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
QDRANT_URL = os.getenv("QDRANT_URL")
|
| 16 |
+
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
|
| 17 |
+
|
| 18 |
+
COLLECTION_NAME = "student_materials" # غيّري الاسم لو عندك اسم تاني
|
| 19 |
+
|
| 20 |
+
# Connect to Qdrant
|
| 21 |
+
client = QdrantClient(
|
| 22 |
+
url=QDRANT_URL,
|
| 23 |
+
api_key=QDRANT_API_KEY,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# Load embedding model
|
| 27 |
+
model = SentenceTransformer("intfloat/e5-large")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def search(query):
|
| 31 |
+
# 1) Embed query
|
| 32 |
+
query_vector = model.encode(query).tolist()
|
| 33 |
+
|
| 34 |
+
# 2) Search Qdrant
|
| 35 |
+
results = client.query_points(
|
| 36 |
+
collection_name=COLLECTION_NAME,
|
| 37 |
+
query=query_vector,
|
| 38 |
+
limit=5
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
return results.points # أهم سطر
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# ==========================
|
| 45 |
+
# Example test
|
| 46 |
+
# ==========================
|
| 47 |
+
if __name__ == "__main__":
|
| 48 |
+
res = search("What is machine learning?")
|
| 49 |
+
for p in res:
|
| 50 |
+
print("Payload:", p.payload)
|
| 51 |
+
print("Score:", p.score)
|
| 52 |
+
print("-" * 50)
|
start.sh
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# 1. Start the AI/RAG Service (scr/co.py) in the background
|
| 4 |
+
# It listens on port 8001 (internal communication only)
|
| 5 |
+
echo "Starting AI Service on port 8001..."
|
| 6 |
+
uvicorn co:app --host 127.0.0.1 --port 8001 --log-level debug &
|
| 7 |
+
|
| 8 |
+
# Wait a few seconds for the AI service to initialize
|
| 9 |
+
sleep 5
|
| 10 |
+
|
| 11 |
+
# 2. Start the Main Service (main.py) in the foreground
|
| 12 |
+
# It listens on port 7860 (Exposed to the world by Hugging Face)
|
| 13 |
+
echo "Starting Main Service on port 7860..."
|
| 14 |
+
uvicorn main:app --host 0.0.0.0 --port 7860 --log-level debug
|
university_chatbot.db
ADDED
|
Binary file (81.9 kB). View file
|
|
|
verify-email.html
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Verify Email - University AI</title>
|
| 7 |
+
<style>
|
| 8 |
+
/* Reusing styles from login.html for consistency */
|
| 9 |
+
body {
|
| 10 |
+
font-family: 'Segoe UI', sans-serif;
|
| 11 |
+
min-height: 100vh;
|
| 12 |
+
display: flex;
|
| 13 |
+
align-items: center;
|
| 14 |
+
justify-content: center;
|
| 15 |
+
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
| 16 |
+
padding: 20px;
|
| 17 |
+
}
|
| 18 |
+
.card {
|
| 19 |
+
background: white;
|
| 20 |
+
padding: 40px;
|
| 21 |
+
border-radius: 15px;
|
| 22 |
+
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
| 23 |
+
width: 100%;
|
| 24 |
+
max-width: 450px;
|
| 25 |
+
text-align: center;
|
| 26 |
+
}
|
| 27 |
+
h1 { color: #3A662A; margin-bottom: 10px; }
|
| 28 |
+
p { color: #666; margin-bottom: 20px; }
|
| 29 |
+
input {
|
| 30 |
+
width: 100%;
|
| 31 |
+
padding: 12px;
|
| 32 |
+
margin-bottom: 20px;
|
| 33 |
+
border: 2px solid #e0e0e0;
|
| 34 |
+
border-radius: 8px;
|
| 35 |
+
font-size: 24px;
|
| 36 |
+
text-align: center;
|
| 37 |
+
letter-spacing: 5px;
|
| 38 |
+
}
|
| 39 |
+
input:focus { outline: none; border-color: #3A662A; }
|
| 40 |
+
.btn {
|
| 41 |
+
width: 100%;
|
| 42 |
+
padding: 14px;
|
| 43 |
+
background: #3A662A;
|
| 44 |
+
color: white;
|
| 45 |
+
border: none;
|
| 46 |
+
border-radius: 8px;
|
| 47 |
+
font-size: 16px;
|
| 48 |
+
cursor: pointer;
|
| 49 |
+
font-weight: 600;
|
| 50 |
+
}
|
| 51 |
+
.btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(58, 102, 42, 0.4); }
|
| 52 |
+
.alert {
|
| 53 |
+
padding: 10px;
|
| 54 |
+
border-radius: 8px;
|
| 55 |
+
margin-bottom: 20px;
|
| 56 |
+
display: none;
|
| 57 |
+
}
|
| 58 |
+
.alert.error { background: #fee; color: #c33; }
|
| 59 |
+
.alert.success { background: #efe; color: #3c3; }
|
| 60 |
+
</style>
|
| 61 |
+
</head>
|
| 62 |
+
<body>
|
| 63 |
+
<div class="card">
|
| 64 |
+
<h1>Verify Account</h1>
|
| 65 |
+
<p>Enter the 6-digit code sent to <br><strong id="emailDisplay">your email</strong></p>
|
| 66 |
+
|
| 67 |
+
<div id="alert" class="alert"></div>
|
| 68 |
+
|
| 69 |
+
<div id="verifyForm">
|
| 70 |
+
<input type="text" id="otp" maxlength="6" placeholder="000000" required pattern="[0-9]{6}">
|
| 71 |
+
<button type="button" class="btn" onclick="handleVerify()">Verify Email</button>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
<p style="margin-top: 20px; font-size: 14px;">
|
| 75 |
+
Didn't receive code?
|
| 76 |
+
<a href="#" id="resendBtn" style="color: #3A662A;">Resend</a>
|
| 77 |
+
<span id="timerContainer" style="display: none; color: #666;">
|
| 78 |
+
(Wait <span id="timer">60</span>s)
|
| 79 |
+
</span>
|
| 80 |
+
</p>
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<script>
|
| 84 |
+
const API_URL = '';
|
| 85 |
+
|
| 86 |
+
// 1. Extract Email from URL
|
| 87 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 88 |
+
// URLSearchParams automatically decodes the parameter (e.g., %40 -> @)
|
| 89 |
+
const email = urlParams.get('email') ? urlParams.get('email').trim() : null;
|
| 90 |
+
|
| 91 |
+
if (email) {
|
| 92 |
+
document.getElementById('emailDisplay').textContent = email;
|
| 93 |
+
} else {
|
| 94 |
+
showAlert('Email not found. Please register again.', 'error');
|
| 95 |
+
document.getElementById('otp').disabled = true;
|
| 96 |
+
document.querySelector('.btn').disabled = true;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
function showAlert(msg, type) {
|
| 100 |
+
const el = document.getElementById('alert');
|
| 101 |
+
el.textContent = msg;
|
| 102 |
+
el.className = `alert ${type}`;
|
| 103 |
+
el.style.display = 'block';
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// 2. Handle Verification
|
| 107 |
+
async function handleVerify() {
|
| 108 |
+
const otpInput = document.getElementById('otp');
|
| 109 |
+
const otp = otpInput.value.trim();
|
| 110 |
+
|
| 111 |
+
if (!otp) {
|
| 112 |
+
showAlert('Please enter the verification code', 'error');
|
| 113 |
+
return;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// Validate exactly 6 digits
|
| 117 |
+
if (!/^\d{6}$/.test(otp)) {
|
| 118 |
+
showAlert('Code must be exactly 6 digits', 'error');
|
| 119 |
+
return;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
try {
|
| 123 |
+
const response = await fetch(`${API_URL}/auth/verify-email`, {
|
| 124 |
+
method: 'POST',
|
| 125 |
+
headers: { 'Content-Type': 'application/json' },
|
| 126 |
+
body: JSON.stringify({ email, otp })
|
| 127 |
+
});
|
| 128 |
+
|
| 129 |
+
const data = await response.json();
|
| 130 |
+
|
| 131 |
+
if (response.ok) {
|
| 132 |
+
document.getElementById('verifyForm').style.display = 'none';
|
| 133 |
+
// Hide resend link paragraph
|
| 134 |
+
document.getElementById('resendBtn').parentElement.style.display = 'none';
|
| 135 |
+
|
| 136 |
+
const card = document.querySelector('.card');
|
| 137 |
+
const successDiv = document.createElement('div');
|
| 138 |
+
successDiv.className = 'success-message';
|
| 139 |
+
successDiv.innerHTML = `
|
| 140 |
+
<div class="success-icon">🎉</div>
|
| 141 |
+
<h3 style="color: #3A662A; margin-bottom: 10px;">Email Verified!</h3>
|
| 142 |
+
<button class="btn" onclick="window.location.href='login.html'">Go to Login 🔐</button>
|
| 143 |
+
`;
|
| 144 |
+
card.appendChild(successDiv);
|
| 145 |
+
} else {
|
| 146 |
+
showAlert(data.detail || 'Verification failed', 'error');
|
| 147 |
+
}
|
| 148 |
+
} catch (err) {
|
| 149 |
+
showAlert('Connection error', 'error');
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
// Restrict input to numbers only
|
| 154 |
+
document.getElementById('otp').addEventListener('input', function(e) {
|
| 155 |
+
this.value = this.value.replace(/[^0-9]/g, '');
|
| 156 |
+
});
|
| 157 |
+
|
| 158 |
+
// Timer Logic
|
| 159 |
+
function startResendTimer(duration = 60) {
|
| 160 |
+
const btn = document.getElementById('resendBtn');
|
| 161 |
+
const container = document.getElementById('timerContainer');
|
| 162 |
+
const timerSpan = document.getElementById('timer');
|
| 163 |
+
|
| 164 |
+
let timeLeft = duration;
|
| 165 |
+
|
| 166 |
+
// Disable button
|
| 167 |
+
btn.style.pointerEvents = 'none';
|
| 168 |
+
btn.style.opacity = '0.5';
|
| 169 |
+
btn.style.textDecoration = 'none';
|
| 170 |
+
|
| 171 |
+
// Show timer
|
| 172 |
+
container.style.display = 'inline';
|
| 173 |
+
timerSpan.textContent = timeLeft;
|
| 174 |
+
|
| 175 |
+
const interval = setInterval(() => {
|
| 176 |
+
timeLeft--;
|
| 177 |
+
timerSpan.textContent = timeLeft;
|
| 178 |
+
|
| 179 |
+
if (timeLeft <= 0) {
|
| 180 |
+
clearInterval(interval);
|
| 181 |
+
btn.style.pointerEvents = 'auto';
|
| 182 |
+
btn.style.opacity = '1';
|
| 183 |
+
btn.style.textDecoration = 'underline';
|
| 184 |
+
container.style.display = 'none';
|
| 185 |
+
}
|
| 186 |
+
}, 1000);
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// Start timer on load
|
| 190 |
+
startResendTimer();
|
| 191 |
+
|
| 192 |
+
// Resend Logic
|
| 193 |
+
document.getElementById('resendBtn').addEventListener('click', async (e) => {
|
| 194 |
+
e.preventDefault();
|
| 195 |
+
if(!email) return;
|
| 196 |
+
try {
|
| 197 |
+
const response = await fetch(`${API_URL}/auth/resend-code`, {
|
| 198 |
+
method: 'POST',
|
| 199 |
+
headers: { 'Content-Type': 'application/json' },
|
| 200 |
+
body: JSON.stringify({ email })
|
| 201 |
+
});
|
| 202 |
+
|
| 203 |
+
if (response.ok) {
|
| 204 |
+
showAlert('New code sent!', 'success');
|
| 205 |
+
startResendTimer();
|
| 206 |
+
} else {
|
| 207 |
+
const data = await response.json();
|
| 208 |
+
showAlert(data.detail || 'Failed to resend', 'error');
|
| 209 |
+
}
|
| 210 |
+
} catch (err) {
|
| 211 |
+
showAlert('Connection error', 'error');
|
| 212 |
+
}
|
| 213 |
+
});
|
| 214 |
+
</script>
|
| 215 |
+
</body>
|
| 216 |
+
</html>
|