Yogya12 commited on
Commit
5c77d42
Β·
0 Parent(s):

Initial commit

Browse files
Files changed (4) hide show
  1. .env.openai +12 -0
  2. main.py +921 -0
  3. requirements.txt +12 -0
  4. static/logo.png +0 -0
.env.openai ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GROQ_API_KEY=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
2
+ PG_HOST=localhost
3
+ PG_PORT=5432
4
+ PG_USER=postgres
5
+ PG_PASS=Viaan1234
6
+ PG_DB=edumate
7
+
8
+ SMTP_EMAIL=aitutor1211@gmail.com
9
+ SMTP_PASSWORD=yjqqpcwqxqbhkxot
10
+ SECRET_KEY=Vr1W_xN28NU4i9SknLzJjRYLQppqj1JX_lkq9eE1HnM-PZk3XLc1fU7TD5fjxQv8FSQgVZ1lLUl4S92nT88XUgA
11
+ USE_HTTPS=true
12
+ CSRF_SECRET=a5e38cf0acb4e1320a87d67136b924a6c3f458eb4398ec4781e099c56c2e6e78
main.py ADDED
@@ -0,0 +1,921 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from fastapi.staticfiles import StaticFiles
3
+
4
+ import asyncio, uuid, os, re, smtplib, random
5
+ from deep_translator import GoogleTranslator
6
+ import edge_tts
7
+ from transformers import AutoTokenizer, AutoModelForCausalLM
8
+ import torch
9
+ from reportlab.pdfgen import canvas
10
+ from datetime import datetime, timedelta
11
+ from email.message import EmailMessage
12
+ import bcrypt
13
+ import html
14
+ from dotenv import load_dotenv
15
+ load_dotenv(".env.openai")
16
+ email_user = os.getenv("SMTP_EMAIL")
17
+ email_pass = os.getenv("SMTP_PASSWORD")
18
+ import psycopg2
19
+ from psycopg2 import sql
20
+ import os
21
+ import uvicorn
22
+ from fastapi import FastAPI, Request, Response
23
+ from fastapi.middleware.wsgi import WSGIMiddleware
24
+ from collections import defaultdict
25
+
26
+ import openai
27
+ import requests
28
+ import hashlib
29
+ otp_send_tracker = defaultdict(list)
30
+ OTP_LIMIT = 3
31
+ OTP_WINDOW = timedelta(minutes=15)
32
+ PG_CONN = {
33
+ "host": os.getenv("PG_HOST"),
34
+ "port": os.getenv("PG_PORT"),
35
+ "user": os.getenv("PG_USER"),
36
+ "password": os.getenv("PG_PASS"),
37
+ "dbname": os.getenv("PG_DB"),
38
+ }
39
+ from collections import defaultdict
40
+ from itsdangerous import URLSafeSerializer
41
+ csrf_signer = URLSafeSerializer(os.getenv("CSRF_SECRET", "defaultcsrfsecret")) # Replace with your own secret
42
+
43
+
44
+ # Tracks login attempts: {email: [timestamps of failed attempts]}
45
+ attempt_tracker = defaultdict(list)
46
+ MAX_ATTEMPTS = 5
47
+ LOCKOUT_WINDOW = timedelta(minutes=10)
48
+ from itsdangerous import TimestampSigner, BadSignature, SignatureExpired
49
+
50
+ signer = TimestampSigner(os.getenv("SECRET_KEY", "defaultsecret"))
51
+ SESSION_COOKIE_NAME = "edumate_session"
52
+ SESSION_EXPIRY_SECONDS = 3600 # 1 hour
53
+ from fastapi import Request
54
+ fastapi_app = FastAPI()
55
+ fastapi_app.mount("/static", StaticFiles(directory="static"), name="static")
56
+
57
+ @fastapi_app.get("/check-login")
58
+ def update_csrf_token(email):
59
+ email = sanitize_input(email)
60
+ if not is_valid_email(email):
61
+ return gr.update(value="")
62
+ return gr.update(value=generate_csrf_token(email))
63
+
64
+ def generate_initial_csrf():
65
+ return generate_csrf_token("guest")
66
+
67
+ def translate_text(text, language, voice_name):
68
+ try:
69
+ # Translate using Google Translator
70
+ if language == "English - US":
71
+ translated = text # no translation needed
72
+ elif language == "Hindi":
73
+ translated = GoogleTranslator(source="auto", target="hi").translate(text)
74
+ else:
75
+ translated = GoogleTranslator(source="auto", target="en").translate(text)
76
+
77
+ # Use Edge TTS
78
+ voice_id = voice_characters[language][voice_name]
79
+ filename = f"tts_{uuid.uuid4().hex}.mp3"
80
+
81
+ # Generate speech
82
+ asyncio.run(edge_tts.Communicate(translated, voice_id).save(filename))
83
+
84
+ return translated, filename
85
+ except Exception as e:
86
+ return f"❌ Error: {str(e)}", None
87
+ def generate_csrf_token(email):
88
+ return csrf_signer.dumps({"email": email})
89
+
90
+
91
+ def toggle_logout_visibility(is_visible):
92
+ return not is_visible, gr.update(visible=not is_visible)
93
+
94
+ def get_chat_filename(user_email):
95
+ user_email = sanitize_input(user_email)
96
+ hashed = hashlib.sha256(user_email.encode()).hexdigest()
97
+ return f"chat_logs/{hashed}.txt"
98
+
99
+
100
+ def check_login(request: Request):
101
+ token = request.cookies.get("edumate_session")
102
+ try:
103
+ email = signer.unsign(token, max_age=3600).decode()
104
+ return {"status": "βœ… Logged in", "email": email}
105
+ except:
106
+ return {"status": "❌ Invalid or expired session"}
107
+
108
+ def generate_session_token(email):
109
+ return signer.sign(email.encode()).decode()
110
+
111
+
112
+ def is_locked_out(email):
113
+ now = datetime.now()
114
+ attempts = attempt_tracker[email]
115
+ # Remove attempts outside the time window
116
+ attempt_tracker[email] = [ts for ts in attempts if now - ts < LOCKOUT_WINDOW]
117
+ return len(attempt_tracker[email]) >= MAX_ATTEMPTS
118
+
119
+
120
+ def get_pg_connection():
121
+ return psycopg2.connect(**PG_CONN)
122
+
123
+ def init_db():
124
+ conn = get_pg_connection()
125
+ cur = conn.cursor()
126
+ cur.execute("""
127
+ CREATE TABLE IF NOT EXISTS users (
128
+ email TEXT PRIMARY KEY,
129
+ hashed_password TEXT NOT NULL,
130
+ username TEXT NOT NULL
131
+ )
132
+ """)
133
+ conn.commit()
134
+ conn.close()
135
+
136
+
137
+ init_db()
138
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
139
+ GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions"
140
+
141
+ GROQ_MODEL = "llama3-8b-8192"
142
+ voice_characters = {
143
+ "English - US": {
144
+ "Aria": "en-US-AriaNeural", "Guy": "en-US-GuyNeural",
145
+ "Christopher": "en-US-ChristopherNeural", "Emma": "en-US-EmmaNeural",
146
+
147
+ },
148
+ "Hindi": {
149
+ "Madhur": "hi-IN-MadhurNeural" }
150
+ }
151
+
152
+ EMAIL_REGEX = re.compile(r"^[\w\.-]+@[\w\.-]+\.\w+$")
153
+ def is_valid_email(email): return bool(EMAIL_REGEX.match(email))
154
+
155
+ def sanitize_input(text):
156
+ if not isinstance(text, str): return ""
157
+ return html.escape(text.strip())
158
+
159
+ USER_FILE = "users.txt"
160
+ def save_user(email, password, username):
161
+ email = sanitize_input(email)
162
+ password = sanitize_input(password)
163
+ username = sanitize_input(username)
164
+ if not (email and password and username): return False
165
+
166
+ hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
167
+ try:
168
+ conn = get_pg_connection()
169
+ cur = conn.cursor()
170
+ cur.execute("INSERT INTO users (email, hashed_password, username) VALUES (%s, %s, %s)",
171
+ (email, hashed, username))
172
+ conn.commit()
173
+ return True
174
+ except psycopg2.IntegrityError:
175
+ return False
176
+ finally:
177
+ conn.close()
178
+
179
+ def get_username(email):
180
+ email = sanitize_input(email)
181
+ conn = get_pg_connection()
182
+ cur = conn.cursor()
183
+ cur.execute("SELECT username FROM users WHERE email = %s", (email,))
184
+ row = cur.fetchone()
185
+ conn.close()
186
+ return row[0] if row else ""
187
+
188
+ def check_user(email, password):
189
+ email = sanitize_input(email)
190
+ password = sanitize_input(password)
191
+ conn = get_pg_connection()
192
+ cur = conn.cursor()
193
+ cur.execute("SELECT hashed_password FROM users WHERE email = %s", (email,))
194
+ row = cur.fetchone()
195
+ conn.close()
196
+
197
+ if row:
198
+ stored_hash = row[0]
199
+ #print("πŸ” Stored hash from DB:", stored_hash)
200
+ #print("πŸ”‘ Password entered:", password)
201
+
202
+ if isinstance(stored_hash, str):
203
+ stored_hash = stored_hash.encode()
204
+
205
+ result = bcrypt.checkpw(password.encode(), stored_hash)
206
+ #print("βœ… Password match:", result)
207
+ return result
208
+
209
+ print("❌ Email not found in DB")
210
+ return False
211
+
212
+
213
+
214
+ otp_store = {}
215
+ def send_otp(email):
216
+ email = sanitize_input(email)
217
+
218
+ # Rate limiting
219
+ now = datetime.now()
220
+ otp_send_tracker[email] = [t for t in otp_send_tracker[email] if now - t < OTP_WINDOW]
221
+ if len(otp_send_tracker[email]) >= OTP_LIMIT:
222
+ print(f"❌ OTP limit reached for {email}")
223
+ return False # limit reached
224
+
225
+ otp = str(random.randint(100000, 999999))
226
+ otp_store[email] = {'otp': otp, 'expires': now + timedelta(minutes=10)}
227
+ otp_send_tracker[email].append(now)
228
+
229
+ msg = EmailMessage()
230
+ msg.set_content(f"Your OTP is: {otp}")
231
+ msg["Subject"] = "AI Tutor Signup OTP"
232
+ msg["From"] = email_user
233
+ msg["To"] = email
234
+
235
+ try:
236
+ with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
237
+ smtp.login(email_user, email_pass)
238
+ smtp.send_message(msg)
239
+ except Exception as e:
240
+ print("SMTP Error:", e)
241
+ return False
242
+
243
+ return True
244
+
245
+
246
+ import json # add at top if not already
247
+
248
+ async def chat_with_ai(message, history, user_email):
249
+ message = message.strip()
250
+ user_email = sanitize_input(user_email)
251
+ if history is None:
252
+ history = []
253
+
254
+ history.append({"role": "user", "content": message})
255
+
256
+ messages = [
257
+ {
258
+ "role": "system",
259
+ "content": (
260
+ "You are EduMate, a helpful educational assistant created by the EduMate team. "
261
+ "You help students with academic questions, concepts, notes, translations, and more. "
262
+ "If someone asks 'Who created you?' or 'What are you?', respond that you were built and trained by EduMate for educational support. "
263
+ "Do not mention Meta, OpenAI, or any external company. Stay consistent with EduMate branding."
264
+ )
265
+ }
266
+ ] + [{"role": m["role"], "content": m["content"]} for m in history]
267
+
268
+ headers = {
269
+ "Authorization": f"Bearer {GROQ_API_KEY}",
270
+ "Content-Type": "application/json"
271
+ }
272
+
273
+ payload = {
274
+ "model": GROQ_MODEL,
275
+ "messages": messages,
276
+ "temperature": 0.7,
277
+ "max_tokens": 1000
278
+ }
279
+
280
+ try:
281
+ res = requests.post(GROQ_API_URL, headers=headers, json=payload)
282
+ res.raise_for_status()
283
+ reply = res.json()["choices"][0]["message"]["content"].strip()
284
+ except requests.exceptions.HTTPError as e:
285
+ reply = f"❌ Groq error: {e.response.text}"
286
+ except Exception as e:
287
+ reply = f"❌ Unexpected error: {str(e)}"
288
+
289
+ history.append({"role": "assistant", "content": ""})
290
+ for i in range(1, len(reply) + 1):
291
+ history[-1]["content"] = reply[:i]
292
+ yield history
293
+ await asyncio.sleep(0.015) # typing delay
294
+
295
+ save_chat(user_email, message, reply)
296
+
297
+
298
+
299
+ async def generate_notes_typed(prompt, user_email):
300
+ prompt = sanitize_input(prompt)
301
+ user_email = sanitize_input(user_email)
302
+
303
+ headers = {
304
+ "Authorization": f"Bearer {GROQ_API_KEY}",
305
+ "Content-Type": "application/json"
306
+ }
307
+
308
+ payload = {
309
+ "model": GROQ_MODEL,
310
+ "messages": [
311
+ {
312
+ "role": "system",
313
+ "content": (
314
+ "You are a helpful academic assistant. Generate well-structured, clear, and concise notes in bullet points only. "
315
+ "Use headings and subheadings. Break complex concepts into small, easy-to-understand bullet points. "
316
+ "Include all relevant subtopics, including key definitions, types, applications, examples, and scientific laws (like Fick’s laws if related). "
317
+ "Do not write full paragraphs. Stick to clean academic study note format."
318
+ )
319
+ },
320
+ {"role": "user", "content": f"Generate notes on the topic: {prompt}"}
321
+ ],
322
+ "temperature": 0.6,
323
+ "max_tokens": 600
324
+ }
325
+
326
+ try:
327
+ res = requests.post(GROQ_API_URL, headers=headers, json=payload)
328
+ res.raise_for_status()
329
+ reply = res.json()["choices"][0]["message"]["content"].strip()
330
+
331
+ save_chat(user_email, f"Generate notes on: {prompt}", reply)
332
+
333
+ output = ""
334
+ for i in range(1, len(reply) + 1):
335
+ output = reply[:i]
336
+ yield output
337
+ await asyncio.sleep(0.01) # delay to simulate typing
338
+ except Exception as e:
339
+ yield f"❌ Error generating notes: {str(e)}"
340
+
341
+
342
+
343
+
344
+
345
+
346
+
347
+ def export_notes_to_pdf(chat_data, topic):
348
+ topic = sanitize_input(topic)
349
+ if isinstance(chat_data, list): # Extract only the AI parts
350
+ text = "\n\n".join(reply for _, reply in chat_data if reply)
351
+ else:
352
+ text = sanitize_input(str(chat_data))
353
+
354
+ fn = f"{topic}_{datetime.now():%Y%m%d_%H%M%S}.pdf"
355
+ c = canvas.Canvas(fn)
356
+ c.setFont("Helvetica", 12)
357
+ y = 800
358
+ for line in text.split("\n"):
359
+ if y < 50:
360
+ c.showPage()
361
+ y = 800
362
+ c.drawString(50, y, line)
363
+ y -= 18
364
+ c.save()
365
+ return fn
366
+
367
+ def load_history(user_email):
368
+ user_email = sanitize_input(user_email)
369
+ path = get_chat_filename(user_email)
370
+ if not os.path.exists(path):
371
+ return []
372
+
373
+ with open(path, encoding="utf-8") as f:
374
+ blocks = f.read().split("\n<<<END>>>\n")
375
+
376
+ history = []
377
+ for blk in blocks:
378
+ if not blk.strip():
379
+ continue
380
+ try:
381
+ user_msg, ai_msg = blk.strip().split("|||", 1)
382
+ history.append({"role": "user", "content": user_msg.strip()})
383
+ history.append({"role": "assistant", "content": ai_msg.strip()})
384
+ except ValueError:
385
+ continue
386
+ return history
387
+
388
+
389
+
390
+ def render_chat_bubbles(user_email):
391
+ history = load_history(user_email) # already a list of dicts
392
+ return gr.update(visible=True, value=history)
393
+
394
+
395
+
396
+
397
+ def delete_all_history(user_email):
398
+ user_email = sanitize_input(user_email)
399
+ path = get_chat_filename(user_email)
400
+ if os.path.exists(path):
401
+ os.remove(path)
402
+ chatbot_update = gr.update(value=[], visible=True)
403
+ chat_bubbles_update = gr.update(value=[], visible=True)
404
+ return chatbot_update, chat_bubbles_update
405
+ def save_chat(user_email, user_msg, ai_msg):
406
+ path = get_chat_filename(user_email)
407
+ os.makedirs(os.path.dirname(path), exist_ok=True)
408
+ with open(path, "a", encoding="utf-8") as f:
409
+ f.write(f"{user_msg} ||| {ai_msg}\n<<<END>>>\n")
410
+
411
+
412
+
413
+
414
+ def logout():
415
+ return gr.update(visible=True), gr.update(visible=False), "", "", gr.update(value=[])
416
+
417
+
418
+
419
+ # Custom CSS (unchanged)
420
+ custom_css = """
421
+ body {
422
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
423
+ background: #f5f7fa;
424
+ color: #1a1a1a;
425
+ }
426
+ .gradio-container {
427
+ max-width: 1200px;
428
+ margin: 0 auto;
429
+ padding: 20px;
430
+ background: #ffffff;
431
+ border-radius: 12px;
432
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
433
+ }
434
+ h3, .gr-markdown h3 {
435
+ color: #2b6cb0;
436
+ font-weight: 600;
437
+ margin-bottom: 20px;
438
+ }
439
+ .gr-button {
440
+ background: #2b6cb0;
441
+ color: white;
442
+ border: none;
443
+ border-radius: 8px;
444
+ padding: 12px 24px;
445
+ font-size: 16px;
446
+ font-weight: 500;
447
+ transition: background 0.3s ease;
448
+ }
449
+ .gr-button:hover {
450
+ background: #1a4971;
451
+ }
452
+ .gr-textbox, .gr-dropdown {
453
+ border: 1px solid #d1d5db;
454
+ border-radius: 8px;
455
+ padding: 12px;
456
+ font-size: 16px;
457
+ }
458
+ .gr-textbox:focus, .gr-dropdown:focus {
459
+ border-color: #2b6cb0;
460
+ box-shadow: 0 0 0 3px rgba(43, 108, 176, 0.2);
461
+ }
462
+ .gr-chatbot {
463
+ border: 1px solid #e2e8f0;
464
+ border-radius: 8px;
465
+ background: #f8fafc;
466
+ padding: 16px;
467
+ max-height: 500px;
468
+ overflow-y: auto;
469
+ }
470
+ .gr-chatbot .message {
471
+ margin-bottom: 16px;
472
+ padding: 12px;
473
+ border-radius: 8px;
474
+ }
475
+ .gr-chatbot .message.user {
476
+ background: #e6f0fa;
477
+ color: #1a1a1a;
478
+ }
479
+ .gr-chatbot .message.bot {
480
+ background: #ffffff;
481
+ border: 1px solid #e2e8f0;
482
+ }
483
+ .gr-radio {
484
+ display: flex;
485
+ gap: 12px;
486
+ padding: 12px;
487
+ background: #f8fafc;
488
+ border-radius: 8px;
489
+ border: 1px solid #e2e8f0;
490
+ }
491
+ .gr-radio input:checked + label {
492
+ background: #2b6cb0;
493
+ color: white;
494
+ border-radius: 6px;
495
+ padding: 8px 16px;
496
+ }
497
+ .gr-audio {
498
+ border-radius: 8px;
499
+ overflow: hidden;
500
+ }
501
+ .gr-file {
502
+ border: 1px solid #e2e8f0;
503
+ border-radius: 8px;
504
+ padding: 12px;
505
+ }
506
+ profile-icon {
507
+ background: none;
508
+ color: #2b6cb0;
509
+ font-size: 24px;
510
+ border: none;
511
+ margin-top: -10px;
512
+ margin-right: 10px;
513
+ float: right;
514
+ }
515
+ logout-btn {
516
+ background: #e53e3e;
517
+ color: white;
518
+ font-size: 14px;
519
+ margin-top: 5px;
520
+ margin-right: 10px;
521
+ float: right;
522
+ }
523
+ footer { display: none !important; }
524
+ """
525
+ with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as app:
526
+ user_email = gr.State("")
527
+ logout_visible = gr.State(False)
528
+ csrf_token_box = gr.Textbox(visible=False)
529
+
530
+
531
+
532
+ with gr.Column(visible=True, elem_classes="login-ui") as login_ui:
533
+ gr.Image("static/logo.png", width=120, show_label=False, container=False)
534
+
535
+
536
+
537
+ gr.Markdown("### EduMate AI- Login / Sign Up", elem_classes="header")
538
+ email = gr.Textbox(label="Email", placeholder="Enter your email")
539
+ email.change(update_csrf_token, [email], csrf_token_box)
540
+
541
+ pwd = gr.Textbox(label="Password", type="password", placeholder="Enter your password")
542
+ username_box = gr.Textbox(label="Create Username (for signup)", placeholder="Choose a username", visible=True)
543
+ otp_box = gr.Textbox(label="OTP (for signup)", placeholder="Enter OTP", visible=True)
544
+
545
+ with gr.Accordion("πŸ“ƒ View Terms & Conditions", open=False):
546
+ with gr.Accordion("πŸ“ƒ View Terms & Conditions", open=False):
547
+ gr.Markdown("""
548
+ ### Terms & Conditions
549
+
550
+ By accessing or using the EduMate AI application, you agree to the following terms:
551
+
552
+ #### 1. Acceptance of Terms
553
+ By using this app, you confirm that you are at least 13 years of age and agree to be bound by these Terms & Conditions. If you do not agree, please do not use the service.
554
+
555
+ #### 2. Purpose of the App
556
+ This application is designed for educational purposes only. It provides AI-powered tools for learning, such as chat assistance, translation, note generation, and voice synthesis.
557
+
558
+ #### 3. User Responsibility
559
+ - You are responsible for all activity under your account.
560
+ - You agree not to use the app for any illegal, harmful, or abusive activities.
561
+ - Do not attempt to reverse-engineer, disrupt, or overload the service.
562
+
563
+ #### 4. Data Privacy
564
+ - We do not share your personal data with third parties.
565
+ - Your email and password are securely stored using encryption.
566
+ - Chat history is stored locally for user convenience and is not publicly accessible.
567
+
568
+ #### 5. Account Security
569
+ - Do not share your login credentials with anyone.
570
+ - We use OTP verification to prevent fake or duplicate accounts.
571
+
572
+ #### 6. Limitation of Liability
573
+ - EduMate AI is provided "as is", without warranty of any kind.
574
+ - We are not responsible for any decisions made based on the app's suggestions or AI-generated content.
575
+
576
+ #### 7. Content Accuracy
577
+ - The AI may generate inaccurate or inappropriate responses occasionally.
578
+ - Users are encouraged to verify important information independently.
579
+
580
+ #### 8. Modification or Termination
581
+ - We reserve the right to update, modify, or terminate the app or these Terms at any time, with or without notice.
582
+
583
+ #### 9. Governing Law
584
+ These terms are governed by the laws of India. Any disputes shall be subject to the jurisdiction of Indian courts.
585
+
586
+ #### 10. Contact Us
587
+ For any questions or concerns about these Terms, please contact the developer at your registered support email.
588
+
589
+ ---
590
+ By continuing to use this application, you agree to abide by these terms.
591
+ """)
592
+
593
+
594
+
595
+
596
+ agree_checkbox = gr.Checkbox(label="I agree to the Terms & Conditions", value=False)
597
+
598
+
599
+
600
+
601
+ with gr.Row():
602
+ btn_login = gr.Button("Login", variant="primary")
603
+ btn_signup = gr.Button("Sign Up", variant="secondary")
604
+ btn_verify = gr.Button("Verify OTP", variant="secondary")
605
+ btn_forgot = gr.Button("Forgot Password?", variant="secondary")
606
+
607
+ status = gr.Textbox(label="Status", interactive=False, placeholder="Status messages will appear here")
608
+
609
+
610
+ with gr.Row(visible=False, elem_classes="main-ui") as main_ui:
611
+ with gr.Column(scale=1, elem_classes="sidebar"):
612
+ gr.Image("static/logo.png", width=120, show_label=False, container=False)
613
+
614
+ nav = gr.Radio(["Chat", "Translate", "Notes", "Chat History"], value="Chat", label="Menu")
615
+ with gr.Column(scale=4):
616
+ with gr.Row():
617
+ gr.Markdown("")
618
+ with gr.Column(scale=0, min_width=80, elem_id="profile-container"):
619
+ profile_btn = gr.Button("πŸ‘€", elem_id="profile-icon", size="sm")
620
+ logout_btn = gr.Button("Logout", visible=False, elem_id="logout-btn", size="sm")
621
+ profile_btn.click(
622
+ fn=toggle_logout_visibility,
623
+ inputs=logout_visible,
624
+ outputs=[logout_visible, logout_btn]
625
+ )
626
+
627
+ #logout_btn.click(fn=logout, inputs=[], outputs=[login_ui, main_ui, user_email, status, chatbot])
628
+
629
+
630
+
631
+ with gr.Column(visible=True, elem_classes="tab-content") as chat_tab:
632
+ chatbot = gr.Chatbot(height=500, value=[], type='messages')
633
+
634
+ inp = gr.Textbox(label="Ask me anything…", placeholder="Type your message here")
635
+ btn_send = gr.Button("Send", variant="primary")
636
+ btn_send.click(chat_with_ai, [inp, chatbot, user_email], chatbot)
637
+ logout_btn.click(fn=logout, inputs=[], outputs=[login_ui, main_ui, user_email, status, chatbot])
638
+ with gr.Column(visible=False, elem_classes="tab-content") as tts_tab:
639
+ txt = gr.Textbox(label="Text to Translate")
640
+ lang = gr.Dropdown(list(voice_characters.keys()), value="English - US", label="Language")
641
+ voice = gr.Dropdown(choices=list(voice_characters["English - US"].keys()), label="Voice")
642
+ lang.change(lambda x: gr.update(choices=list(voice_characters[x].keys()),
643
+ value=list(voice_characters[x].keys())[0]), lang, voice)
644
+ btn_t = gr.Button("Translate & Speak", variant="primary")
645
+ out_txt = gr.Textbox(label="Translated Text")
646
+ out_audio = gr.Audio(label="Audio Output")
647
+ btn_t.click(translate_text, [txt, lang, voice], [out_txt, out_audio])
648
+
649
+ with gr.Column(visible=False, elem_classes="tab-content") as notes_tab:
650
+ topic = gr.Textbox(label="Note Topic", placeholder="Enter the topic for your notes")
651
+ btn_n = gr.Button("Generate Notes", variant="primary")
652
+ note_out = gr.Textbox(lines=12, label="Generated Notes")
653
+ btn_n.click(generate_notes_typed, [topic, user_email], note_out)
654
+
655
+
656
+ btn_pdf = gr.Button("Export Notes as PDF", variant="secondary")
657
+ file_out = gr.File(visible=False, label="Download PDF")
658
+ btn_pdf.click(export_notes_to_pdf, [note_out, topic], file_out)
659
+ file_out.change(lambda _: gr.update(visible=True), outputs=file_out)
660
+
661
+ with gr.Column(visible=False, elem_classes="tab-content") as history_tab:
662
+ gr.Markdown("### πŸ•’ Chat History", elem_classes="header")
663
+ btn_clear = gr.Button("πŸ—‘οΈ Delete All History", variant="secondary")
664
+ chat_bubbles = gr.Chatbot(height=500, visible=False, type='messages')
665
+ with gr.Column(visible=False, elem_classes="forgot-ui") as forgot_ui:
666
+ gr.Markdown("### πŸ”‘ Forgot Password")
667
+ email_input = gr.Textbox(label="Registered Email")
668
+ otp_input = gr.Textbox(label="OTP (sent to email)")
669
+ new_pwd = gr.Textbox(label="New Password", type="password")
670
+ confirm_pwd = gr.Textbox(label="Confirm Password", type="password")
671
+ status_fp = gr.Textbox(label="Status", interactive=False)
672
+
673
+ send_btn = gr.Button("Send OTP")
674
+ verify_btn = gr.Button("Reset Password")
675
+
676
+ def send_otp_fp(em):
677
+ em = sanitize_input(em)
678
+ if not is_valid_email(em):
679
+ return "❌ Invalid email"
680
+ if not get_username(em):
681
+ return "❌ Email not registered"
682
+ if send_otp(em):
683
+ return "πŸ“§ OTP sent to your email"
684
+ return "❌ Failed to send OTP"
685
+
686
+ send_btn.click(send_otp_fp, [email_input], status_fp)
687
+
688
+ def verify_and_reset_fp(em, otp_inp, pwd1, pwd2):
689
+ em = sanitize_input(em)
690
+ otp_inp = sanitize_input(otp_inp)
691
+ pwd1 = sanitize_input(pwd1)
692
+ pwd2 = sanitize_input(pwd2)
693
+
694
+ if pwd1 != pwd2:
695
+ return "❌ Passwords do not match"
696
+ if len(pwd1) < 6:
697
+ return "❌ Password too short"
698
+
699
+ otp_data = otp_store.get(em)
700
+ if otp_data and otp_inp == otp_data["otp"] and datetime.now() < otp_data["expires"]:
701
+ hashed = bcrypt.hashpw(pwd1.encode(), bcrypt.gensalt()).decode()
702
+ try:
703
+ conn = get_pg_connection()
704
+ cur = conn.cursor()
705
+ cur.execute("UPDATE users SET hashed_password = %s WHERE email = %s", (hashed, em))
706
+ conn.commit()
707
+ return "βœ… Password reset successful"
708
+ except Exception as e:
709
+ return f"❌ Error: {e}"
710
+ finally:
711
+ conn.close()
712
+ return "❌ OTP invalid or expired"
713
+
714
+ verify_btn.click(verify_and_reset_fp, [email_input, otp_input, new_pwd, confirm_pwd], status_fp)
715
+
716
+
717
+
718
+
719
+
720
+
721
+ def open_forgot_ui():
722
+ return (
723
+ gr.update(visible=False), # login_ui
724
+ gr.update(visible=False), # main_ui
725
+ gr.update(visible=True) ) # forgot_ui
726
+
727
+
728
+ btn_forgot.click(open_forgot_ui, [], [login_ui, main_ui, forgot_ui])
729
+
730
+ def on_nav_change(tab, em):
731
+ chat_v = (tab == "Chat")
732
+ tts_v = (tab == "Translate")
733
+ notes_v = (tab == "Notes")
734
+ hist_v = (tab == "Chat History")
735
+
736
+ chat_bubbles_update = render_chat_bubbles(em) if hist_v else gr.update(visible=False)
737
+
738
+ return (
739
+ gr.update(visible=chat_v),
740
+ gr.update(visible=tts_v),
741
+ gr.update(visible=notes_v),
742
+ gr.update(visible=hist_v),
743
+ chat_bubbles_update # πŸ‘ˆ assign this directly to chat_bubbles
744
+ )
745
+
746
+
747
+ nav.change(on_nav_change, [nav, user_email],
748
+ [chat_tab, tts_tab, notes_tab, history_tab, chat_bubbles])
749
+
750
+ btn_clear.click(delete_all_history, user_email, [chatbot, chat_bubbles])
751
+ def on_login(em, pw, agree, csrf_token):
752
+ em = sanitize_input(em)
753
+ pw = sanitize_input(pw)
754
+ csrf_token = sanitize_input(csrf_token)
755
+
756
+ if not is_valid_email(em):
757
+ return (gr.update(visible=True), gr.update(visible=False), "", "❌ Invalid email", gr.update(value=[]), gr.update(visible=False))
758
+
759
+ if not agree:
760
+ return (gr.update(visible=True), gr.update(visible=False), "", "❌ Please accept the Terms & Conditions", gr.update(value=[]), gr.update(visible=False))
761
+
762
+ # βœ… CSRF Token Verification
763
+ try:
764
+ token_data = csrf_signer.loads(csrf_token)
765
+ if token_data["email"] != em:
766
+ return (gr.update(visible=True), gr.update(visible=False), "", "❌ CSRF token mismatch", gr.update(value=[]), gr.update(visible=False))
767
+ except Exception as e:
768
+ return (gr.update(visible=True), gr.update(visible=False), "", f"❌ Invalid CSRF token: {str(e)}", gr.update(value=[]), gr.update(visible=False))
769
+
770
+ if is_locked_out(em):
771
+ return (gr.update(visible=True), gr.update(visible=False), "", "πŸ”’ Too many failed attempts. Try again later.", gr.update(value=[]), gr.update(visible=False))
772
+
773
+ if check_user(em, pw):
774
+ attempt_tracker[em] = []
775
+ token = generate_session_token(em)
776
+ new_csrf_token = generate_csrf_token(em)
777
+
778
+ return (
779
+ gr.update(visible=False),
780
+ gr.update(visible=True),
781
+ em,
782
+ f"βœ… Logged in as {em}",
783
+ gr.update(value=[]),
784
+ gr.update(value=token, visible=False),
785
+ gr.update(value=new_csrf_token, visible=False)
786
+ )
787
+
788
+ attempt_tracker[em].append(datetime.now())
789
+ return (gr.update(visible=True), gr.update(visible=False), "", "❌ Wrong credentials", gr.update(value=[]), gr.update(visible=False))
790
+
791
+
792
+
793
+
794
+
795
+ session_token_box = gr.Textbox(visible=False) # Define this once at top
796
+
797
+ btn_login.click(
798
+ on_login,
799
+ [email, pwd, agree_checkbox,csrf_token_box],
800
+ [login_ui, main_ui, user_email, status, chatbot, session_token_box]
801
+ )
802
+
803
+
804
+
805
+
806
+ def on_signup(em, pw, un, agree):
807
+ em = sanitize_input(em)
808
+ pw = sanitize_input(pw)
809
+ un = sanitize_input(un)
810
+ if not is_valid_email(em): return "❌ Invalid email"
811
+ if get_username(em): return "❌ Already registered"
812
+ if not agree:
813
+ return "❌ Please accept the Terms & Conditions"
814
+ if send_otp(em):
815
+ otp_store[em + "_pw"] = pw
816
+ otp_store[em + "_un"] = un
817
+ return "πŸ“§ OTP sent"
818
+
819
+ return "❌ OTP failed"
820
+
821
+ btn_signup.click(on_signup, [email, pwd, username_box, agree_checkbox], status)
822
+
823
+ def on_verify(em, otp_inp):
824
+ em = sanitize_input(em)
825
+ otp_inp = sanitize_input(otp_inp)
826
+ otp_data = otp_store.get(em)
827
+ pw = otp_store.get(em + "_pw", "")
828
+ un = otp_store.get(em + "_un", "")
829
+ if otp_data and otp_inp == otp_data['otp'] and datetime.now() < otp_data['expires'] and pw and un:
830
+ save_user(em, pw, un)
831
+ del otp_store[em], otp_store[em + "_pw"], otp_store[em + "_un"]
832
+ return "βœ… Signup completeβ€”please login"
833
+ return "❌ Wrong OTP"
834
+ btn_verify.click(on_verify, [email, otp_box], status)
835
+
836
+
837
+ # βœ… 1. FastAPI app created first
838
+
839
+
840
+
841
+ fastapi_app.mount("/", WSGIMiddleware(app.launch(prevent_thread_lock=True)))
842
+
843
+
844
+ @fastapi_app.get("/ping")
845
+ def health_check():
846
+ return {"status": "βœ… FastAPI is working"}
847
+
848
+
849
+ # Mount your Gradio app at `/`
850
+ fastapi_app.mount("/", WSGIMiddleware(app))
851
+
852
+ @fastapi_app.post("/login")
853
+ async def fastapi_login(request: Request):
854
+ try:
855
+ data = await request.json()
856
+ email = sanitize_input(data.get("email", ""))
857
+ password = sanitize_input(data.get("password", ""))
858
+ csrf_token = sanitize_input(data.get("csrf_token", ""))
859
+ # βœ… Check CSRF token format
860
+ if not csrf_token or '.' not in csrf_token:
861
+ return JSONResponse(status_code=403, content={"success": False, "message": "❌ Invalid CSRF token format"})
862
+
863
+ try:
864
+ csrf_payload = csrf_signer.loads(csrf_token)
865
+ if csrf_payload.get("email") != email:
866
+ return JSONResponse(status_code=403, content={"success": False, "message": "❌ CSRF email mismatch"})
867
+ except Exception as e:
868
+ return JSONResponse(status_code=403, content={"success": False, "message": f"❌ Invalid CSRF token: {str(e)}"})
869
+
870
+
871
+ # 1. Check if email and password are valid
872
+ if not is_valid_email(email) or not check_user(email, password):
873
+ return JSONResponse(status_code=401, content={"success": False, "message": "❌ Invalid credentials"})
874
+
875
+ # 2. Validate CSRF token
876
+ session_token = request.cookies.get(SESSION_COOKIE_NAME)
877
+ if not session_token:
878
+ return JSONResponse(status_code=403, content={"success": False, "message": "❌ Missing session token"})
879
+
880
+ try:
881
+ # Re-sign the email from session cookie to validate it's still intact
882
+ signed_email = signer.unsign(session_token, max_age=SESSION_EXPIRY_SECONDS).decode()
883
+ if signed_email != email:
884
+ return JSONResponse(status_code=403, content={"success": False, "message": "❌ Email mismatch in session"})
885
+ except Exception as e:
886
+ return JSONResponse(status_code=403, content={"success": False, "message": f"❌ Invalid session token: {str(e)}"})
887
+
888
+ # Optional: Match CSRF token logic if you're storing tokens somewhere
889
+
890
+ # 3. If everything is valid, regenerate session
891
+ new_token = generate_session_token(email)
892
+
893
+ response = JSONResponse(content={"success": True, "message": "βœ… Logged in successfully"})
894
+ response.set_cookie(
895
+ key=SESSION_COOKIE_NAME,
896
+ value=new_token,
897
+ httponly=True,
898
+ secure=os.getenv("USE_HTTPS", "false").lower() == "true",
899
+ samesite="Strict",
900
+ max_age=SESSION_EXPIRY_SECONDS,
901
+ path="/"
902
+ )
903
+ return response
904
+
905
+ except Exception as e:
906
+ return JSONResponse(status_code=500, content={"success": False, "message": f"❌ Server error: {str(e)}"})
907
+
908
+
909
+ @fastapi_app.get("/check-login")
910
+ def check_login(request: Request):
911
+ token = request.cookies.get(SESSION_COOKIE_NAME)
912
+ try:
913
+ email = signer.unsign(token, max_age=SESSION_EXPIRY_SECONDS).decode()
914
+ return {"status": "βœ… Logged in", "email": email}
915
+ except Exception:
916
+ return {"status": "❌ Invalid or expired session"}
917
+
918
+ if __name__ == "__main__":
919
+ uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)
920
+
921
+
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ fastapi
3
+ uvicorn
4
+ edge-tts
5
+ torch
6
+ transformers
7
+ deep-translator
8
+ python-dotenv
9
+ bcrypt
10
+ psycopg2
11
+ reportlab
12
+ itsdangerous
static/logo.png ADDED