Alsmwal commited on
Commit
18ad9a9
·
verified ·
1 Parent(s): f3f1dd3

Upload 28 files

Browse files
Files changed (28) hide show
  1. .env +2 -0
  2. Admin-Dashboard.html +1547 -0
  3. Dockerfile +16 -0
  4. __init__.py +0 -0
  5. backend.code-workspace +10 -0
  6. ch.png +0 -0
  7. chat.html +1360 -0
  8. chatbot.py +30 -0
  9. chunk_text.py +75 -0
  10. clean_text.py +57 -0
  11. co.py +49 -0
  12. embedding.py +210 -0
  13. forgot-password.html +310 -0
  14. index.html +364 -0
  15. index_lectures.py +7 -0
  16. ingest.py +7 -0
  17. login.html +471 -0
  18. main.py +1395 -0
  19. process_pdf.py +255 -0
  20. rag.py +157 -0
  21. register.html +568 -0
  22. requierments.txt +28 -0
  23. requirements.txt +18 -0
  24. reset-password.html +417 -0
  25. search.py +52 -0
  26. start.sh +14 -0
  27. university_chatbot.db +0 -0
  28. 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>&copy; 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>