AbdulWahab14 commited on
Commit
f0c1afd
Β·
verified Β·
1 Parent(s): 30ce828

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1381 -0
app.py ADDED
@@ -0,0 +1,1381 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from transformers import pipeline
3
+ import PyPDF2
4
+ import re
5
+ import os
6
+ import io
7
+ import random
8
+ import time
9
+ import hashlib
10
+ import secrets
11
+ import json
12
+ import sqlite3
13
+ from datetime import datetime, timedelta
14
+ from pathlib import Path
15
+ from groq import Groq
16
+
17
+ # ==================== CONFIGURATION ====================
18
+ try:
19
+ from google import genai
20
+ from google.genai import types
21
+ GENAI_AVAILABLE = True
22
+ except ImportError:
23
+ GENAI_AVAILABLE = False
24
+ genai = None
25
+ types = None
26
+
27
+ # Environment variables
28
+ hf_token = os.environ.get("HF_TOKEN")
29
+ gemini_key = os.environ.get("GEMINI_API_KEY")
30
+ groq_key = os.environ.get("GROQ_API_KEY")
31
+ dev_password = os.environ.get("DEV_PASSWORD", "dev123")
32
+
33
+ # Database path (persistent storage)
34
+ DATA_DIR = Path("/tmp/data") if os.path.exists("/tmp") else Path("./data")
35
+ DATA_DIR.mkdir(exist_ok=True)
36
+ DB_PATH = DATA_DIR / "student_ai_suite.db"
37
+
38
+ # API Clients
39
+ gemini_client = None
40
+ groq_client = None
41
+
42
+ if gemini_key and GENAI_AVAILABLE:
43
+ try:
44
+ gemini_client = genai.Client(api_key=gemini_key)
45
+ except:
46
+ try:
47
+ import google.generativeai as old_genai
48
+ old_genai.configure(api_key=gemini_key)
49
+ gemini_client = old_genai
50
+ except:
51
+ pass
52
+
53
+ if groq_key:
54
+ try:
55
+ groq_client = Groq(api_key=groq_key)
56
+ except:
57
+ pass
58
+
59
+ # Lazy load summarizer
60
+ summarizer = None
61
+
62
+ def load_summarizer():
63
+ global summarizer
64
+ if summarizer is None:
65
+ try:
66
+ summarizer = pipeline("summarization", model="sshleifer/distilbart-cnn-12-6", device=-1)
67
+ except:
68
+ pass
69
+ return summarizer
70
+
71
+ # ==================== DATABASE MANAGER ====================
72
+ class DatabaseManager:
73
+ def __init__(self, db_path):
74
+ self.db_path = db_path
75
+ self.init_database()
76
+
77
+ def get_connection(self):
78
+ """Get database connection with row factory"""
79
+ conn = sqlite3.connect(self.db_path)
80
+ conn.row_factory = sqlite3.Row
81
+ return conn
82
+
83
+ def init_database(self):
84
+ """Initialize database tables"""
85
+ conn = self.get_connection()
86
+ cursor = conn.cursor()
87
+
88
+ # Users table
89
+ cursor.execute("""
90
+ CREATE TABLE IF NOT EXISTS users (
91
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
92
+ username TEXT UNIQUE NOT NULL,
93
+ password_hash TEXT NOT NULL,
94
+ name TEXT NOT NULL,
95
+ email TEXT,
96
+ role TEXT DEFAULT 'user',
97
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
98
+ last_login TIMESTAMP,
99
+ is_active BOOLEAN DEFAULT 1,
100
+ is_dev BOOLEAN DEFAULT 0
101
+ )
102
+ """)
103
+
104
+ # Sessions table for tracking active sessions
105
+ cursor.execute("""
106
+ CREATE TABLE IF NOT EXISTS sessions (
107
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
108
+ user_id INTEGER NOT NULL,
109
+ session_token TEXT UNIQUE NOT NULL,
110
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
111
+ expires_at TIMESTAMP NOT NULL,
112
+ ip_address TEXT,
113
+ user_agent TEXT,
114
+ is_active BOOLEAN DEFAULT 1,
115
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
116
+ )
117
+ """)
118
+
119
+ # Activity logs (optional - for analytics)
120
+ cursor.execute("""
121
+ CREATE TABLE IF NOT EXISTS activity_logs (
122
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
123
+ user_id INTEGER,
124
+ action TEXT NOT NULL,
125
+ tool_used TEXT,
126
+ input_summary TEXT,
127
+ output_summary TEXT,
128
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
129
+ execution_time_ms INTEGER,
130
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
131
+ )
132
+ """)
133
+
134
+ # User preferences
135
+ cursor.execute("""
136
+ CREATE TABLE IF NOT EXISTS user_preferences (
137
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
138
+ user_id INTEGER UNIQUE NOT NULL,
139
+ default_essay_type TEXT DEFAULT 'Argumentative',
140
+ default_essay_tone TEXT DEFAULT 'Academic',
141
+ default_word_count INTEGER DEFAULT 500,
142
+ default_quiz_questions INTEGER DEFAULT 5,
143
+ default_quiz_time INTEGER DEFAULT 2,
144
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
145
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
146
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
147
+ )
148
+ """)
149
+
150
+ conn.commit()
151
+ conn.close()
152
+ print(f"βœ… Database initialized at {self.db_path}")
153
+
154
+ # ==================== USER OPERATIONS ====================
155
+
156
+ def create_user(self, username, password_hash, name, email, role="user", is_dev=False):
157
+ """Create new user"""
158
+ try:
159
+ conn = self.get_connection()
160
+ cursor = conn.cursor()
161
+
162
+ cursor.execute("""
163
+ INSERT INTO users (username, password_hash, name, email, role, is_dev)
164
+ VALUES (?, ?, ?, ?, ?, ?)
165
+ """, (username, password_hash, name, email, role, is_dev))
166
+
167
+ user_id = cursor.lastrowid
168
+
169
+ # Create default preferences
170
+ cursor.execute("""
171
+ INSERT INTO user_preferences (user_id) VALUES (?)
172
+ """, (user_id,))
173
+
174
+ conn.commit()
175
+ conn.close()
176
+ return True, user_id
177
+
178
+ except sqlite3.IntegrityError:
179
+ return False, "Username already exists"
180
+ except Exception as e:
181
+ return False, str(e)
182
+
183
+ def get_user_by_username(self, username):
184
+ """Get user by username"""
185
+ conn = self.get_connection()
186
+ cursor = conn.cursor()
187
+
188
+ cursor.execute("""
189
+ SELECT * FROM users WHERE username = ? AND is_active = 1
190
+ """, (username,))
191
+
192
+ user = cursor.fetchone()
193
+ conn.close()
194
+
195
+ if user:
196
+ return dict(user)
197
+ return None
198
+
199
+ def get_user_by_id(self, user_id):
200
+ """Get user by ID"""
201
+ conn = self.get_connection()
202
+ cursor = conn.cursor()
203
+
204
+ cursor.execute("""
205
+ SELECT * FROM users WHERE id = ? AND is_active = 1
206
+ """, (user_id,))
207
+
208
+ user = cursor.fetchone()
209
+ conn.close()
210
+
211
+ if user:
212
+ return dict(user)
213
+ return None
214
+
215
+ def update_last_login(self, user_id):
216
+ """Update last login timestamp"""
217
+ conn = self.get_connection()
218
+ cursor = conn.cursor()
219
+
220
+ cursor.execute("""
221
+ UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?
222
+ """, (user_id,))
223
+
224
+ conn.commit()
225
+ conn.close()
226
+
227
+ # ==================== SESSION OPERATIONS ====================
228
+
229
+ def create_session(self, user_id, session_token, expires_at, ip_address=None, user_agent=None):
230
+ """Create new session"""
231
+ conn = self.get_connection()
232
+ cursor = conn.cursor()
233
+
234
+ # Invalidate old sessions for this user
235
+ cursor.execute("""
236
+ UPDATE sessions SET is_active = 0 WHERE user_id = ?
237
+ """, (user_id,))
238
+
239
+ cursor.execute("""
240
+ INSERT INTO sessions (user_id, session_token, expires_at, ip_address, user_agent)
241
+ VALUES (?, ?, ?, ?, ?)
242
+ """, (user_id, session_token, expires_at, ip_address, user_agent))
243
+
244
+ conn.commit()
245
+ conn.close()
246
+ return True
247
+
248
+ def get_session(self, session_token):
249
+ """Validate and get session"""
250
+ conn = self.get_connection()
251
+ cursor = conn.cursor()
252
+
253
+ cursor.execute("""
254
+ SELECT s.*, u.username, u.name, u.role, u.is_dev
255
+ FROM sessions s
256
+ JOIN users u ON s.user_id = u.id
257
+ WHERE s.session_token = ?
258
+ AND s.is_active = 1
259
+ AND s.expires_at > CURRENT_TIMESTAMP
260
+ AND u.is_active = 1
261
+ """, (session_token,))
262
+
263
+ session = cursor.fetchone()
264
+ conn.close()
265
+
266
+ if session:
267
+ return dict(session)
268
+ return None
269
+
270
+ def invalidate_session(self, session_token):
271
+ """Logout - invalidate session"""
272
+ conn = self.get_connection()
273
+ cursor = conn.cursor()
274
+
275
+ cursor.execute("""
276
+ UPDATE sessions SET is_active = 0 WHERE session_token = ?
277
+ """, (session_token,))
278
+
279
+ conn.commit()
280
+ conn.close()
281
+ return True
282
+
283
+ def cleanup_expired_sessions(self):
284
+ """Remove expired sessions"""
285
+ conn = self.get_connection()
286
+ cursor = conn.cursor()
287
+
288
+ cursor.execute("""
289
+ UPDATE sessions SET is_active = 0 WHERE expires_at < CURRENT_TIMESTAMP
290
+ """)
291
+
292
+ conn.commit()
293
+ conn.close()
294
+
295
+ # ==================== PREFERENCES ====================
296
+
297
+ def get_user_preferences(self, user_id):
298
+ """Get user preferences"""
299
+ conn = self.get_connection()
300
+ cursor = conn.cursor()
301
+
302
+ cursor.execute("""
303
+ SELECT * FROM user_preferences WHERE user_id = ?
304
+ """, (user_id,))
305
+
306
+ prefs = cursor.fetchone()
307
+ conn.close()
308
+
309
+ if prefs:
310
+ return dict(prefs)
311
+ return None
312
+
313
+ def update_preferences(self, user_id, **kwargs):
314
+ """Update user preferences"""
315
+ allowed_fields = ['default_essay_type', 'default_essay_tone', 'default_word_count',
316
+ 'default_quiz_questions', 'default_quiz_time']
317
+
318
+ updates = {k: v for k, v in kwargs.items() if k in allowed_fields}
319
+
320
+ if not updates:
321
+ return False
322
+
323
+ set_clause = ", ".join([f"{k} = ?" for k in updates.keys()])
324
+ values = list(updates.values()) + [user_id]
325
+
326
+ conn = self.get_connection()
327
+ cursor = conn.cursor()
328
+
329
+ cursor.execute(f"""
330
+ UPDATE user_preferences
331
+ SET {set_clause}, updated_at = CURRENT_TIMESTAMP
332
+ WHERE user_id = ?
333
+ """, values)
334
+
335
+ conn.commit()
336
+ conn.close()
337
+ return True
338
+
339
+ # ==================== ACTIVITY LOGGING ====================
340
+
341
+ def log_activity(self, user_id, action, tool_used=None, input_summary=None,
342
+ output_summary=None, execution_time_ms=None):
343
+ """Log user activity (optional analytics)"""
344
+ conn = self.get_connection()
345
+ cursor = conn.cursor()
346
+
347
+ cursor.execute("""
348
+ INSERT INTO activity_logs
349
+ (user_id, action, tool_used, input_summary, output_summary, execution_time_ms)
350
+ VALUES (?, ?, ?, ?, ?, ?)
351
+ """, (user_id, action, tool_used, input_summary, output_summary, execution_time_ms))
352
+
353
+ conn.commit()
354
+ conn.close()
355
+
356
+ def get_user_stats(self, user_id):
357
+ """Get user activity statistics"""
358
+ conn = self.get_connection()
359
+ cursor = conn.cursor()
360
+
361
+ cursor.execute("""
362
+ SELECT
363
+ COUNT(*) as total_activities,
364
+ COUNT(DISTINCT tool_used) as tools_used,
365
+ MAX(timestamp) as last_activity
366
+ FROM activity_logs
367
+ WHERE user_id = ?
368
+ """, (user_id,))
369
+
370
+ stats = cursor.fetchone()
371
+ conn.close()
372
+
373
+ return dict(stats) if stats else None
374
+
375
+ # ==================== ADMIN OPERATIONS ====================
376
+
377
+ def get_all_users(self):
378
+ """Get all users (admin only)"""
379
+ conn = self.get_connection()
380
+ cursor = conn.cursor()
381
+
382
+ cursor.execute("""
383
+ SELECT id, username, name, email, role, created_at, last_login, is_active, is_dev
384
+ FROM users
385
+ ORDER BY created_at DESC
386
+ """)
387
+
388
+ users = [dict(row) for row in cursor.fetchall()]
389
+ conn.close()
390
+ return users
391
+
392
+ def get_database_stats(self):
393
+ """Get database statistics"""
394
+ conn = self.get_connection()
395
+ cursor = conn.cursor()
396
+
397
+ stats = {}
398
+
399
+ cursor.execute("SELECT COUNT(*) FROM users WHERE is_dev = 0")
400
+ stats['total_users'] = cursor.fetchone()[0]
401
+
402
+ cursor.execute("SELECT COUNT(*) FROM sessions WHERE is_active = 1")
403
+ stats['active_sessions'] = cursor.fetchone()[0]
404
+
405
+ cursor.execute("SELECT COUNT(*) FROM activity_logs")
406
+ stats['total_activities'] = cursor.fetchone()[0]
407
+
408
+ cursor.execute("""
409
+ SELECT tool_used, COUNT(*) as count
410
+ FROM activity_logs
411
+ GROUP BY tool_used
412
+ ORDER BY count DESC
413
+ """)
414
+ stats['tool_usage'] = [dict(row) for row in cursor.fetchall()]
415
+
416
+ conn.close()
417
+ return stats
418
+
419
+ # Initialize database
420
+ db = DatabaseManager(DB_PATH)
421
+
422
+ # ==================== USER AUTHENTICATION ====================
423
+ class UserAuth:
424
+ def __init__(self, database):
425
+ self.db = database
426
+ self.failed_attempts = {} # Still in-memory for brute force protection
427
+ self._init_dev_account()
428
+
429
+ def _hash(self, password):
430
+ return hashlib.sha256(password.encode()).hexdigest()
431
+
432
+ def _init_dev_account(self):
433
+ """Create hidden developer account"""
434
+ if dev_password:
435
+ # Check if dev exists
436
+ existing = self.db.get_user_by_username("admin")
437
+ if not existing:
438
+ self.db.create_user(
439
+ username="admin",
440
+ password_hash=self._hash(dev_password),
441
+ name="Developer",
442
+ email="dev@local",
443
+ role="developer",
444
+ is_dev=True
445
+ )
446
+ print("βœ… Developer account created")
447
+
448
+ def register(self, username, password, name, email):
449
+ """Public user registration with database"""
450
+ # Validation
451
+ if not all([username, password, name]):
452
+ return False, "All fields are required"
453
+
454
+ if len(username) < 3:
455
+ return False, "Username must be at least 3 characters"
456
+
457
+ if len(password) < 6:
458
+ return False, "Password must be at least 6 characters"
459
+
460
+ # Create user
461
+ password_hash = self._hash(password)
462
+ success, result = self.db.create_user(username, password_hash, name, email)
463
+
464
+ if success:
465
+ return True, "Account created successfully!"
466
+ else:
467
+ return False, result # Error message
468
+
469
+ def login(self, username, password):
470
+ """Login with database validation"""
471
+ if not username or not password:
472
+ return None, "Please enter both username and password"
473
+
474
+ # Brute force check
475
+ if username in self.failed_attempts:
476
+ if self.failed_attempts[username]["count"] >= 5:
477
+ last = self.failed_attempts[username]["last_attempt"]
478
+ if datetime.now() - last < timedelta(minutes=15):
479
+ return None, "Too many failed attempts. Try again in 15 minutes."
480
+ else:
481
+ del self.failed_attempts[username]
482
+
483
+ # Get user from database
484
+ user = self.db.get_user_by_username(username)
485
+
486
+ if not user:
487
+ self._record_failed_attempt(username)
488
+ return None, "Invalid username or password"
489
+
490
+ # Verify password
491
+ password_hash = self._hash(password)
492
+ if user['password_hash'] != password_hash:
493
+ self._record_failed_attempt(username)
494
+ return None, "Invalid username or password"
495
+
496
+ # Success - update last login
497
+ self.db.update_last_login(user['id'])
498
+
499
+ if username in self.failed_attempts:
500
+ del self.failed_attempts[username]
501
+
502
+ # Create session
503
+ session_token = secrets.token_urlsafe(32)
504
+ expires_at = datetime.now() + timedelta(hours=24)
505
+
506
+ self.db.create_session(
507
+ user_id=user['id'],
508
+ session_token=session_token,
509
+ expires_at=expires_at
510
+ )
511
+
512
+ return session_token, {
513
+ 'user_id': user['id'],
514
+ 'username': user['username'],
515
+ 'name': user['name'],
516
+ 'role': user['role'],
517
+ 'is_dev': user['is_dev']
518
+ }
519
+
520
+ def _record_failed_attempt(self, username):
521
+ if username not in self.failed_attempts:
522
+ self.failed_attempts[username] = {"count": 0, "last_attempt": datetime.now()}
523
+ self.failed_attempts[username]["count"] += 1
524
+ self.failed_attempts[username]["last_attempt"] = datetime.now()
525
+
526
+ def validate_session(self, session_token):
527
+ """Validate session from database"""
528
+ if not session_token:
529
+ return None
530
+
531
+ session = self.db.get_session(session_token)
532
+ return session
533
+
534
+ def logout(self, session_token):
535
+ """Logout - invalidate in database"""
536
+ if session_token:
537
+ self.db.invalidate_session(session_token)
538
+ return True
539
+
540
+ def get_user_count(self):
541
+ """Get count of non-dev users"""
542
+ stats = self.db.get_database_stats()
543
+ return stats.get('total_users', 0)
544
+
545
+ # Initialize auth with database
546
+ auth_system = UserAuth(db)
547
+
548
+ # ==================== [ALL YOUR EXISTING FUNCTIONS REMAIN THE SAME] ====================
549
+ # ... (Include all your previous functions: extract_text_from_pdf, summarize_with_gemini, etc.)
550
+
551
+ def extract_text_from_pdf(pdf_file):
552
+ if pdf_file is None:
553
+ return None, "Please upload a PDF file."
554
+
555
+ try:
556
+ if isinstance(pdf_file, str):
557
+ with open(pdf_file, 'rb') as f:
558
+ pdf_reader = PyPDF2.PdfReader(f)
559
+ text = ""
560
+ for page in pdf_reader.pages:
561
+ page_text = page.extract_text()
562
+ if page_text:
563
+ text += page_text + "\n"
564
+ else:
565
+ if hasattr(pdf_file, 'read'):
566
+ pdf_bytes = pdf_file.read()
567
+ if hasattr(pdf_file, 'seek'):
568
+ pdf_file.seek(0)
569
+ else:
570
+ pdf_bytes = pdf_file
571
+
572
+ if isinstance(pdf_bytes, bytes):
573
+ pdf_stream = io.BytesIO(pdf_bytes)
574
+ else:
575
+ pdf_stream = io.BytesIO(pdf_bytes.encode() if isinstance(pdf_bytes, str) else pdf_bytes)
576
+
577
+ pdf_reader = PyPDF2.PdfReader(pdf_stream)
578
+ text = ""
579
+ for page in pdf_reader.pages:
580
+ page_text = page.extract_text()
581
+ if page_text:
582
+ text += page_text + "\n"
583
+
584
+ text = re.sub(r'\s+', ' ', text).strip()
585
+
586
+ if len(text) < 50:
587
+ return None, "Could not extract text. PDF may be image-based or scanned."
588
+
589
+ return text, None
590
+
591
+ except Exception as e:
592
+ return None, f"Error reading PDF: {str(e)}"
593
+
594
+ def summarize_with_gemini(text, max_length, min_length):
595
+ if not gemini_client:
596
+ return None
597
+
598
+ try:
599
+ if hasattr(gemini_client, 'models'):
600
+ prompt = f"Summarize the following text in {min_length}-{max_length} words:\n\n{text[:15000]}"
601
+ try:
602
+ response = gemini_client.models.generate_content(
603
+ model="gemini-2.5-flash",
604
+ contents=prompt
605
+ )
606
+ return response.text
607
+ except:
608
+ if hasattr(gemini_client, 'GenerativeModel'):
609
+ model = gemini_client.GenerativeModel('gemini-2.5-flash')
610
+ response = model.generate_content(prompt)
611
+ return response.text
612
+ elif hasattr(gemini_client, 'GenerativeModel'):
613
+ model = gemini_client.GenerativeModel('gemini-2.5-flash')
614
+ prompt = f"Summarize the following text in {min_length}-{max_length} words:\n\n{text[:15000]}"
615
+ response = model.generate_content(prompt)
616
+ return response.text
617
+ except Exception as e:
618
+ print(f"Gemini error: {e}")
619
+
620
+ return None
621
+
622
+ def summarize_pdf(pdf_file, max_length, min_length):
623
+ text, error = extract_text_from_pdf(pdf_file)
624
+ if error:
625
+ return error
626
+
627
+ gemini_result = summarize_with_gemini(text, max_length, min_length)
628
+ if gemini_result:
629
+ return gemini_result
630
+
631
+ summ = load_summarizer()
632
+ if summ:
633
+ try:
634
+ result = summ(text[:3500], max_length=max_length, min_length=min_length, do_sample=False)
635
+ return result[0]['summary_text']
636
+ except Exception as e:
637
+ return f"Error: {str(e)}"
638
+
639
+ return "Error: No summarization available"
640
+
641
+ def generate_essay_with_gemini(prompt, essay_type, word_count, tone):
642
+ if not gemini_client:
643
+ return None
644
+
645
+ try:
646
+ full_prompt = f"""Write a {essay_type} essay in {tone} tone (~{word_count} words).
647
+ Topic: {prompt}
648
+ Requirements: Engaging intro, structured body, strong conclusion."""
649
+
650
+ if hasattr(gemini_client, 'models'):
651
+ response = gemini_client.models.generate_content(
652
+ model="gemini-2.5-flash",
653
+ contents=full_prompt
654
+ )
655
+ essay = response.text.strip()
656
+ else:
657
+ model = gemini_client.GenerativeModel('gemini-2.5-flash')
658
+ response = model.generate_content(full_prompt)
659
+ essay = response.text.strip()
660
+
661
+ word_count_actual = len(essay.split())
662
+ return f"""# {essay_type} Essay: {prompt[:50]}{'...' if len(prompt) > 50 else ''}
663
+
664
+ {essay}
665
+
666
+ ---
667
+ *~{word_count_actual} words | {tone} tone*"""
668
+
669
+ except Exception as e:
670
+ print(f"Essay error: {e}")
671
+ return None
672
+
673
+ def generate_essay(prompt, essay_type, word_count, tone):
674
+ if not prompt or len(prompt.strip()) < 10:
675
+ return "Please provide a detailed prompt (at least 10 characters)."
676
+
677
+ result = generate_essay_with_gemini(prompt, essay_type, word_count, tone)
678
+ if result:
679
+ return result
680
+
681
+ return "❌ Essay generation failed. Please check Gemini API configuration."
682
+
683
+ def summarize_text(text, max_length, min_length):
684
+ if len(text.strip()) < 100:
685
+ return "Please provide at least 100 characters."
686
+
687
+ gemini_result = summarize_with_gemini(text, max_length, min_length)
688
+ if gemini_result:
689
+ return gemini_result
690
+
691
+ summ = load_summarizer()
692
+ if summ:
693
+ try:
694
+ result = summ(text[:3500], max_length=max_length, min_length=min_length, do_sample=False)
695
+ return result[0]['summary_text']
696
+ except Exception as e:
697
+ return f"Error: {str(e)}"
698
+
699
+ return "Error: No summarization available"
700
+
701
+ # Quiz Functions
702
+ def extract_sentences(text):
703
+ sentences = re.split(r'[.!?]', text)
704
+ return [s.strip() for s in sentences if len(s.split()) > 6]
705
+
706
+ def create_quiz(text, num_questions):
707
+ sentences = extract_sentences(text)
708
+ if len(sentences) < num_questions:
709
+ num_questions = len(sentences)
710
+
711
+ selected = random.sample(sentences, num_questions)
712
+ quiz_data = []
713
+
714
+ for sentence in selected:
715
+ words = sentence.split()
716
+ if len(words) < 5:
717
+ continue
718
+ keyword = random.choice(words[2:-2])
719
+ question = sentence.replace(keyword, "_____")
720
+ all_words = list(set(text.split()))
721
+ wrong = random.sample([w for w in all_words if w != keyword], min(3, len(all_words)))
722
+ options = wrong + [keyword]
723
+ random.shuffle(options)
724
+
725
+ quiz_data.append({
726
+ "question": question,
727
+ "options": options,
728
+ "answer": keyword
729
+ })
730
+
731
+ return quiz_data
732
+
733
+ def start_quiz(text, num_questions, timer_minutes):
734
+ if not text.strip():
735
+ return "⚠️ Please enter study material.", gr.update(choices=[], visible=False), "", None, 0, None, "", gr.update(visible=False)
736
+
737
+ quiz = create_quiz(text, num_questions)
738
+ if not quiz:
739
+ return "⚠️ Could not generate quiz. Please provide more text.", gr.update(choices=[], visible=False), "", None, 0, None, "", gr.update(visible=False)
740
+
741
+ end_time = time.time() + (timer_minutes * 60)
742
+ return show_question(quiz, 0, 0, end_time)
743
+
744
+ def show_question(quiz, index, score, end_time):
745
+ if time.time() > end_time:
746
+ return finish_quiz(quiz, score, len(quiz))
747
+
748
+ if index >= len(quiz):
749
+ return finish_quiz(quiz, score, len(quiz))
750
+
751
+ q = quiz[index]
752
+ remaining = int(end_time - time.time())
753
+ timer_text = f"⏳ {remaining // 60}:{remaining % 60:02d}"
754
+
755
+ return (
756
+ f"**Question {index+1} of {len(quiz)}**\n\n{q['question']}",
757
+ gr.update(choices=q["options"], value=None, visible=True),
758
+ f"Score: {score}/{len(quiz)}",
759
+ quiz,
760
+ index,
761
+ score,
762
+ end_time,
763
+ timer_text,
764
+ gr.update(visible=True)
765
+ )
766
+
767
+ def submit_answer(selected, quiz, index, score, end_time):
768
+ if not selected:
769
+ return show_question(quiz, index, score, end_time)
770
+
771
+ if selected == quiz[index]["answer"]:
772
+ score += 1
773
+
774
+ return show_question(quiz, index + 1, score, end_time)
775
+
776
+ def finish_quiz(quiz, score, total):
777
+ percentage = (score / total * 100) if total > 0 else 0
778
+ emoji = "πŸŽ‰" if percentage >= 80 else "πŸ‘" if percentage >= 60 else "πŸ“š"
779
+
780
+ return (
781
+ f"""## {emoji} Quiz Complete!
782
+
783
+ **Final Score: {score}/{total}** ({percentage:.1f}%)
784
+
785
+ {'Excellent work!' if percentage >= 80 else 'Good job!' if percentage >= 60 else 'Keep practicing!'}""",
786
+ gr.update(choices=[], visible=False),
787
+ "",
788
+ None,
789
+ 0,
790
+ score,
791
+ None,
792
+ "⏰ Time's up!" if total > 0 else "",
793
+ gr.update(visible=False)
794
+ )
795
+
796
+ def translate_to_urdu(text):
797
+ if not text or not text.strip():
798
+ return "Please enter some text to translate."
799
+
800
+ if not groq_client:
801
+ return "❌ Groq API not configured."
802
+
803
+ try:
804
+ chat_completion = groq_client.chat.completions.create(
805
+ messages=[
806
+ {
807
+ "role": "system",
808
+ "content": "You are a professional English to Urdu translator. Translate accurately to Urdu (اردو) using natural language. Respond ONLY with the translation."
809
+ },
810
+ {
811
+ "role": "user",
812
+ "content": f"Translate to Urdu:\n\n{text}"
813
+ }
814
+ ],
815
+ model="llama-3.3-70b-versatile",
816
+ temperature=0.3,
817
+ max_completion_tokens=2048,
818
+ )
819
+ return chat_completion.choices[0].message.content
820
+ except Exception as e:
821
+ return f"Error: {str(e)}"
822
+
823
+ # ==================== CUSTOM CSS ====================
824
+ custom_css = """
825
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Nastaliq+Urdu&display=swap');
826
+
827
+ :root {
828
+ --primary: #6366f1;
829
+ --primary-dark: #4f46e5;
830
+ --secondary: #8b5cf6;
831
+ --success: #10b981;
832
+ --warning: #f59e0b;
833
+ --danger: #ef4444;
834
+ --dark: #1f2937;
835
+ --light: #f3f4f6;
836
+ --gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
837
+ }
838
+
839
+ body {
840
+ font-family: 'Inter', sans-serif !important;
841
+ background: #f8fafc !important;
842
+ }
843
+
844
+ .auth-container {
845
+ max-width: 450px;
846
+ margin: 3rem auto;
847
+ padding: 2.5rem;
848
+ background: white;
849
+ border-radius: 24px;
850
+ box-shadow: 0 25px 80px rgba(0,0,0,0.15);
851
+ text-align: center;
852
+ }
853
+
854
+ .auth-logo {
855
+ width: 80px;
856
+ height: 80px;
857
+ background: var(--gradient);
858
+ border-radius: 20px;
859
+ display: flex;
860
+ align-items: center;
861
+ justify-content: center;
862
+ margin: 0 auto 1.5rem;
863
+ font-size: 2.5rem;
864
+ color: white;
865
+ }
866
+
867
+ .auth-title {
868
+ font-size: 1.875rem;
869
+ font-weight: 700;
870
+ color: var(--dark);
871
+ margin-bottom: 0.5rem;
872
+ }
873
+
874
+ .auth-subtitle {
875
+ color: #6b7280;
876
+ margin-bottom: 2rem;
877
+ font-size: 1rem;
878
+ }
879
+
880
+ .auth-toggle {
881
+ display: flex;
882
+ background: #f3f4f6;
883
+ border-radius: 12px;
884
+ padding: 4px;
885
+ margin-bottom: 1.5rem;
886
+ }
887
+
888
+ .auth-toggle-btn {
889
+ flex: 1;
890
+ padding: 0.75rem;
891
+ border: none;
892
+ background: transparent;
893
+ border-radius: 8px;
894
+ font-weight: 500;
895
+ cursor: pointer;
896
+ transition: all 0.3s;
897
+ }
898
+
899
+ .auth-toggle-btn.active {
900
+ background: white;
901
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
902
+ color: var(--primary);
903
+ }
904
+
905
+ .input-group {
906
+ margin-bottom: 1rem;
907
+ text-align: left;
908
+ }
909
+
910
+ .input-label {
911
+ display: block;
912
+ font-size: 0.875rem;
913
+ font-weight: 500;
914
+ color: #374151;
915
+ margin-bottom: 0.5rem;
916
+ }
917
+
918
+ .input-field {
919
+ width: 100%;
920
+ padding: 0.875rem 1rem;
921
+ border: 2px solid #e5e7eb;
922
+ border-radius: 12px;
923
+ font-size: 1rem;
924
+ transition: all 0.3s;
925
+ box-sizing: border-box;
926
+ }
927
+
928
+ .input-field:focus {
929
+ outline: none;
930
+ border-color: var(--primary);
931
+ box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
932
+ }
933
+
934
+ .btn-primary {
935
+ width: 100%;
936
+ padding: 1rem;
937
+ background: var(--gradient) !important;
938
+ color: white !important;
939
+ border: none !important;
940
+ border-radius: 12px !important;
941
+ font-size: 1rem;
942
+ font-weight: 600 !important;
943
+ cursor: pointer;
944
+ transition: all 0.3s !important;
945
+ margin-top: 0.5rem;
946
+ }
947
+
948
+ .btn-primary:hover {
949
+ transform: translateY(-2px);
950
+ box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4) !important;
951
+ }
952
+
953
+ .btn-secondary {
954
+ width: 100%;
955
+ padding: 0.875rem;
956
+ background: white !important;
957
+ color: var(--dark) !important;
958
+ border: 2px solid #e5e7eb !important;
959
+ border-radius: 12px !important;
960
+ font-weight: 500 !important;
961
+ margin-top: 0.75rem;
962
+ }
963
+
964
+ .auth-message {
965
+ padding: 1rem;
966
+ border-radius: 12px;
967
+ margin-top: 1rem;
968
+ font-size: 0.875rem;
969
+ }
970
+
971
+ .auth-message.success {
972
+ background: #d1fae5;
973
+ color: #065f46;
974
+ border: 1px solid #a7f3d0;
975
+ }
976
+
977
+ .auth-message.error {
978
+ background: #fee2e2;
979
+ color: #991b1b;
980
+ border: 1px solid #fecaca;
981
+ }
982
+
983
+ .app-header {
984
+ background: var(--gradient);
985
+ color: white;
986
+ padding: 1.5rem 2rem;
987
+ border-radius: 0 0 30px 30px;
988
+ margin: -20px -20px 2rem -20px;
989
+ box-shadow: 0 10px 40px rgba(102, 126, 234, 0.3);
990
+ position: relative;
991
+ }
992
+
993
+ .app-header h1 {
994
+ font-size: 2rem;
995
+ font-weight: 700;
996
+ margin: 0;
997
+ display: flex;
998
+ align-items: center;
999
+ gap: 0.75rem;
1000
+ }
1001
+
1002
+ .user-widget {
1003
+ position: absolute;
1004
+ top: 1.5rem;
1005
+ right: 2rem;
1006
+ background: rgba(255,255,255,0.2);
1007
+ backdrop-filter: blur(10px);
1008
+ padding: 0.5rem 1rem;
1009
+ border-radius: 50px;
1010
+ display: flex;
1011
+ align-items: center;
1012
+ gap: 0.75rem;
1013
+ font-size: 0.875rem;
1014
+ }
1015
+
1016
+ .status-bar {
1017
+ display: flex;
1018
+ gap: 0.75rem;
1019
+ margin-bottom: 1.5rem;
1020
+ flex-wrap: wrap;
1021
+ }
1022
+
1023
+ .status-pill {
1024
+ padding: 0.5rem 1rem;
1025
+ border-radius: 50px;
1026
+ font-size: 0.8rem;
1027
+ font-weight: 500;
1028
+ display: flex;
1029
+ align-items: center;
1030
+ gap: 0.5rem;
1031
+ }
1032
+
1033
+ .status-ok { background: #d1fae5; color: #065f46; }
1034
+ .status-warn { background: #fef3c7; color: #92400e; }
1035
+ .status-error { background: #fee2e2; color: #991b1b; }
1036
+
1037
+ .tool-card {
1038
+ background: white;
1039
+ border-radius: 20px;
1040
+ padding: 1.5rem;
1041
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
1042
+ border: 1px solid #f3f4f6;
1043
+ transition: all 0.3s;
1044
+ }
1045
+
1046
+ .tool-card:hover {
1047
+ transform: translateY(-4px);
1048
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.08);
1049
+ }
1050
+
1051
+ .tool-icon {
1052
+ width: 56px;
1053
+ height: 56px;
1054
+ border-radius: 16px;
1055
+ display: flex;
1056
+ align-items: center;
1057
+ justify-content: center;
1058
+ font-size: 1.75rem;
1059
+ margin-bottom: 1rem;
1060
+ background: var(--gradient);
1061
+ color: white;
1062
+ }
1063
+
1064
+ .quiz-card {
1065
+ background: var(--gradient);
1066
+ color: white;
1067
+ padding: 2rem;
1068
+ border-radius: 20px;
1069
+ text-align: center;
1070
+ }
1071
+
1072
+ .quiz-timer {
1073
+ font-size: 2rem;
1074
+ font-weight: 700;
1075
+ font-family: monospace;
1076
+ background: rgba(255,255,255,0.2);
1077
+ padding: 1rem;
1078
+ border-radius: 16px;
1079
+ margin-bottom: 1.5rem;
1080
+ }
1081
+
1082
+ .urdu-text {
1083
+ font-family: 'Noto Nastaliq Urdu', serif !important;
1084
+ font-size: 1.5em !important;
1085
+ line-height: 2 !important;
1086
+ direction: rtl !important;
1087
+ text-align: right !important;
1088
+ background: #f8fafc;
1089
+ padding: 1.5rem;
1090
+ border-radius: 16px;
1091
+ border: 2px solid #e5e7eb;
1092
+ }
1093
+
1094
+ .app-footer {
1095
+ text-align: center;
1096
+ padding: 2rem;
1097
+ color: #9ca3af;
1098
+ font-size: 0.875rem;
1099
+ margin-top: 3rem;
1100
+ border-top: 1px solid #f3f4f6;
1101
+ }
1102
+ """
1103
+
1104
+ # ==================== AUTH HANDLERS ====================
1105
+ def handle_login(username, password):
1106
+ session_token, user_data = auth_system.login(username, password)
1107
+ if session_token:
1108
+ return {
1109
+ session_token_state: gr.update(value=session_token),
1110
+ auth_screen: gr.update(visible=False),
1111
+ main_app: gr.update(visible=True),
1112
+ user_display: gr.update(value=f"πŸ‘€ {user_data['name']}"),
1113
+ login_message: gr.update(visible=False),
1114
+ signup_message: gr.update(visible=False)
1115
+ }
1116
+ else:
1117
+ return {
1118
+ session_token_state: gr.update(value=""),
1119
+ auth_screen: gr.update(visible=True),
1120
+ main_app: gr.update(visible=False),
1121
+ user_display: gr.update(value=""),
1122
+ login_message: gr.update(value=f"❌ {user_data}", visible=True),
1123
+ signup_message: gr.update(visible=False)
1124
+ }
1125
+
1126
+ def handle_signup(username, password, name, email):
1127
+ success, message = auth_system.register(username, password, name, email)
1128
+
1129
+ if success:
1130
+ # Auto-login after signup
1131
+ session_token, user_data = auth_system.login(username, password)
1132
+ if session_token:
1133
+ return {
1134
+ session_token_state: gr.update(value=session_token),
1135
+ auth_screen: gr.update(visible=False),
1136
+ main_app: gr.update(visible=True),
1137
+ user_display: gr.update(value=f"πŸ‘€ {user_data['name']}"),
1138
+ signup_message: gr.update(value=f"βœ… {message}", visible=True),
1139
+ login_message: gr.update(visible=False)
1140
+ }
1141
+
1142
+ return {
1143
+ session_token_state: gr.update(value=""),
1144
+ auth_screen: gr.update(visible=True),
1145
+ main_app: gr.update(visible=False),
1146
+ user_display: gr.update(value=""),
1147
+ signup_message: gr.update(value=f"❌ {message}", visible=True),
1148
+ login_message: gr.update(visible=False)
1149
+ }
1150
+
1151
+ def handle_logout(session_token):
1152
+ auth_system.logout(session_token)
1153
+ return {
1154
+ session_token_state: gr.update(value=""),
1155
+ auth_screen: gr.update(visible=True),
1156
+ main_app: gr.update(visible=False),
1157
+ user_display: gr.update(value=""),
1158
+ login_username: gr.update(value=""),
1159
+ login_password: gr.update(value=""),
1160
+ signup_username: gr.update(value=""),
1161
+ signup_password: gr.update(value=""),
1162
+ signup_name: gr.update(value=""),
1163
+ signup_email: gr.update(value=""),
1164
+ login_message: gr.update(visible=False),
1165
+ signup_message: gr.update(visible=False)
1166
+ }
1167
+
1168
+ def toggle_auth_mode(mode):
1169
+ if mode == "login":
1170
+ return {
1171
+ login_form: gr.update(visible=True),
1172
+ signup_form: gr.update(visible=False),
1173
+ login_tab: gr.update(elem_classes="auth-toggle-btn active"),
1174
+ signup_tab: gr.update(elem_classes="auth-toggle-btn")
1175
+ }
1176
+ else:
1177
+ return {
1178
+ login_form: gr.update(visible=False),
1179
+ signup_form: gr.update(visible=True),
1180
+ login_tab: gr.update(elem_classes="auth-toggle-btn"),
1181
+ signup_tab: gr.update(elem_classes="auth-toggle-btn active")
1182
+ }
1183
+
1184
+ # ==================== UI BUILDER ====================
1185
+ def build_auth_screen():
1186
+ with gr.Column(visible=True, elem_classes="auth-container") as auth_screen:
1187
+ gr.Markdown("""
1188
+ <div class="auth-logo">πŸŽ“</div>
1189
+ <h1 class="auth-title">Student AI Suite</h1>
1190
+ <p class="auth-subtitle">Your personal academic assistant</p>
1191
+ """)
1192
+
1193
+ with gr.Row(elem_classes="auth-toggle"):
1194
+ login_tab = gr.Button("Sign In", elem_classes="auth-toggle-btn active")
1195
+ signup_tab = gr.Button("Create Account", elem_classes="auth-toggle-btn")
1196
+
1197
+ with gr.Column(visible=True) as login_form:
1198
+ login_username = gr.Textbox(label="Username", placeholder="Enter your username", elem_classes="input-field")
1199
+ login_password = gr.Textbox(label="Password", type="password", placeholder="Enter your password", elem_classes="input-field")
1200
+ login_btn = gr.Button("Sign In", elem_classes="btn-primary")
1201
+ login_message = gr.Markdown(visible=False, elem_classes="auth-message")
1202
+
1203
+ with gr.Column(visible=False) as signup_form:
1204
+ signup_name = gr.Textbox(label="Full Name", placeholder="Your name", elem_classes="input-field")
1205
+ signup_email = gr.Textbox(label="Email", placeholder="your@email.com", elem_classes="input-field")
1206
+ signup_username = gr.Textbox(label="Username", placeholder="Choose a username", elem_classes="input-field")
1207
+ signup_password = gr.Textbox(label="Password", type="password", placeholder="Min 6 characters", elem_classes="input-field")
1208
+ signup_btn = gr.Button("Create Account", elem_classes="btn-primary")
1209
+ signup_message = gr.Markdown(visible=False, elem_classes="auth-message")
1210
+
1211
+ gr.Markdown("""
1212
+ <div style="margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid #f3f4f6; color: #9ca3af; font-size: 0.875rem;">
1213
+ πŸ”’ Secure β€’ Private β€’ Free Forever
1214
+ </div>
1215
+ """)
1216
+
1217
+ return (auth_screen, login_tab, signup_tab, login_form, signup_form,
1218
+ login_username, login_password, login_btn, login_message,
1219
+ signup_name, signup_email, signup_username, signup_password, signup_btn, signup_message)
1220
+
1221
+ def build_main_app():
1222
+ with gr.Column(visible=False) as main_app:
1223
+ with gr.Row(elem_classes="app-header"):
1224
+ gr.Markdown("""
1225
+ <h1>πŸŽ“ Student AI Suite</h1>
1226
+ <div style="opacity: 0.9; font-size: 1rem; margin-top: 0.25rem;">
1227
+ Essay β€’ PDF β€’ Quiz β€’ Translate
1228
+ </div>
1229
+ """)
1230
+
1231
+ with gr.Row(elem_classes="user-widget"):
1232
+ user_display = gr.Markdown()
1233
+ logout_btn = gr.Button("Logout", size="sm", variant="secondary")
1234
+
1235
+ with gr.Row(elem_classes="status-bar"):
1236
+ gemini_status = "βœ… Gemini" if gemini_client else "❌ Gemini"
1237
+ groq_status = "βœ… Groq" if groq_client else "❌ Groq"
1238
+ gr.Markdown(f"""
1239
+ <span class="status-pill {'status-ok' if gemini_client else 'status-error'}">πŸ€– {gemini_status}</span>
1240
+ <span class="status-pill {'status-ok' if groq_client else 'status-error'}">🌐 {groq_status}</span>
1241
+ """)
1242
+
1243
+ with gr.Tabs():
1244
+ with gr.TabItem("πŸ“ Essay & PDF"):
1245
+ with gr.Tabs():
1246
+ with gr.Tab("✍️ Essay Generator"):
1247
+ with gr.Row():
1248
+ with gr.Column():
1249
+ essay_prompt = gr.Textbox(label="Essay Topic", placeholder="e.g., 'Impact of Artificial Intelligence on Modern Education'", lines=3)
1250
+ with gr.Row():
1251
+ essay_type = gr.Dropdown(["Argumentative", "Expository", "Descriptive", "Persuasive"], value="Argumentative", label="Type")
1252
+ essay_tone = gr.Dropdown(["Academic", "Formal", "Neutral"], value="Academic", label="Tone")
1253
+ essay_words = gr.Slider(200, 1000, 500, step=50, label="Word Count")
1254
+ essay_btn = gr.Button("✨ Generate Essay", variant="primary")
1255
+
1256
+ with gr.Column():
1257
+ essay_output = gr.Markdown(elem_classes="output-box")
1258
+
1259
+ with gr.Tab("πŸ“„ PDF Summarizer"):
1260
+ with gr.Row():
1261
+ with gr.Column():
1262
+ pdf_file = gr.File(label="Upload PDF", file_types=[".pdf"], type="binary")
1263
+ with gr.Row():
1264
+ pdf_max = gr.Slider(50, 500, 200, step=10, label="Max Words")
1265
+ pdf_min = gr.Slider(20, 200, 50, step=10, label="Min Words")
1266
+ pdf_btn = gr.Button("πŸ“ Summarize PDF", variant="primary")
1267
+
1268
+ gr.Markdown("---")
1269
+ pdf_text = gr.Textbox(label="Or paste text", lines=4)
1270
+ text_btn = gr.Button("Summarize Text")
1271
+
1272
+ with gr.Column():
1273
+ pdf_output = gr.Textbox(label="Summary", lines=12, elem_classes="output-box")
1274
+
1275
+ with gr.TabItem("🎯 Smart Quiz"):
1276
+ with gr.Row():
1277
+ with gr.Column(scale=1):
1278
+ quiz_text = gr.Textbox(label="Study Material", placeholder="Paste your notes, textbook content, or any study material here...", lines=8)
1279
+ with gr.Row():
1280
+ quiz_num = gr.Slider(1, 10, 5, step=1, label="Questions")
1281
+ quiz_time = gr.Slider(1, 10, 2, step=1, label="Minutes")
1282
+ quiz_start = gr.Button("πŸš€ Start Quiz", variant="primary")
1283
+
1284
+ with gr.Column(scale=2):
1285
+ with gr.Column(elem_classes="quiz-card"):
1286
+ quiz_timer = gr.Markdown("⏳ 02:00", elem_classes="quiz-timer")
1287
+ quiz_question = gr.Markdown("### Ready to test your knowledge?")
1288
+ quiz_options = gr.Radio(choices=[], label="Select Answer", visible=False)
1289
+ quiz_submit = gr.Button("Submit Answer", visible=False)
1290
+ quiz_score = gr.Markdown()
1291
+
1292
+ quiz_state = gr.State()
1293
+ quiz_idx = gr.State(0)
1294
+ quiz_scr = gr.State(0)
1295
+ quiz_end = gr.State()
1296
+
1297
+ with gr.TabItem("🌍 Urdu Translator"):
1298
+ with gr.Row():
1299
+ with gr.Column():
1300
+ trans_input = gr.Textbox(label="English Text", placeholder="Enter text to translate to Urdu...", lines=6)
1301
+ trans_btn = gr.Button("πŸ”„ Translate", variant="primary")
1302
+ gr.Examples(examples=["Hello, how are you?", "Pakistan is beautiful", "I love learning"], inputs=trans_input)
1303
+
1304
+ with gr.Column():
1305
+ trans_output = gr.Textbox(label="اردو Ψͺرجمہ", lines=6, elem_classes="urdu-text", interactive=False)
1306
+
1307
+ gr.Markdown("""
1308
+ <div class="app-footer">
1309
+ <p>Made with ❀️ for students worldwide</p>
1310
+ </div>
1311
+ """)
1312
+
1313
+ return (main_app, user_display, logout_btn, essay_prompt, essay_type, essay_tone,
1314
+ essay_words, essay_btn, essay_output, pdf_file, pdf_max, pdf_min, pdf_btn,
1315
+ pdf_text, text_btn, pdf_output, quiz_text, quiz_num, quiz_time, quiz_start,
1316
+ quiz_timer, quiz_question, quiz_options, quiz_submit, quiz_score,
1317
+ quiz_state, quiz_idx, quiz_scr, quiz_end, trans_input, trans_btn, trans_output)
1318
+
1319
+ # ==================== MAIN ====================
1320
+ with gr.Blocks(title="Student AI Suite", css=custom_css) as demo:
1321
+ session_token_state = gr.State("")
1322
+
1323
+ (auth_screen, login_tab, signup_tab, login_form, signup_form,
1324
+ login_username, login_password, login_btn, login_message,
1325
+ signup_name, signup_email, signup_username, signup_password, signup_btn, signup_message) = build_auth_screen()
1326
+
1327
+ (main_app, user_display, logout_btn, essay_prompt, essay_type, essay_tone,
1328
+ essay_words, essay_btn, essay_output, pdf_file, pdf_max, pdf_min, pdf_btn,
1329
+ pdf_text, text_btn, pdf_output, quiz_text, quiz_num, quiz_time, quiz_start,
1330
+ quiz_timer, quiz_question, quiz_options, quiz_submit, quiz_score,
1331
+ quiz_state, quiz_idx, quiz_scr, quiz_end, trans_input, trans_btn, trans_output) = build_main_app()
1332
+
1333
+ # Event Handlers
1334
+ login_tab.click(lambda: toggle_auth_mode("login"),
1335
+ outputs=[login_form, signup_form, login_tab, signup_tab])
1336
+ signup_tab.click(lambda: toggle_auth_mode("signup"),
1337
+ outputs=[login_form, signup_form, login_tab, signup_tab])
1338
+
1339
+ login_btn.click(handle_login,
1340
+ inputs=[login_username, login_password],
1341
+ outputs=[session_token_state, auth_screen, main_app, user_display, login_message, signup_message])
1342
+
1343
+ signup_btn.click(handle_signup,
1344
+ inputs=[signup_username, signup_password, signup_name, signup_email],
1345
+ outputs=[session_token_state, auth_screen, main_app, user_display, signup_message, login_message])
1346
+
1347
+ logout_btn.click(handle_logout,
1348
+ inputs=[session_token_state],
1349
+ outputs=[session_token_state, auth_screen, main_app, user_display,
1350
+ login_username, login_password, signup_username, signup_password,
1351
+ signup_name, signup_email, login_message, signup_message])
1352
+
1353
+ # Tools
1354
+ essay_btn.click(generate_essay,
1355
+ inputs=[essay_prompt, essay_type, essay_words, essay_tone],
1356
+ outputs=essay_output)
1357
+
1358
+ pdf_btn.click(summarize_pdf,
1359
+ inputs=[pdf_file, pdf_max, pdf_min],
1360
+ outputs=pdf_output)
1361
+
1362
+ text_btn.click(summarize_text,
1363
+ inputs=[pdf_text, pdf_max, pdf_min],
1364
+ outputs=pdf_output)
1365
+
1366
+ quiz_start.click(start_quiz,
1367
+ inputs=[quiz_text, quiz_num, quiz_time],
1368
+ outputs=[quiz_question, quiz_options, quiz_score, quiz_state,
1369
+ quiz_idx, quiz_scr, quiz_end, quiz_timer, quiz_submit])
1370
+
1371
+ quiz_submit.click(submit_answer,
1372
+ inputs=[quiz_options, quiz_state, quiz_idx, quiz_scr, quiz_end],
1373
+ outputs=[quiz_question, quiz_options, quiz_score, quiz_state,
1374
+ quiz_idx, quiz_scr, quiz_end, quiz_timer, quiz_submit])
1375
+
1376
+ trans_btn.click(translate_to_urdu,
1377
+ inputs=trans_input,
1378
+ outputs=trans_output)
1379
+
1380
+ if __name__ == "__main__":
1381
+ demo.launch(server_name="0.0.0.0", server_port=7860)