Ashok75 commited on
Commit
10a21dd
·
verified ·
1 Parent(s): 8833cd8

Upload 2 files

Browse files
Files changed (2) hide show
  1. auth.html +1510 -0
  2. file_pipeline.py +397 -0
auth.html ADDED
@@ -0,0 +1,1510 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-bs-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>GAKR AI - Login/Signup</title>
7
+ <link rel="stylesheet" href="https://cdn.replit.com/agent/bootstrap-agent-dark-theme.min.css">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
9
+ <style>
10
+ :root {
11
+ --gakr-blue: #4285F4;
12
+ --gakr-blue-dark: #1a73e8;
13
+ --gakr-blue-light: #e8f0fe;
14
+ --gakr-grey-text: #5f6368;
15
+ --gakr-grey-hover-bg: rgba(95, 99, 104, 0.1);
16
+ }
17
+
18
+ :root[data-bs-theme="dark"] {
19
+ --gakr-blue: #8ab4f8;
20
+ --gakr-blue-dark: #669df6;
21
+ --gakr-blue-light: rgba(138, 180, 248, 0.1);
22
+ --gakr-grey-text: #bdc1c6;
23
+ --gakr-grey-hover-bg: rgba(189, 193, 198, 0.1);
24
+ }
25
+
26
+ @import url('https://fonts.googleapis.com/css?family=Poppins:400,500,600,700&display=swap');
27
+
28
+ * {
29
+ margin: 0;
30
+ padding: 0;
31
+ box-sizing: border-box;
32
+ font-family: 'Poppins', sans-serif;
33
+ }
34
+
35
+ html, body {
36
+ display: flex;
37
+ flex-direction: column;
38
+ align-items: center;
39
+ height: 100%;
40
+ width: 100%;
41
+ background: -webkit-linear-gradient(left, #003366,#004080,#0059b3 , #0073e6);
42
+ color: var(--bs-body-color);
43
+ padding-top: 5vh;
44
+ padding-bottom: 5vh;
45
+ min-height: 100vh;
46
+ /* Allow space for modal overlay */
47
+ position: relative;
48
+ }
49
+
50
+ ::selection {
51
+ background: var(--gakr-blue);
52
+ color: #fff;
53
+ }
54
+
55
+ .wrapper {
56
+ overflow: hidden;
57
+ max-width: 440px;
58
+ width: 90%;
59
+ background: var(--bs-body-bg);
60
+ box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.5);
61
+ padding: 30px;
62
+ border-radius: 15px;
63
+ margin-top: 10px;
64
+ margin-bottom: 10px;
65
+ z-index: 10; /* Ensure wrapper is below modal but above background */
66
+ }
67
+
68
+ .wrapper .title-text {
69
+ display: flex;
70
+ width: 200%;
71
+ }
72
+
73
+ .wrapper .title {
74
+ width: 50%;
75
+ font-size: 35px;
76
+ font-weight: 600;
77
+ text-align: center;
78
+ transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
79
+ color: var(--bs-body-color);
80
+ }
81
+
82
+ .wrapper .slide-controls {
83
+ position: relative;
84
+ display: flex;
85
+ height: 50px;
86
+ width: 100%;
87
+ overflow: hidden;
88
+ margin: 30px 0 10px 0;
89
+ justify-content: space-between;
90
+ border: 1px solid var(--bs-border-color);
91
+ border-radius: 15px;
92
+ }
93
+
94
+ .slide-controls .slide {
95
+ height: 100%;
96
+ width: 100%;
97
+ color: var(--bs-body-color);
98
+ font-size: 18px;
99
+ font-weight: 500;
100
+ text-align: center;
101
+ line-height: 48px;
102
+ cursor: pointer;
103
+ z-index: 1;
104
+ transition: all 0.6s ease;
105
+ }
106
+
107
+ .slide-controls label.signup{
108
+ color: var(--bs-body-color);
109
+ }
110
+
111
+ .slide-controls .slider-tab {
112
+ position: absolute;
113
+ height: 100%;
114
+ width: 50%;
115
+ left: 0;
116
+ z-index: 0;
117
+ border-radius: 15px;
118
+ background: var(--gakr-blue);
119
+ transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
120
+ }
121
+
122
+ input[type="radio"]{
123
+ display: none;
124
+ }
125
+
126
+ #signup:checked ~ .slider-tab{
127
+ left: 50%;
128
+ }
129
+ #signup:checked ~ label.signup{
130
+ color: #fff;
131
+ cursor: default;
132
+ user-select: none;
133
+ }
134
+ #signup:checked ~ label.login{
135
+ color: var(--bs-body-color);
136
+ }
137
+ #login:checked ~ label.signup{
138
+ color: var(--bs-body-color);
139
+ }
140
+ #login:checked ~ label.login{
141
+ color: #fff;
142
+ cursor: default;
143
+ user-select: none;
144
+ }
145
+
146
+ .wrapper .form-container {
147
+ width: 100%;
148
+ overflow: hidden;
149
+ }
150
+
151
+ .form-container .form-inner {
152
+ display: flex;
153
+ width: 200%;
154
+ }
155
+
156
+ .form-container .form-inner form {
157
+ width: 50%;
158
+ transition: all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
159
+ }
160
+
161
+ .form-inner form .field {
162
+ height: 50px;
163
+ width: 100%;
164
+ margin-top: 20px;
165
+ position: relative;
166
+ }
167
+
168
+ .form-inner form .field:has(+ .field.btn) {
169
+ margin-bottom: 25px;
170
+ }
171
+
172
+ .form-inner form .field input{
173
+ height: 100%;
174
+ width: 100%;
175
+ outline: none;
176
+ padding-left: 15px;
177
+ border-radius: 15px;
178
+ border: 1px solid var(--bs-border-color);
179
+ border-bottom-width: 2px;
180
+ font-size: 17px;
181
+ transition: all 0.3s ease;
182
+ color: var(--bs-body-color);
183
+ background-color: var(--bs-body-bg);
184
+ }
185
+
186
+ .form-inner form .field input.error {
187
+ border-color: red;
188
+ }
189
+
190
+ .form-inner form .field input::placeholder{
191
+ color: var(--bs-secondary-color);
192
+ transition: all 0.3s ease;
193
+ }
194
+
195
+ form .field input:focus::placeholder{
196
+ color: var(--gakr-blue);
197
+ }
198
+
199
+ .form-inner form .pass-link{
200
+ margin-top: 5px;
201
+ font-size: 0.9rem;
202
+ text-align: right; /* Align to right */
203
+ }
204
+
205
+ .form-inner form .signup-link{
206
+ text-align: center;
207
+ margin-top: 30px;
208
+ font-size: 0.9rem;
209
+ }
210
+
211
+ .form-inner form .pass-link a,
212
+ .form-inner form .signup-link a{
213
+ color: var(--gakr-blue);
214
+ text-decoration: none;
215
+ transition: color 0.2s ease-in-out;
216
+ }
217
+
218
+ .form-inner form .pass-link a:hover,
219
+ form-inner form .signup-link a:hover{
220
+ text-decoration: underline;
221
+ color: var(--gakr-blue-dark);
222
+ }
223
+
224
+ form .btn {
225
+ height: 50px;
226
+ width: 100%;
227
+ margin-top: 20px;
228
+ border-radius: 15px;
229
+ position: relative;
230
+ overflow: hidden;
231
+ background: none;
232
+ transition: none;
233
+ }
234
+
235
+ form .btn input[type="submit"] {
236
+ height: 100%;
237
+ width: 100%;
238
+ z-index: 1;
239
+ position: relative;
240
+ background: var(--gakr-blue);
241
+ border: none;
242
+ color: #fff;
243
+ padding: 0;
244
+ border-radius: 15px;
245
+ font-size: 20px;
246
+ font-weight: 500;
247
+ cursor: pointer;
248
+ transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out;
249
+ display: flex;
250
+ align-items: center;
251
+ justify-content: center;
252
+ }
253
+
254
+ form .btn input[type="submit"]:hover {
255
+ background-color: var(--gakr-blue-dark);
256
+ }
257
+
258
+ form .btn input[type="submit"]:active {
259
+ transform: scale(0.98);
260
+ transition: background-color 0s, transform 0.1s;
261
+ }
262
+
263
+ .error-message {
264
+ color: red;
265
+ font-size: 0.8rem;
266
+ margin-top: 4px;
267
+ position: absolute;
268
+ bottom: -18px;
269
+ left: 15px;
270
+ white-space: nowrap;
271
+ }
272
+
273
+ .form-message {
274
+ text-align: center;
275
+ margin-top: 20px;
276
+ font-size: 0.9rem;
277
+ min-height: 1.2em;
278
+ }
279
+
280
+ .form-message.success {
281
+ color: green;
282
+ }
283
+
284
+ .form-message.error {
285
+ color: red;
286
+ }
287
+
288
+ .loading-spinner {
289
+ display: inline-block;
290
+ width: 18px;
291
+ height: 18px;
292
+ border: 3px solid rgba(255, 255, 255, .3);
293
+ border-radius: 50%;
294
+ border-top-color: #fff;
295
+ animation: spin 1s ease-in-out infinite;
296
+ margin-left: 10px;
297
+ vertical-align: middle;
298
+ }
299
+
300
+ @keyframes spin {
301
+ to { -webkit-transform: rotate(360deg); }
302
+ }
303
+
304
+ .form-inner form .btn input[type="submit"]:disabled {
305
+ opacity: 0.7;
306
+ cursor: not-allowed;
307
+ position: relative;
308
+ }
309
+
310
+ /* --- Modal Styles --- */
311
+ .modal-overlay {
312
+ position: fixed;
313
+ top: 0;
314
+ left: 0;
315
+ width: 100%;
316
+ height: 100%;
317
+ background-color: rgba(0, 0, 0, 0.6); /* Semi-transparent black overlay */
318
+ display: flex;
319
+ justify-content: center;
320
+ align-items: center;
321
+ z-index: 100; /* On top of everything */
322
+ visibility: hidden; /* Hidden by default */
323
+ opacity: 0;
324
+ transition: visibility 0s, opacity 0.3s ease-in-out;
325
+ }
326
+
327
+ .modal-overlay.show {
328
+ visibility: visible;
329
+ opacity: 1;
330
+ }
331
+
332
+ .modal-content {
333
+ background-color: var(--bs-body-bg);
334
+ padding: 30px;
335
+ border-radius: 15px;
336
+ box-shadow: 0px 5px 20px rgba(0, 0, 0, 0.7);
337
+ text-align: center;
338
+ max-width: 350px;
339
+ width: 80%;
340
+ transform: translateY(-20px); /* Slight animation on show */
341
+ transition: transform 0.3s ease-in-out;
342
+ }
343
+
344
+ .modal-overlay.show .modal-content {
345
+ transform: translateY(0);
346
+ }
347
+
348
+ .modal-message {
349
+ font-size: 1.1rem;
350
+ margin-bottom: 25px;
351
+ color: var(--bs-body-color);
352
+ }
353
+ .modal-message p {
354
+ margin-bottom: 10px; /* Spacing for paragraphs in modal message */
355
+ line-height: 1.4;
356
+ }
357
+ .modal-message strong {
358
+ font-size: 1.2rem; /* Heading for attempts message */
359
+ display: block;
360
+ margin-bottom: 15px;
361
+ }
362
+ .modal-message ul {
363
+ list-style: none;
364
+ padding: 0;
365
+ margin-bottom: 20px;
366
+ }
367
+ .modal-message ul li {
368
+ margin-bottom: 8px;
369
+ font-size: 1rem;
370
+ }
371
+
372
+
373
+ .modal-button {
374
+ background-color: var(--gakr-blue);
375
+ color: #fff;
376
+ border: none;
377
+ padding: 10px 25px;
378
+ border-radius: 10px;
379
+ font-size: 1rem;
380
+ cursor: pointer;
381
+ transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out;
382
+ }
383
+
384
+ .modal-button:hover {
385
+ background-color: var(--gakr-blue-dark);
386
+ }
387
+
388
+ .modal-button:active {
389
+ transform: scale(0.98);
390
+ }
391
+
392
+ .modal-buttons-stacked {
393
+ display: flex;
394
+ flex-direction: column; /* Stack buttons vertically */
395
+ gap: 10px;
396
+ margin-top: 20px;
397
+ }
398
+
399
+ .modal-buttons-stacked .modal-button {
400
+ width: 100%; /* Make buttons full width */
401
+ }
402
+
403
+ .modal-buttons-inline {
404
+ display: flex;
405
+ justify-content: space-between; /* Space them out */
406
+ gap: 15px;
407
+ margin-top: 20px;
408
+ }
409
+ .modal-buttons-inline .modal-button {
410
+ flex-grow: 1; /* Allow buttons to grow to fill space */
411
+ max-width: none; /* Override max-width from .modal-buttons */
412
+ }
413
+
414
+
415
+ /* Eye icon styles */
416
+ .field .toggle-password {
417
+ position: absolute;
418
+ right: 15px;
419
+ top: 50%;
420
+ transform: translateY(-50%);
421
+ cursor: pointer;
422
+ color: var(--bs-secondary-color);
423
+ font-size: 0.9rem;
424
+ }
425
+
426
+ .field .toggle-password:hover {
427
+ color: var(--gakr-blue);
428
+ }
429
+
430
+ /* Added styles for Forgot/Reset Modals */
431
+ .modal-form {
432
+ text-align: left;
433
+ margin-top: 20px;
434
+ }
435
+ .modal-form .field {
436
+ margin-bottom: 15px;
437
+ height: auto; /* Override default field height */
438
+ }
439
+ .modal-form label {
440
+ display: block;
441
+ margin-bottom: 5px;
442
+ font-size: 0.9rem;
443
+ color: var(--bs-body-color);
444
+ }
445
+ .modal-form input[type="text"],
446
+ .modal-form input[type="email"],
447
+ .modal-form input[type="password"] {
448
+ width: 100%;
449
+ padding: 10px 15px;
450
+ border: 1px solid var(--bs-border-color);
451
+ border-radius: 10px;
452
+ background-color: var(--bs-body-bg);
453
+ color: var(--bs-body-color);
454
+ box-sizing: border-box; /* Crucial for consistent width */
455
+ }
456
+ .modal-form .error-message {
457
+ position: static; /* Remove absolute positioning */
458
+ margin-top: 5px;
459
+ margin-left: 0;
460
+ white-space: normal; /* Allow message to wrap */
461
+ }
462
+ .modal-buttons {
463
+ display: flex;
464
+ justify-content: center;
465
+ gap: 15px;
466
+ margin-top: 20px;
467
+ }
468
+ .modal-buttons button {
469
+ width: 100%;
470
+ max-width: 150px;
471
+ }
472
+ </style>
473
+ </head>
474
+ <body>
475
+
476
+ <div class="wrapper">
477
+ <div class="title-text">
478
+ <div class="title login">Login Form</div>
479
+ <div class="title signup">Signup Form</div>
480
+ </div>
481
+ <div class="form-container">
482
+ <div class="slide-controls">
483
+ <input type="radio" name="slide" id="login" checked>
484
+ <input type="radio" name="slide" id="signup">
485
+ <label for="login" class="slide login">Login</label>
486
+ <label for="signup" class="slide signup">Signup</label>
487
+ <div class="slider-tab"></div>
488
+ </div>
489
+ <div class="form-inner">
490
+ <form action="#" class="login" id="loginForm">
491
+ <div class="field">
492
+ <input type="text" placeholder="Username or Email" required id="loginUsernameOrEmail" name="username_or_email">
493
+ <div class="error-message" id="loginUsernameOrEmailError"></div>
494
+ </div>
495
+ <div class="field">
496
+ <input type="password" placeholder="Password" required id="loginPassword" name="password">
497
+ <span class="toggle-password fa-solid fa-eye-slash"></span>
498
+ <div class="error-message" id="loginPasswordError"></div>
499
+ </div>
500
+ <div class="pass-link"><a href="#" id="forgotPasswordLink">Forgot password?</a></div>
501
+ <div class="field btn">
502
+ <input type="submit" value="Login">
503
+ </div>
504
+ <div class="signup-link">Not a member? <a href="">Signup now</a></div>
505
+ </form>
506
+ <form action="#" class="signup" id="signupForm">
507
+ <div class="field">
508
+ <input type="text" placeholder="Name" required id="signupName" name="name">
509
+ <div class="error-message" id="signupNameError"></div>
510
+ </div>
511
+ <div class="field">
512
+ <input type="text" placeholder="Email Address" required id="signupEmail" name="email">
513
+ <div class="error-message" id="signupEmailError"></div>
514
+ </div>
515
+ <div class="field">
516
+ <input type="password" placeholder="Password" required id="signupPassword" name="password">
517
+ <span class="toggle-password fa-solid fa-eye-slash"></span>
518
+ <div class="error-message" id="signupPasswordError"></div>
519
+ </div>
520
+ <div class="field">
521
+ <input type="password" placeholder="Confirm password" required id="signupConfirmPassword" name="confirm_password">
522
+ <span class="toggle-password fa-solid fa-eye-slash"></span>
523
+ <div class="error-message" id="signupConfirmPasswordError"></div>
524
+ </div>
525
+ <div class="field btn">
526
+ <input type="submit" value="Signup">
527
+ </div>
528
+ </form>
529
+ </div>
530
+ </div>
531
+ <div class="form-message" id="generalFormMessage"></div>
532
+ </div>
533
+
534
+ <div class="modal-overlay" id="customModal">
535
+ <div class="modal-content">
536
+ <div class="modal-message" id="modalMessage"></div>
537
+ <div id="modalButtonsContainer" class="modal-buttons">
538
+ </div>
539
+ </div>
540
+ </div>
541
+
542
+ <div class="modal-overlay" id="forgotPasswordRequestModal">
543
+ <div class="modal-content">
544
+ <p class="modal-message" id="forgotModalMessage">Enter your registered email to request a password reset.</p>
545
+
546
+ <form id="forgotPasswordRequestForm" class="modal-form">
547
+ <div class="field">
548
+ <label for="forgotEmail">Email:</label>
549
+ <input type="email" id="forgotEmail" name="email" required>
550
+ <div class="error-message" id="forgotEmailError"></div>
551
+ </div>
552
+ <div class="modal-buttons-stacked">
553
+ <button type="submit" class="modal-button" id="forgotSubmitButton">Request OTP</button>
554
+ <button type="button" class="modal-button" id="forgotCancelButton" style="background-color: var(--bs-secondary-color);">Cancel</button>
555
+ </div>
556
+ </form>
557
+
558
+ <div id="otpSection" style="display: none;">
559
+ <form id="verifyOtpForm" class="modal-form">
560
+ <div class="field">
561
+ <label for="otpInput">Enter OTP:</label>
562
+ <input type="text" id="otpInput" name="otp" required maxlength="6" pattern="\d{6}" title="Please enter a 6-digit OTP">
563
+ <div class="error-message" id="otpError"></div>
564
+ </div>
565
+ <div class="modal-buttons-stacked">
566
+ <button type="submit" class="modal-button" id="verifyOtpButton">Verify OTP</button>
567
+ <button type="button" class="modal-button" id="resendOtpButton" style="background-color: var(--bs-secondary-color);">Send Again</button>
568
+ <button type="button" class="modal-button" id="otpCancelButton" style="background-color: var(--bs-secondary-color);">Cancel</button>
569
+ </div>
570
+ </form>
571
+ </div>
572
+ </div>
573
+ </div>
574
+
575
+ <div class="modal-overlay" id="resetPasswordModal">
576
+ <div class="modal-content">
577
+ <p class="modal-message">Enter your new password.</p>
578
+ <form id="resetPasswordForm" class="modal-form">
579
+ <div class="field">
580
+ <label for="resetEmail">Email:</label>
581
+ <input type="email" id="resetEmail" name="email" required readonly style="background-color: var(--bs-secondary-bg); cursor: not-allowed;">
582
+ <div class="error-message" id="resetEmailError"></div>
583
+ </div>
584
+ <div class="field">
585
+ <label for="newPassword">New Password:</label>
586
+ <input type="password" id="newPassword" name="new_password" required>
587
+ <span class="toggle-password fa-solid fa-eye-slash"></span>
588
+ <div class="error-message" id="newPasswordError"></div>
589
+ </div>
590
+ <div class="field">
591
+ <label for="confirmNewPassword">Confirm New Password:</label>
592
+ <input type="password" id="confirmNewPassword" name="confirm_new_password" required>
593
+ <span class="toggle-password fa-solid fa-eye-slash"></span>
594
+ <div class="error-message" id="confirmNewPasswordError"></div>
595
+ </div>
596
+ <div class="modal-buttons-stacked">
597
+ <button type="submit" class="modal-button" id="resetSubmitButton">Reset Password</button>
598
+ <button type="button" class="modal-button" id="resetCancelButton" style="background-color: var(--bs-secondary-color);">Cancel</button>
599
+ </div>
600
+ </form>
601
+ </div>
602
+ </div>
603
+
604
+ <script>
605
+ document.addEventListener('DOMContentLoaded', function() {
606
+ const loginText = document.querySelector(".title-text .login");
607
+ const loginFormDiv = document.querySelector("form.login");
608
+ const signupLink = document.querySelector("form .signup-link a");
609
+ const loginRadio = document.getElementById('login');
610
+ const signupRadio = document.getElementById('signup');
611
+
612
+ const loginForm = document.getElementById('loginForm');
613
+ const signupForm = document.getElementById('signupForm');
614
+
615
+ // Existing form inputs and error displays
616
+ const loginUsernameOrEmailInput = document.getElementById('loginUsernameOrEmail');
617
+ const loginPasswordInput = document.getElementById('loginPassword');
618
+ const loginSubmitButton = loginForm ? loginForm.querySelector('input[type="submit"]') : null;
619
+
620
+ const signupNameInput = document.getElementById('signupName');
621
+ const signupEmailInput = document.getElementById('signupEmail');
622
+ const signupPasswordInput = document.getElementById('signupPassword');
623
+ const signupConfirmPasswordInput = document.getElementById('signupConfirmPassword');
624
+ const signupSubmitButton = signupForm ? signupForm.querySelector('input[type="submit"]') : null;
625
+
626
+ const loginUsernameOrEmailError = document.getElementById('loginUsernameOrEmailError');
627
+ const loginPasswordError = document.getElementById('loginPasswordError');
628
+
629
+ const signupNameError = document.getElementById('signupNameError');
630
+ const signupEmailError = document.getElementById('signupEmailError');
631
+ const signupPasswordError = document.getElementById('signupPasswordError');
632
+ const signupConfirmPasswordError = document.getElementById('signupConfirmPasswordError');
633
+
634
+ const generalFormMessage = document.getElementById('generalFormMessage');
635
+
636
+ // Modal elements (main alert modal)
637
+ const customModal = document.getElementById('customModal');
638
+ const modalMessageDiv = document.getElementById('modalMessage');
639
+ const modalButtonsContainer = document.getElementById('modalButtonsContainer');
640
+
641
+ // Forgot Password Request & OTP Modals elements
642
+ const forgotPasswordLink = document.getElementById('forgotPasswordLink');
643
+ const forgotPasswordRequestModal = document.getElementById('forgotPasswordRequestModal'); // The main overlay for forgot/otp
644
+ const forgotModalMessage = document.getElementById('forgotModalMessage'); // Message within the forgot/otp modal
645
+
646
+ // Elements for Step 1: Request Email
647
+ const forgotPasswordRequestForm = document.getElementById('forgotPasswordRequestForm');
648
+ const forgotEmailInput = document.getElementById('forgotEmail');
649
+ const forgotEmailError = document.getElementById('forgotEmailError');
650
+ const forgotSubmitButton = document.getElementById('forgotSubmitButton');
651
+ const forgotCancelButton = document.getElementById('forgotCancelButton');
652
+
653
+ // Elements for Step 2: Enter OTP
654
+ const otpSection = document.getElementById('otpSection'); // The container for OTP input and buttons
655
+ const verifyOtpForm = document.getElementById('verifyOtpForm');
656
+ const otpInput = document.getElementById('otpInput');
657
+ const otpError = document.getElementById('otpError');
658
+ const verifyOtpButton = document.getElementById('verifyOtpButton');
659
+ const resendOtpButton = document.getElementById('resendOtpButton');
660
+ const otpCancelButton = document.getElementById('otpCancelButton');
661
+
662
+ // Reset Password Modal elements (Step 3: New Password)
663
+ const resetPasswordModal = document.getElementById('resetPasswordModal');
664
+ const resetPasswordForm = document.getElementById('resetPasswordForm');
665
+ const resetEmailInput = document.getElementById('resetEmail'); // Hidden but read-only email field
666
+ const newPasswordInput = document.getElementById('newPassword');
667
+ const confirmNewPasswordInput = document.getElementById('confirmNewPassword');
668
+ const resetEmailError = document.getElementById('resetEmailError'); // Error for reset email (should be rare)
669
+ const newPasswordError = document.getElementById('newPasswordError');
670
+ const confirmNewPasswordError = document.getElementById('confirmNewPasswordError');
671
+ const resetSubmitButton = document.getElementById('resetSubmitButton');
672
+ const resetCancelButton = document.getElementById('resetCancelButton');
673
+
674
+ // New: Login Attempt Counter
675
+ let loginAttempts = 0;
676
+ const MAX_LOGIN_ATTEMPTS = 3;
677
+
678
+ // Stores the email across the forgot password/OTP/reset flow
679
+ let currentForgotEmail = '';
680
+
681
+ // Function to clear all error messages and input error classes
682
+ function clearErrors(formType = 'all') {
683
+ if (formType === 'all' || formType === 'main') {
684
+ document.querySelectorAll('.error-message').forEach(el => el.textContent = '');
685
+ document.querySelectorAll('.form-inner form .field input').forEach(el => el.classList.remove('error'));
686
+ if (generalFormMessage) {
687
+ generalFormMessage.textContent = '';
688
+ generalFormMessage.className = 'form-message';
689
+ }
690
+ }
691
+ if (formType === 'all' || formType === 'forgot') {
692
+ if (forgotEmailError) forgotEmailError.textContent = '';
693
+ if (forgotEmailInput) forgotEmailInput.classList.remove('error');
694
+ // Also clear the general forgotModalMessage when resetting for new flow
695
+ if (forgotModalMessage) forgotModalMessage.innerHTML = 'Enter your registered email to request a password reset.';
696
+ }
697
+ if (formType === 'all' || formType === 'otp') {
698
+ if (otpError) otpError.textContent = '';
699
+ if (otpInput) otpInput.classList.remove('error');
700
+ }
701
+ if (formType === 'all' || formType === 'reset') {
702
+ if (resetEmailError) resetEmailError.textContent = '';
703
+ if (newPasswordError) newPasswordError.textContent = '';
704
+ if (confirmNewPasswordError) confirmNewPasswordError.textContent = '';
705
+ if (resetEmailInput) resetEmailInput.classList.remove('error');
706
+ if (newPasswordInput) newPasswordInput.classList.remove('error');
707
+ if (confirmNewPasswordInput) confirmNewPasswordInput.classList.remove('error');
708
+ }
709
+ }
710
+
711
+ // Function to display general form-level message (below the forms)
712
+ function displayFormMessage(message, type = 'info') {
713
+ if (generalFormMessage) {
714
+ generalFormMessage.textContent = message;
715
+ generalFormMessage.className = 'form-message ' + type;
716
+ }
717
+ }
718
+
719
+ // Universal modal display function
720
+ function showCustomModal(modalElement) {
721
+ modalElement.classList.add('show');
722
+ }
723
+
724
+ // Universal modal hide function
725
+ function hideCustomModal(modalElement) {
726
+ modalElement.classList.remove('show');
727
+ clearErrors(); // Clear all errors when any modal is hidden
728
+ }
729
+
730
+ // Function to show a flexible custom modal (for general alerts/info)
731
+ function showFlexibleModal(messageHTML, buttonsConfig, callback = null) {
732
+ modalMessageDiv.innerHTML = messageHTML;
733
+ modalButtonsContainer.innerHTML = ''; // Clear previous buttons
734
+
735
+ if (buttonsConfig && buttonsConfig.length > 0) {
736
+ const isInline = buttonsConfig.every(btn => !btn.newline);
737
+ modalButtonsContainer.className = isInline ? 'modal-buttons-inline' : 'modal-buttons-stacked';
738
+
739
+ buttonsConfig.forEach(btnConf => {
740
+ const button = document.createElement('button');
741
+ button.className = 'modal-button';
742
+ button.textContent = btnConf.text;
743
+ if (btnConf.style) {
744
+ button.style = btnConf.style;
745
+ }
746
+ button.onclick = function() {
747
+ hideCustomModal(customModal);
748
+ if (btnConf.action && typeof btnConf.action === 'function') {
749
+ btnConf.action();
750
+ }
751
+ if (callback && typeof callback === 'function') {
752
+ callback();
753
+ }
754
+ };
755
+ modalButtonsContainer.appendChild(button);
756
+ });
757
+ } else {
758
+ const okButton = document.createElement('button');
759
+ okButton.className = 'modal-button';
760
+ okButton.textContent = 'OK';
761
+ okButton.onclick = function() {
762
+ hideCustomModal(customModal);
763
+ if (callback && typeof callback === 'function') {
764
+ callback();
765
+ }
766
+ };
767
+ modalButtonsContainer.classList.remove('modal-buttons-inline', 'modal-buttons-stacked');
768
+ modalButtonsContainer.classList.add('modal-buttons');
769
+ modalButtonsContainer.appendChild(okButton);
770
+ }
771
+ showCustomModal(customModal);
772
+ }
773
+
774
+ // Slide logic
775
+ function slideToSignup() {
776
+ if (loginFormDiv && loginText) {
777
+ loginFormDiv.style.marginLeft = "-50%";
778
+ loginText.style.marginLeft = "-50%";
779
+ clearErrors('main');
780
+ }
781
+ }
782
+
783
+ function slideToLogin() {
784
+ if (loginFormDiv && loginText) {
785
+ loginFormDiv.style.marginLeft = "0%";
786
+ loginText.style.marginLeft = "0%";
787
+ clearErrors('main');
788
+ }
789
+ }
790
+
791
+ // Event listeners for radio buttons and signup link
792
+ if (signupRadio) {
793
+ signupRadio.addEventListener('change', function() {
794
+ if (this.checked) {
795
+ slideToSignup();
796
+ }
797
+ });
798
+ }
799
+
800
+ if (loginRadio) {
801
+ loginRadio.addEventListener('change', function() {
802
+ if (this.checked) {
803
+ slideToLogin();
804
+ }
805
+ });
806
+ }
807
+
808
+ if (signupLink && signupRadio) {
809
+ signupLink.onclick = ((e) => {
810
+ e.preventDefault();
811
+ signupRadio.checked = true;
812
+ slideToSignup();
813
+ });
814
+ }
815
+
816
+ // Handle URL parameter for initial tab
817
+ const urlParams = new URLSearchParams(window.location.search);
818
+ const showSignup = urlParams.get('signup');
819
+
820
+ if (showSignup === 'true') {
821
+ if (signupRadio) {
822
+ signupRadio.checked = true;
823
+ slideToSignup();
824
+ }
825
+ } else {
826
+ if (loginRadio) {
827
+ loginRadio.checked = true;
828
+ slideToLogin();
829
+ }
830
+ }
831
+
832
+ // Function to toggle password visibility
833
+ function setupPasswordToggle(passwordInput, toggleIcon) {
834
+ if (passwordInput && toggleIcon) {
835
+ toggleIcon.addEventListener('click', function() {
836
+ const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
837
+ passwordInput.setAttribute('type', type);
838
+ this.classList.toggle('fa-eye');
839
+ this.classList.toggle('fa-eye-slash');
840
+ });
841
+ }
842
+ }
843
+
844
+ // Setup password toggles for all password fields
845
+ setupPasswordToggle(loginPasswordInput, document.querySelector('#loginForm .toggle-password'));
846
+ setupPasswordToggle(signupPasswordInput, document.querySelector('#signupForm #signupPassword + .toggle-password'));
847
+ setupPasswordToggle(signupConfirmPasswordInput, document.querySelector('#signupForm #signupConfirmPassword + .toggle-password'));
848
+ setupPasswordToggle(newPasswordInput, document.querySelector('#resetPasswordForm #newPassword + .toggle-password'));
849
+ setupPasswordToggle(confirmNewPasswordInput, document.querySelector('#resetPasswordForm #confirmNewPassword + .toggle-password'));
850
+
851
+
852
+ // Login Form Submission Handler
853
+ if (loginForm) {
854
+ loginForm.addEventListener('submit', function(event) {
855
+ event.preventDefault();
856
+ clearErrors('main'); // Clear main form errors
857
+
858
+ // Only proceed if login attempts are below limit
859
+ if (loginAttempts >= MAX_LOGIN_ATTEMPTS) {
860
+ showLoginAttemptsLimitModal();
861
+ return; // Stop submission
862
+ }
863
+
864
+ let isValid = true;
865
+
866
+ const usernameOrEmail = loginUsernameOrEmailInput.value.trim();
867
+ const password = loginPasswordInput.value.trim();
868
+
869
+ if (usernameOrEmail === '') {
870
+ isValid = false;
871
+ loginUsernameOrEmailError.textContent = 'Username or Email is required';
872
+ loginUsernameOrEmailInput.classList.add('error');
873
+ }
874
+ if (password === '') {
875
+ isValid = false;
876
+ loginPasswordError.textContent = 'Password is required';
877
+ loginPasswordInput.classList.add('error');
878
+ }
879
+
880
+ if (isValid) {
881
+ submitFormData(event.target, loginSubmitButton, '/api/login', 'main');
882
+ }
883
+ });
884
+ }
885
+
886
+ // Signup Form Submission Handler
887
+ if (signupForm) {
888
+ signupForm.addEventListener('submit', function(event) {
889
+ event.preventDefault();
890
+ clearErrors('main');
891
+
892
+ let isValid = true;
893
+
894
+ const name = signupNameInput.value.trim();
895
+ const email = signupEmailInput.value.trim();
896
+ const password = signupPasswordInput.value.trim();
897
+ const confirmPassword = signupConfirmPasswordInput.value.trim();
898
+
899
+ if (name === '') {
900
+ isValid = false;
901
+ signupNameError.textContent = 'Name is required';
902
+ signupNameInput.classList.add('error');
903
+ }
904
+
905
+ if (email === '') {
906
+ isValid = false;
907
+ signupEmailError.textContent = 'Email is required';
908
+ signupEmailInput.classList.add('error');
909
+ } else if (!isValidEmail(email)) {
910
+ isValid = false;
911
+ signupEmailError.textContent = 'Enter a valid email address';
912
+ signupEmailInput.classList.add('error');
913
+ }
914
+
915
+ if (password === '') {
916
+ isValid = false;
917
+ signupPasswordError.textContent = 'Password is required';
918
+ signupPasswordInput.classList.add('error');
919
+ } else if (password.length < 6) {
920
+ isValid = false;
921
+ signupPasswordError.textContent = 'Password must be at least 6 characters';
922
+ signupPasswordInput.classList.add('error');
923
+ }
924
+
925
+ if (confirmPassword === '') {
926
+ isValid = false;
927
+ signupConfirmPasswordError.textContent = 'Confirm password is required';
928
+ signupConfirmPasswordInput.classList.add('error');
929
+ } else if (password !== confirmPassword) {
930
+ isValid = false;
931
+ signupConfirmPasswordError.textContent = 'Passwords do not match';
932
+ signupConfirmPasswordInput.classList.add('error');
933
+ signupPasswordInput.classList.add('error');
934
+ }
935
+
936
+ if (isValid) {
937
+ submitFormData(event.target, signupSubmitButton, '/api/signup', 'main');
938
+ }
939
+ });
940
+ }
941
+
942
+ // Forgot Password Link Handler (Initial Click)
943
+ if (forgotPasswordLink) {
944
+ forgotPasswordLink.addEventListener('click', function(event) {
945
+ event.preventDefault();
946
+ clearErrors('forgot'); // Clear errors for email request
947
+ clearErrors('otp'); // Clear errors for OTP section
948
+
949
+ forgotPasswordRequestForm.reset(); // Clear email input
950
+ otpInput.value = ''; // Clear OTP input
951
+ // Reset the general message for the forgot password modal
952
+ forgotModalMessage.innerHTML = 'Enter your registered email to request a password reset.';
953
+
954
+ // Show the email request form and hide the OTP section
955
+ forgotPasswordRequestForm.style.display = 'block';
956
+ if (otpSection) otpSection.style.display = 'none'; // Ensure OTP section is hidden initially
957
+
958
+ showCustomModal(forgotPasswordRequestModal);
959
+ });
960
+ }
961
+
962
+ // Step 1: Forgot Password Request Form Submission Handler (Requests OTP)
963
+ if (forgotPasswordRequestForm) {
964
+ forgotPasswordRequestForm.addEventListener('submit', async function(event) {
965
+ event.preventDefault();
966
+ clearErrors('forgot'); // Clear errors specific to the email request form
967
+
968
+ let isValid = true;
969
+ const email = forgotEmailInput.value.trim();
970
+
971
+ if (email === '') {
972
+ isValid = false;
973
+ forgotEmailError.textContent = 'Email is required.';
974
+ forgotEmailInput.classList.add('error');
975
+ } else if (!isValidEmail(email)) {
976
+ isValid = false;
977
+ forgotEmailError.textContent = 'Enter a valid email address.';
978
+ forgotEmailInput.classList.add('error');
979
+ }
980
+
981
+ if (isValid) {
982
+ forgotSubmitButton.disabled = true;
983
+ forgotSubmitButton.innerHTML = '<span class="loading-spinner"></span> Sending OTP...';
984
+
985
+ try {
986
+ const response = await fetch('/api/forgot_password_request', {
987
+ method: 'POST',
988
+ headers: { 'Content-Type': 'application/json' },
989
+ body: JSON.stringify({ email: email })
990
+ });
991
+
992
+ const result = await response.json();
993
+
994
+ if (response.ok) {
995
+ currentForgotEmail = email; // Store email for subsequent steps
996
+
997
+ // Transition to OTP input section
998
+ forgotPasswordRequestForm.style.display = 'none';
999
+ if (otpSection) otpSection.style.display = 'block';
1000
+ otpInput.value = ''; // Clear previous OTP in case of resend
1001
+
1002
+ // Update message for OTP section
1003
+ let messageText = `<p>An OTP has been sent to <strong>${email}</strong>. Please enter it below.</p>`;
1004
+ // Note: Do not display simulated OTP in production. This is for testing convenience.
1005
+ // if (result.simulated_otp) {
1006
+ // messageText += `<p><strong>SIMULATED OTP (FOR TESTING):</strong> ${result.simulated_otp}</p>`;
1007
+ // }
1008
+ forgotModalMessage.innerHTML = messageText;
1009
+
1010
+ } else {
1011
+ displayModalFormErrors(result, forgotPasswordRequestForm, 'forgot');
1012
+ }
1013
+ } catch (error) {
1014
+ console.error('Fetch error:', error);
1015
+ showFlexibleModal('<p>Network error. Could not request OTP. Please try again.</p>', [{ text: 'OK' }]);
1016
+ } finally {
1017
+ forgotSubmitButton.disabled = false;
1018
+ forgotSubmitButton.innerHTML = 'Request OTP';
1019
+ }
1020
+ }
1021
+ });
1022
+ }
1023
+
1024
+ // Step 2: OTP Verification Form Submission Handler
1025
+ if (verifyOtpForm) {
1026
+ verifyOtpForm.addEventListener('submit', async function(event) {
1027
+ event.preventDefault();
1028
+ clearErrors('otp'); // Clear errors specific to the OTP verification form
1029
+
1030
+ let isValid = true;
1031
+ const otp = otpInput.value.trim();
1032
+
1033
+ if (otp === '') {
1034
+ isValid = false;
1035
+ otpError.textContent = 'OTP is required.';
1036
+ otpInput.classList.add('error');
1037
+ } else if (!/^\d{6}$/.test(otp)) { // Ensure it's 6 digits
1038
+ isValid = false;
1039
+ otpError.textContent = 'OTP must be a 6-digit number.';
1040
+ otpInput.classList.add('error');
1041
+ }
1042
+
1043
+ if (isValid) {
1044
+ verifyOtpButton.disabled = true;
1045
+ verifyOtpButton.innerHTML = '<span class="loading-spinner"></span> Verifying...';
1046
+
1047
+ try {
1048
+ const response = await fetch('/api/verify_otp', {
1049
+ method: 'POST',
1050
+ headers: { 'Content-Type': 'application/json' },
1051
+ body: JSON.stringify({ email: currentForgotEmail, otp: otp })
1052
+ });
1053
+
1054
+ const result = await response.json();
1055
+
1056
+ if (response.ok) {
1057
+ hideCustomModal(forgotPasswordRequestModal); // Hide the OTP modal
1058
+ resetEmailInput.value = currentForgotEmail; // Pre-fill email in new password form
1059
+ newPasswordInput.value = ''; // Clear new password field
1060
+ confirmNewPasswordInput.value = ''; // Clear confirm new password field
1061
+ showCustomModal(resetPasswordModal); // Show the new password modal
1062
+
1063
+ } else {
1064
+ otpError.textContent = result.message || 'OTP verification failed.';
1065
+ otpInput.classList.add('error');
1066
+ otpInput.classList.add('error'); // Ensure input field is marked
1067
+ // Update the main message area of the OTP modal if a general message is returned
1068
+ if (result.message) {
1069
+ forgotModalMessage.innerHTML = `<p>${result.message}</p>`;
1070
+ }
1071
+ }
1072
+ } catch (error) {
1073
+ console.error('Fetch error:', error);
1074
+ showFlexibleModal('<p>Network error. Could not verify OTP. Please try again.</p>', [{ text: 'OK' }]);
1075
+ } finally {
1076
+ verifyOtpButton.disabled = false;
1077
+ verifyOtpButton.innerHTML = 'Verify OTP';
1078
+ }
1079
+ }
1080
+ });
1081
+ }
1082
+
1083
+ // Step 2: Resend OTP Button Handler
1084
+ if (resendOtpButton) {
1085
+ resendOtpButton.addEventListener('click', async function(event) {
1086
+ event.preventDefault();
1087
+ clearErrors('otp'); // Clear any previous OTP errors
1088
+ resendOtpButton.disabled = true;
1089
+ resendOtpButton.innerHTML = '<span class="loading-spinner"></span> Resending...';
1090
+
1091
+ try {
1092
+ const response = await fetch('/api/forgot_password_request', {
1093
+ method: 'POST',
1094
+ headers: { 'Content-Type': 'application/json' },
1095
+ body: JSON.stringify({ email: currentForgotEmail })
1096
+ });
1097
+
1098
+ const result = await response.json();
1099
+
1100
+ if (response.ok) {
1101
+ let messageText = `<p>New OTP sent to <strong>${currentForgotEmail}</strong>. Please enter it below.</p>`;
1102
+ // if (result.simulated_otp) { // Again, remove in production
1103
+ // messageText += `<p><strong>SIMULATED OTP (FOR TESTING):</strong> ${result.simulated_otp}</p>`;
1104
+ // }
1105
+ forgotModalMessage.innerHTML = messageText;
1106
+ otpInput.value = ''; // Clear previous OTP
1107
+ otpInput.classList.remove('error'); // Clear error state
1108
+ otpError.textContent = ''; // Clear error message
1109
+ } else {
1110
+ // If there's an error on resend, display it in the OTP section's error message
1111
+ otpError.textContent = result.message || 'Failed to resend OTP.';
1112
+ otpInput.classList.add('error'); // Potentially highlight OTP input again
1113
+ if (result.message) {
1114
+ forgotModalMessage.innerHTML = `<p>${result.message}</p>`;
1115
+ }
1116
+ }
1117
+ } catch (error) {
1118
+ console.error('Fetch error:', error);
1119
+ showFlexibleModal('<p>Network error. Could not resend OTP. Please try again.</p>', [{ text: 'OK' }]);
1120
+ } finally {
1121
+ resendOtpButton.disabled = false;
1122
+ resendOtpButton.innerHTML = 'Send Again';
1123
+ }
1124
+ });
1125
+ }
1126
+
1127
+ // Handlers for Cancel buttons in the forgot/reset modals
1128
+ if (forgotCancelButton) {
1129
+ forgotCancelButton.addEventListener('click', function() {
1130
+ hideCustomModal(forgotPasswordRequestModal);
1131
+ forgotPasswordRequestForm.reset();
1132
+ currentForgotEmail = ''; // Clear stored email
1133
+ clearErrors('forgot'); // Ensure all forgot-related errors are cleared
1134
+ });
1135
+ }
1136
+
1137
+ if (otpCancelButton) {
1138
+ otpCancelButton.addEventListener('click', function() {
1139
+ hideCustomModal(forgotPasswordRequestModal);
1140
+ verifyOtpForm.reset();
1141
+ currentForgotEmail = ''; // Clear stored email
1142
+ clearErrors('otp'); // Ensure all OTP-related errors are cleared
1143
+ });
1144
+ }
1145
+
1146
+ if (resetCancelButton) {
1147
+ resetCancelButton.addEventListener('click', function() {
1148
+ hideCustomModal(resetPasswordModal);
1149
+ resetPasswordForm.reset();
1150
+ currentForgotEmail = ''; // Clear stored email
1151
+ clearErrors('reset'); // Ensure all reset-related errors are cleared
1152
+ });
1153
+ }
1154
+
1155
+ // Step 3: Reset Password Form Submission Handler
1156
+ if (resetPasswordForm) {
1157
+ resetPasswordForm.addEventListener('submit', async function(event) {
1158
+ event.preventDefault();
1159
+ clearErrors('reset'); // Clear errors specific to the reset password form
1160
+
1161
+ let isValid = true;
1162
+ const email = resetEmailInput.value.trim(); // This should be currentForgotEmail
1163
+ const newPassword = newPasswordInput.value.trim();
1164
+ const confirmNewPassword = confirmNewPasswordInput.value.trim();
1165
+
1166
+
1167
+ if (email === '') { // Should be pre-filled, but a final check
1168
+ isValid = false;
1169
+ resetEmailError.textContent = 'Email is required.';
1170
+ resetEmailInput.classList.add('error');
1171
+ } else if (!isValidEmail(email)) {
1172
+ isValid = false;
1173
+ resetEmailError.textContent = 'Invalid email format.'; // Should not happen if pre-filled correctly
1174
+ resetEmailInput.classList.add('error');
1175
+ }
1176
+
1177
+ if (newPassword === '') {
1178
+ isValid = false;
1179
+ newPasswordError.textContent = 'New password is required.';
1180
+ newPasswordInput.classList.add('error');
1181
+ } else if (newPassword.length < 6) {
1182
+ isValid = false;
1183
+ newPasswordError.textContent = 'New password must be at least 6 characters.';
1184
+ newPasswordInput.classList.add('error');
1185
+ }
1186
+
1187
+ if (confirmNewPassword === '') {
1188
+ isValid = false;
1189
+ confirmNewPasswordError.textContent = 'Confirm new password is required.';
1190
+ confirmNewPasswordInput.classList.add('error');
1191
+ } else if (newPassword !== confirmNewPassword) {
1192
+ isValid = false;
1193
+ confirmNewPasswordError.textContent = 'Passwords do not match.';
1194
+ confirmNewPasswordInput.classList.add('error');
1195
+ newPasswordInput.classList.add('error'); // Highlight both if they don't match
1196
+ }
1197
+
1198
+
1199
+ if (isValid) {
1200
+ resetSubmitButton.disabled = true;
1201
+ resetSubmitButton.innerHTML = '<span class="loading-spinner"></span> Resetting...';
1202
+
1203
+ try {
1204
+ const response = await fetch('/api/reset_password', {
1205
+ method: 'POST',
1206
+ headers: { 'Content-Type': 'application/json' },
1207
+ body: JSON.stringify({ email: email, new_password: newPassword }) // Ensure correct variable name for new password
1208
+ });
1209
+
1210
+ const result = await response.json();
1211
+
1212
+ if (response.ok) {
1213
+ hideCustomModal(resetPasswordModal);
1214
+ resetPasswordForm.reset();
1215
+ currentForgotEmail = ''; // Clear stored email after successful reset
1216
+ showFlexibleModal(
1217
+ '<p>Password reset successful. You can now login with your new password.</p>',
1218
+ [{ text: 'OK', action: () => {
1219
+ loginRadio.checked = true;
1220
+ slideToLogin();
1221
+ }}]
1222
+ );
1223
+ } else {
1224
+ // Display error messages from the backend
1225
+ displayModalFormErrors(result, resetPasswordForm, 'reset');
1226
+ if (result.message && (result.message.includes('OTP not verified') || result.message.includes('not initiated'))) {
1227
+ showFlexibleModal(
1228
+ `<p>${result.message}</p><p>Please start the password reset process again.</p>`,
1229
+ [{ text: 'Start Over', action: () => {
1230
+ hideCustomModal(resetPasswordModal);
1231
+ forgotPasswordLink.click(); // Re-open the initial forgot password modal
1232
+ }}],
1233
+ null // No general callback
1234
+ );
1235
+ }
1236
+ }
1237
+ } catch (error) {
1238
+ console.error('Fetch error:', error);
1239
+ showFlexibleModal('<p>Network error. Could not reset password. Please try again.</p>', [{ text: 'OK' }]);
1240
+ } finally {
1241
+ resetSubmitButton.disabled = false;
1242
+ resetSubmitButton.innerHTML = 'Reset Password';
1243
+ }
1244
+ }
1245
+ });
1246
+ }
1247
+
1248
+ // This function handles displaying errors from the backend for the main login/signup forms
1249
+ function handleMainFormErrors(result, formElement, endpoint) {
1250
+ if (endpoint === '/api/login') {
1251
+ if (result.message && (result.message.includes('Account not found') || result.message.includes('Invalid credentials'))) {
1252
+ showFlexibleModal(
1253
+ `<p>${result.message}</p><p>If you are not registered, please sign up.</p>`,
1254
+ [{ text: 'Sign Up', action: () => {
1255
+ signupRadio.checked = true;
1256
+ slideToSignup();
1257
+ }}, { text: 'OK', style: 'background-color: var(--bs-secondary-color);' }]
1258
+ );
1259
+ }
1260
+ } else if (endpoint === '/api/signup') {
1261
+ if (result.message && (result.message.includes('Email already registered') || result.message.includes('Username already taken'))) {
1262
+ showFlexibleModal(
1263
+ `<p>${result.message}</p><p>If you are already registered, please log in.</p>`,
1264
+ [{ text: 'Login', action: () => {
1265
+ loginRadio.checked = true;
1266
+ slideToLogin();
1267
+ }}, { text: 'OK', style: 'background-color: var(--bs-secondary-color);' }]
1268
+ );
1269
+ }
1270
+ }
1271
+ if (result.errors) {
1272
+ for (const field in result.errors) {
1273
+ let inputElement, errorElement;
1274
+
1275
+ if (formElement.id === 'loginForm' && field === 'username_or_email') {
1276
+ inputElement = loginUsernameOrEmailInput;
1277
+ errorElement = loginUsernameOrEmailError;
1278
+ } else if (formElement.id === 'signupForm' && field === 'name') {
1279
+ inputElement = signupNameInput;
1280
+ errorElement = signupNameError;
1281
+ }
1282
+ else {
1283
+ // This handles password errors for login and email/password for signup
1284
+ // Need to be careful with field names. Example: if backend returns 'password' for signup.
1285
+ inputElement = formElement.querySelector(`[name="${field}"]`);
1286
+ // This assumes predictable error element IDs based on input IDs
1287
+ const inputId = inputElement ? inputElement.id : null;
1288
+ errorElement = inputId ? document.getElementById(`${inputId}Error`) : null;
1289
+ }
1290
+
1291
+ if (errorElement) errorElement.textContent = result.errors[field];
1292
+ if (inputElement) inputElement.classList.add('error');
1293
+ }
1294
+ }
1295
+ }
1296
+
1297
+ // Generic function for handling errors in modals (forgot, otp, reset)
1298
+ function displayModalFormErrors(result, formElement, formContext) {
1299
+ // Clear errors specific to the current modal's form context
1300
+ clearErrors(formContext);
1301
+
1302
+ if (result.errors) {
1303
+ for (const field in result.errors) {
1304
+ // This is robust: Find the input element by its 'name' attribute
1305
+ const inputElement = formElement.querySelector(`[name="${field}"]`);
1306
+ // Then, find its associated error message element by looking for a sibling with the ID pattern
1307
+ const errorElement = inputElement ? document.getElementById(`${inputElement.id}Error`) : null;
1308
+
1309
+ if (errorElement) errorElement.textContent = result.errors[field];
1310
+ if (inputElement) inputElement.classList.add('error');
1311
+ }
1312
+ }
1313
+ // Update the main message area of the modal if a general message is returned
1314
+ const targetModalMessage = formElement.closest('.modal-content').querySelector('.modal-message');
1315
+ if (targetModalMessage && result.message) {
1316
+ targetModalMessage.innerHTML = `<p>${result.message}</p>`;
1317
+ }
1318
+ }
1319
+
1320
+ // Main data submission function (unchanged for main forms, adapted for modals)
1321
+ async function submitFormData(formElement, submitButton, endpoint, formContext) {
1322
+ if (!formElement || !submitButton || !endpoint) {
1323
+ console.error("submitFormData called with missing arguments.");
1324
+ return;
1325
+ }
1326
+
1327
+ const formData = new FormData(formElement);
1328
+ const data = {};
1329
+ for (let [key, value] of formData.entries()) {
1330
+ data[key] = value;
1331
+ }
1332
+
1333
+ submitButton.disabled = true;
1334
+ if (formContext === 'main') {
1335
+ displayFormMessage("Submitting...", 'info');
1336
+ } else {
1337
+ // For modal buttons, update their innerHTML directly
1338
+ submitButton.innerHTML = '<span class="loading-spinner"></span> Submitting...';
1339
+ }
1340
+
1341
+ try {
1342
+ const response = await fetch(endpoint, {
1343
+ method: 'POST',
1344
+ headers: {
1345
+ 'Content-Type': 'application/json',
1346
+ },
1347
+ body: JSON.stringify(data),
1348
+ });
1349
+
1350
+ const result = await response.json();
1351
+
1352
+ if (response.ok) {
1353
+ if (formContext === 'main') {
1354
+ clearErrors('main');
1355
+ formElement.reset();
1356
+ displayFormMessage('', 'info'); // Clear "Submitting..." message
1357
+ loginAttempts = 0; // Reset login attempts on successful login
1358
+
1359
+ if (endpoint === '/api/signup') {
1360
+ showFlexibleModal(
1361
+ '<p>Registration successful! Please log in.</p>',
1362
+ [{ text: 'OK', action: () => {
1363
+ loginRadio.checked = true;
1364
+ slideToLogin();
1365
+ const url = new URL(window.location);
1366
+ url.searchParams.delete('signup'); // Clean up URL
1367
+ window.history.replaceState({}, '', url.toString());
1368
+ }}]
1369
+ );
1370
+ } else if (endpoint === '/api/login') {
1371
+ showFlexibleModal(
1372
+ '<p>Login successful!</p>',
1373
+ [{
1374
+ text: 'OK',
1375
+ action: () => {
1376
+ // Mark user as logged in so chat.html knows
1377
+ localStorage.setItem('gakr_is_logged_in', 'true');
1378
+ alert('Welcome! You are logged in.');
1379
+ // Optional: remove any guest flag if you had one
1380
+ // localStorage.removeItem('gakr_is_guest');
1381
+
1382
+ // Redirect back to chat interface
1383
+ window.location.href = 'chat.html';
1384
+ }
1385
+ }]
1386
+ );
1387
+ }
1388
+
1389
+ }
1390
+ // For forgot/otp/reset, successful handling is now done directly in their respective event listeners
1391
+ // so no generic handling here for those contexts.
1392
+ } else { // Server returned an error (response.ok is false)
1393
+ if (formContext === 'main') {
1394
+ if (endpoint === '/api/login' && result.message && result.message.includes('Invalid credentials')) {
1395
+ loginAttempts++;
1396
+ loginPasswordInput.classList.add('error');
1397
+ loginPasswordError.textContent = result.message;
1398
+
1399
+ if (loginAttempts >= MAX_LOGIN_ATTEMPTS) {
1400
+ showLoginAttemptsLimitModal();
1401
+ loginPasswordInput.value = ''; // Clear password after max attempts
1402
+ } else {
1403
+ displayFormMessage(`Incorrect password. You have ${MAX_LOGIN_ATTEMPTS - loginAttempts} attempts left.`, 'error');
1404
+ }
1405
+ } else {
1406
+ displayFormMessage(result.message || 'An error occurred.', 'error');
1407
+ handleMainFormErrors(result, formElement, endpoint);
1408
+ }
1409
+ } else if (formContext === 'forgot') {
1410
+ displayModalFormErrors(result, formElement, 'forgot');
1411
+ } else if (formContext === 'otp') {
1412
+ displayModalFormErrors(result, formElement, 'otp');
1413
+ } else if (formContext === 'reset') {
1414
+ displayModalFormErrors(result, formElement, 'reset');
1415
+ }
1416
+ }
1417
+ } catch (error) {
1418
+ console.error('Fetch error:', error);
1419
+ if (formContext === 'main') {
1420
+ displayFormMessage('Network error. Please try again.', 'error');
1421
+ } else {
1422
+ showFlexibleModal(
1423
+ '<p>Network error. Please check your internet connection and try again.</p>',
1424
+ [{ text: 'OK' }]
1425
+ );
1426
+ }
1427
+ } finally {
1428
+ submitButton.disabled = false;
1429
+ // Reset button text specifically for modal forms if not handled by success/error flows above
1430
+ if (formContext === 'forgot') {
1431
+ submitButton.innerHTML = 'Request OTP';
1432
+ } else if (formContext === 'otp') {
1433
+ submitButton.innerHTML = 'Verify OTP';
1434
+ } else if (formContext === 'reset') {
1435
+ submitButton.innerHTML = 'Reset Password';
1436
+ }
1437
+ }
1438
+ }
1439
+
1440
+
1441
+ function showLoginAttemptsLimitModal() {
1442
+ const messageHtml = `
1443
+ <strong>You have made three incorrect login attempts.</strong>
1444
+ <ul>
1445
+ <li>1. If you are not registered, please <a href="#" id="modalSignupLink">sign up now</a>.</li>
1446
+ <li>2. If you are already registered, please <a href="#" id="modalLoginLink">login now</a> with correct credentials.</li>
1447
+ <li>3. If you have forgotten your password, click <a href="#" id="modalForgotPasswordLink">forgot password</a> to reset it.</li>
1448
+ </ul>
1449
+ `;
1450
+
1451
+ const buttons = [
1452
+ { text: 'Login', action: () => {
1453
+ loginRadio.checked = true;
1454
+ slideToLogin();
1455
+ loginAttempts = 0;
1456
+ loginPasswordInput.value = '';
1457
+ }},
1458
+ { text: 'Signup', action: () => {
1459
+ signupRadio.checked = true;
1460
+ slideToSignup();
1461
+ loginAttempts = 0;
1462
+ loginPasswordInput.value = '';
1463
+ }}
1464
+ ];
1465
+
1466
+ showFlexibleModal(messageHtml, buttons, () => {
1467
+ const modalSignupLink = document.getElementById('modalSignupLink');
1468
+ const modalLoginLink = document.getElementById('modalLoginLink');
1469
+ const modalForgotPasswordLink = document.getElementById('modalForgotPasswordLink');
1470
+
1471
+ if (modalSignupLink) {
1472
+ modalSignupLink.onclick = (e) => {
1473
+ e.preventDefault();
1474
+ hideCustomModal(customModal);
1475
+ signupRadio.checked = true;
1476
+ slideToSignup();
1477
+ loginAttempts = 0;
1478
+ loginPasswordInput.value = '';
1479
+ };
1480
+ }
1481
+ if (modalLoginLink) {
1482
+ modalLoginLink.onclick = (e) => {
1483
+ e.preventDefault();
1484
+ hideCustomModal(customModal);
1485
+ loginRadio.checked = true;
1486
+ slideToLogin();
1487
+ loginAttempts = 0;
1488
+ loginPasswordInput.value = '';
1489
+ };
1490
+ }
1491
+ if (modalForgotPasswordLink) {
1492
+ modalForgotPasswordLink.onclick = (e) => {
1493
+ e.preventDefault();
1494
+ hideCustomModal(customModal);
1495
+ forgotPasswordLink.click();
1496
+ loginAttempts = 0;
1497
+ loginPasswordInput.value = '';
1498
+ };
1499
+ }
1500
+ });
1501
+ }
1502
+
1503
+ function isValidEmail(email) {
1504
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1505
+ return emailRegex.test(email);
1506
+ }
1507
+ });
1508
+ </script>
1509
+ </body>
1510
+ </html>
file_pipeline.py ADDED
@@ -0,0 +1,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ file_pipeline.py
4
+
5
+ Universal file pipeline for GAKR AI.
6
+
7
+ Responsibilities:
8
+ - Create dataupload/ folder structure (if not present)
9
+ - Save uploaded files to disk
10
+ - Detect file type
11
+ - Run type-specific extractors
12
+ - Return structured, text-friendly context for Phi-3
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ import mimetypes
19
+ from datetime import datetime
20
+ from typing import List, Dict, Any
21
+
22
+ import pandas as pd
23
+ import pdfplumber
24
+ import docx
25
+ import fitz # PyMuPDF
26
+ from PIL import Image
27
+ import pytesseract
28
+ import whisper
29
+ import ffmpeg
30
+ from fastapi import UploadFile
31
+
32
+ # ============================================================
33
+ # PATHS & FOLDERS
34
+ # ============================================================
35
+
36
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
37
+ UPLOAD_ROOT = os.path.join(BASE_DIR, "dataupload")
38
+
39
+ FOLDERS = {
40
+ "image": "images",
41
+ "video": "videos",
42
+ "audio": "audio",
43
+ "document": "documents",
44
+ "tabular": "tabular",
45
+ "other": "other",
46
+ }
47
+
48
+
49
+ def ensure_folders() -> None:
50
+ """
51
+ Ensure base upload folder and all subfolders exist.
52
+ """
53
+ os.makedirs(UPLOAD_ROOT, exist_ok=True)
54
+ for sub in FOLDERS.values():
55
+ os.makedirs(os.path.join(UPLOAD_ROOT, sub), exist_ok=True)
56
+
57
+
58
+ ensure_folders()
59
+
60
+ # ============================================================
61
+ # TYPE DETECTION & PATHS
62
+ # ============================================================
63
+
64
+
65
+ def detect_kind(filename: str, content_type: str | None) -> str:
66
+ """
67
+ Decide logical kind: tabular, document, image, audio, video, other.
68
+ """
69
+ ext = os.path.splitext(filename)[1].lower()
70
+
71
+ if ext in [".csv", ".xlsx", ".xls", ".json"]:
72
+ return "tabular"
73
+ if ext in [".pdf", ".txt"]:
74
+ return "document"
75
+ if ext in [".docx"]:
76
+ return "document"
77
+ if ext in [".png", ".jpg", ".jpeg", ".webp", ".bmp"]:
78
+ return "image"
79
+ if ext in [".mp3", ".wav", ".m4a"]:
80
+ return "audio"
81
+ if ext in [".mp4", ".mkv", ".mov", ".avi"]:
82
+ return "video"
83
+
84
+ if content_type:
85
+ if content_type.startswith("image/"):
86
+ return "image"
87
+ if content_type.startswith("audio/"):
88
+ return "audio"
89
+ if content_type.startswith("video/"):
90
+ return "video"
91
+
92
+ return "other"
93
+
94
+
95
+ def make_target_path(kind: str, filename: str) -> str:
96
+ """
97
+ Build a safe, timestamped filepath under dataupload/{sub}/.
98
+ """
99
+ sub = FOLDERS.get(kind, "other")
100
+ safe_name = os.path.basename(filename)
101
+ timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S_%f")
102
+ final_name = f"{timestamp}_{safe_name}"
103
+ return os.path.join(UPLOAD_ROOT, sub, final_name)
104
+
105
+
106
+ # ============================================================
107
+ # MAIN MULTI-FILE ENTRY POINT
108
+ # ============================================================
109
+
110
+
111
+ async def process_files(files: List[UploadFile]) -> Dict[str, Any]:
112
+ """
113
+ Save all files, run analyses, and return structured context.
114
+
115
+ Output example:
116
+ {
117
+ "files": [
118
+ {
119
+ "original_name": "...",
120
+ "stored_path": "dataupload/documents/...",
121
+ "kind": "document",
122
+ "summary": { ... }
123
+ },
124
+ ...
125
+ ]
126
+ }
127
+ """
128
+ ensure_folders()
129
+ results: List[Dict[str, Any]] = []
130
+
131
+ for uf in files:
132
+ try:
133
+ kind = detect_kind(uf.filename, uf.content_type)
134
+ target_path = make_target_path(kind, uf.filename)
135
+
136
+ # Save file to disk
137
+ try:
138
+ with open(target_path, "wb") as out:
139
+ data = await uf.read()
140
+ out.write(data)
141
+ except Exception as save_err:
142
+ results.append(
143
+ {
144
+ "original_name": uf.filename,
145
+ "stored_path": None,
146
+ "kind": kind,
147
+ "summary": {
148
+ "error": f"Failed to save file: {save_err}"
149
+ },
150
+ }
151
+ )
152
+ continue
153
+
154
+ # Analyze by type
155
+ try:
156
+ summary = analyze_file(target_path, kind)
157
+ except Exception as analyze_err:
158
+ summary = {
159
+ "error": f"Unexpected error in analysis: {analyze_err}"
160
+ }
161
+
162
+ results.append(
163
+ {
164
+ "original_name": uf.filename,
165
+ "stored_path": os.path.relpath(target_path, BASE_DIR),
166
+ "kind": kind,
167
+ "summary": summary,
168
+ }
169
+ )
170
+
171
+ except Exception as outer_err:
172
+ results.append(
173
+ {
174
+ "original_name": getattr(uf, "filename", "unknown"),
175
+ "stored_path": None,
176
+ "kind": "unknown",
177
+ "summary": {
178
+ "error": f"Fatal error while handling file: {outer_err}"
179
+ },
180
+ }
181
+ )
182
+
183
+ return {"files": results}
184
+
185
+
186
+ # ============================================================
187
+ # TYPE-SPECIFIC ANALYSIS
188
+ # ============================================================
189
+
190
+
191
+ def analyze_file(path: str, kind: str) -> Dict[str, Any]:
192
+ if kind == "tabular":
193
+ return analyze_tabular(path)
194
+ if kind == "document":
195
+ return analyze_document(path)
196
+ if kind == "image":
197
+ return analyze_image(path)
198
+ if kind == "audio":
199
+ return analyze_audio(path)
200
+ if kind == "video":
201
+ return analyze_video(path)
202
+ return {"type": "other", "note": "Unsupported or unknown file type"}
203
+
204
+
205
+ # ---------- TABULAR: CSV / Excel / JSON ----------
206
+
207
+
208
+ def analyze_tabular(path: str) -> Dict[str, Any]:
209
+ ext = os.path.splitext(path)[1].lower()
210
+ df = None
211
+
212
+ try:
213
+ if ext == ".csv":
214
+ df = pd.read_csv(path)
215
+ elif ext in [".xlsx", ".xls"]:
216
+ df = pd.read_excel(path)
217
+ elif ext == ".json":
218
+ df = pd.read_json(path)
219
+ else:
220
+ return {
221
+ "type": "tabular",
222
+ "error": f"Unsupported tabular format: {ext}",
223
+ }
224
+ except Exception as e:
225
+ return {"type": "tabular", "error": f"Failed to load table: {e}"}
226
+
227
+ summary: Dict[str, Any] = {
228
+ "type": "tabular",
229
+ "rows": int(df.shape[0]),
230
+ "columns": [str(c) for c in df.columns],
231
+ }
232
+
233
+ try:
234
+ summary["missing_values"] = df.isna().sum().to_dict()
235
+ except Exception as e:
236
+ summary["missing_values_error"] = str(e)
237
+
238
+ try:
239
+ summary["numeric_stats"] = df.describe(include="number").to_dict()
240
+ except Exception:
241
+ summary["numeric_stats"] = {}
242
+
243
+ return summary
244
+
245
+
246
+ # ---------- DOCUMENTS: PDF / DOCX / TXT ----------
247
+
248
+
249
+ def analyze_document(path: str) -> Dict[str, Any]:
250
+ ext = os.path.splitext(path)[1].lower()
251
+ text = ""
252
+
253
+ try:
254
+ if ext == ".pdf":
255
+ # First try pdfplumber
256
+ try:
257
+ with pdfplumber.open(path) as pdf:
258
+ pages = []
259
+ for page in pdf.pages[:10]:
260
+ t = page.extract_text()
261
+ if t:
262
+ pages.append(t)
263
+ text = "\n".join(pages)
264
+ except Exception:
265
+ # Fallback to PyMuPDF
266
+ doc = fitz.open(path)
267
+ chunks = []
268
+ for page in doc[:10]:
269
+ chunks.append(page.get_text())
270
+ text = "\n".join(chunks)
271
+ elif ext == ".docx":
272
+ d = docx.Document(path)
273
+ paras = [p.text for p in d.paragraphs if p.text.strip()]
274
+ text = "\n".join(paras)
275
+ else: # .txt or unknown plain-text
276
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
277
+ text = f.read()
278
+ except Exception as e:
279
+ return {
280
+ "type": "document",
281
+ "error": f"Failed to extract document text: {e}",
282
+ }
283
+
284
+ short = text[:4000]
285
+ return {
286
+ "type": "document",
287
+ "char_count": len(text),
288
+ "preview": short,
289
+ }
290
+
291
+
292
+ # ---------- IMAGES ----------
293
+
294
+
295
+ def analyze_image(path: str) -> Dict[str, Any]:
296
+ try:
297
+ img = Image.open(path)
298
+ except Exception as e:
299
+ return {"type": "image", "error": f"Failed to open image: {e}"}
300
+
301
+ try:
302
+ text = pytesseract.image_to_string(img)
303
+ except Exception as e:
304
+ text = ""
305
+ ocr_error = str(e)
306
+ else:
307
+ ocr_error = None
308
+
309
+ short = text[:2000]
310
+
311
+ result: Dict[str, Any] = {
312
+ "type": "image",
313
+ "size": {"width": img.width, "height": img.height},
314
+ "ocr_preview": short,
315
+ }
316
+ if ocr_error:
317
+ result["ocr_error"] = ocr_error
318
+
319
+ return result
320
+
321
+
322
+ # ---------- AUDIO (Whisper) ----------
323
+
324
+ _whisper_model = None
325
+
326
+
327
+ def get_whisper_model():
328
+ global _whisper_model
329
+ if _whisper_model is None:
330
+ try:
331
+ _whisper_model = whisper.load_model("small")
332
+ except Exception as e:
333
+ raise RuntimeError(f"Failed to load Whisper model: {e}")
334
+ return _whisper_model
335
+
336
+
337
+ def analyze_audio(path: str) -> Dict[str, Any]:
338
+ try:
339
+ model = get_whisper_model()
340
+ except Exception as e:
341
+ return {"type": "audio", "error": str(e)}
342
+
343
+ try:
344
+ result = model.transcribe(path)
345
+ except Exception as e:
346
+ return {"type": "audio", "error": f"Whisper transcription failed: {e}"}
347
+
348
+ text = result.get("text", "") or ""
349
+ short = text[:4000]
350
+ duration = None
351
+ try:
352
+ if result.get("segments"):
353
+ duration = result["segments"][-1].get("end", None)
354
+ except Exception:
355
+ duration = None
356
+
357
+ return {
358
+ "type": "audio",
359
+ "duration_sec": duration,
360
+ "transcript_preview": short,
361
+ }
362
+
363
+
364
+ # ---------- VIDEO (audio extraction + Whisper) ----------
365
+
366
+
367
+ def analyze_video(path: str) -> Dict[str, Any]:
368
+ audio_path = path + ".tmp_audio.wav"
369
+ audio_summary: Dict[str, Any]
370
+
371
+ try:
372
+ (
373
+ ffmpeg
374
+ .input(path)
375
+ .output(audio_path, ac=1, ar=16000)
376
+ .overwrite_output()
377
+ .run(quiet=True)
378
+ )
379
+ except Exception as e:
380
+ return {
381
+ "type": "video",
382
+ "error": f"Failed to extract audio from video: {e}",
383
+ }
384
+
385
+ try:
386
+ audio_summary = analyze_audio(audio_path)
387
+ finally:
388
+ try:
389
+ if os.path.exists(audio_path):
390
+ os.remove(audio_path)
391
+ except Exception:
392
+ pass
393
+
394
+ return {
395
+ "type": "video",
396
+ "audio_analysis": audio_summary,
397
+ }