Param20h commited on
Commit
c64b502
Β·
unverified Β·
1 Parent(s): ff3b480

Final Commit | Done With the RAG APP

Browse files
.env.example ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MODEL_NAME=
2
+ OLLAMA_BASE_URL=
3
+ CHROMA_DB_PATH=
4
+ UPLOAD_FOLDER=uploads
5
+
6
+ OAUTHLIB_INSECURE_TRANSPORT=
7
+ OAUTHLIB_RELAX_TOKEN_SCOPE=
8
+ TUNNEL_URL=
9
+
10
+ SECRET_KEY=
11
+ GOOGLE_CLIENT_ID=
12
+ GOOGLE_CLIENT_SECRET=
13
+
14
+
15
+ GROQ_API_KEY=
app.py CHANGED
@@ -33,19 +33,18 @@ from flask_login import LoginManager, login_user, logout_user, login_required, c
33
  from flask_dance.contrib.google import make_google_blueprint, google
34
  from flask_dance.consumer import oauth_authorized
35
  from dotenv import load_dotenv
36
- from models import db, User
37
  from rag.chunker import load_and_chunk
38
  from rag.embeddings import store_embeddings
39
  from rag.retriever import retrieve_chunks
40
  from rag.generator import generate_answer
41
- from config import SECRET_KEY, SQLALCHEMY_DATABASE_URI, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
42
 
43
  # ── Init ─────────────────────────────────────────────
44
  load_dotenv()
45
  app = Flask(__name__)
46
 
47
  app.config["SECRET_KEY"] = SECRET_KEY
48
- app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
49
  app.config["UPLOAD_FOLDER"] = "uploads"
50
 
51
  META_PATH = os.path.join("vectorstore", "metadata.pkl")
@@ -62,7 +61,6 @@ google_bp = make_google_blueprint(
62
  app.register_blueprint(google_bp, url_prefix="/login")
63
 
64
  # ── Database & Login Manager ──────────────────────────
65
- db.init_app(app)
66
  login_manager = LoginManager()
67
  login_manager.init_app(app)
68
  login_manager.login_view = "login"
@@ -71,10 +69,7 @@ chat_history = {}
71
 
72
  @login_manager.user_loader
73
  def load_user(user_id):
74
- return db.session.get(User, int(user_id))
75
-
76
- with app.app_context():
77
- db.create_all()
78
 
79
  # ── Google OAuth Signal Handler ───────────────────────
80
  @oauth_authorized.connect_via(google_bp)
@@ -90,22 +85,30 @@ def google_logged_in(blueprint, token):
90
  google_info = resp.json()
91
  email = google_info.get("email")
92
  name = google_info.get("name", "")
 
93
  username = name.replace(" ", "_").lower() if name else email.split("@")[0]
94
 
95
  if not email:
96
  return False
97
 
98
  with app.app_context():
99
- user = User.query.filter_by(email=email).first()
100
 
101
  if not user:
102
- if User.query.filter_by(username=username).first():
103
  username = username + "_g"
104
 
105
- user = User(username=username, email=email)
 
 
 
 
106
  user.set_password(os.urandom(24).hex())
107
- db.session.add(user)
108
- db.session.commit()
 
 
 
109
 
110
  login_user(user)
111
 
@@ -114,6 +117,52 @@ def google_logged_in(blueprint, token):
114
 
115
  return False
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  # ── Helper Functions ──────────────────────────────────
118
  def allowed_file(filename):
119
  return "." in filename and filename.rsplit(".", 1)[1].lower() in {"pdf", "docx", "txt"}
@@ -143,16 +192,15 @@ def register():
143
  email = data.get("email")
144
  password = data.get("password")
145
 
146
- if User.query.filter_by(username=username).first():
147
  return render_template("register.html", error="Username already exists!")
148
 
149
- if User.query.filter_by(email=email).first():
150
  return render_template("register.html", error="Email already exists!")
151
 
152
  user = User(username=username, email=email)
153
  user.set_password(password)
154
- db.session.add(user)
155
- db.session.commit()
156
  return redirect(url_for("login"))
157
 
158
  return render_template("register.html")
@@ -164,7 +212,7 @@ def login():
164
  username = data.get("username")
165
  password = data.get("password")
166
 
167
- user = User.query.filter_by(username=username).first()
168
 
169
  if not user or not user.check_password(password):
170
  return render_template("login.html", error="Invalid username or password!")
@@ -187,6 +235,68 @@ def logout():
187
  def chat():
188
  return render_template("chat.html", username=current_user.username)
189
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  @app.route("/files", methods=["GET"])
191
  @login_required
192
  def get_files():
@@ -201,6 +311,9 @@ def get_files():
201
  @login_required
202
  def upload():
203
  try:
 
 
 
204
  if "pdf" not in request.files:
205
  return jsonify({"error": "No file found"}), 400
206
 
@@ -229,6 +342,9 @@ def upload():
229
  @login_required
230
  def ask():
231
  try:
 
 
 
232
  data = request.get_json()
233
  question = data.get("question", "").strip()
234
  filename = data.get("filename", "").strip()
@@ -238,7 +354,7 @@ def ask():
238
 
239
  meta_path = get_user_meta_path(current_user.username)
240
  context_chunks = retrieve_chunks(question, filename, meta_path)
241
- answer = generate_answer(question, context_chunks)
242
 
243
  username = current_user.username
244
  if username not in chat_history:
 
33
  from flask_dance.contrib.google import make_google_blueprint, google
34
  from flask_dance.consumer import oauth_authorized
35
  from dotenv import load_dotenv
36
+ from models import User
37
  from rag.chunker import load_and_chunk
38
  from rag.embeddings import store_embeddings
39
  from rag.retriever import retrieve_chunks
40
  from rag.generator import generate_answer
41
+ from config import SECRET_KEY, MONGO_URI, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
42
 
43
  # ── Init ─────────────────────────────────────────────
44
  load_dotenv()
45
  app = Flask(__name__)
46
 
47
  app.config["SECRET_KEY"] = SECRET_KEY
 
48
  app.config["UPLOAD_FOLDER"] = "uploads"
49
 
50
  META_PATH = os.path.join("vectorstore", "metadata.pkl")
 
61
  app.register_blueprint(google_bp, url_prefix="/login")
62
 
63
  # ── Database & Login Manager ──────────────────────────
 
64
  login_manager = LoginManager()
65
  login_manager.init_app(app)
66
  login_manager.login_view = "login"
 
69
 
70
  @login_manager.user_loader
71
  def load_user(user_id):
72
+ return User.get(user_id)
 
 
 
73
 
74
  # ── Google OAuth Signal Handler ───────────────────────
75
  @oauth_authorized.connect_via(google_bp)
 
85
  google_info = resp.json()
86
  email = google_info.get("email")
87
  name = google_info.get("name", "")
88
+ picture = google_info.get("picture","")
89
  username = name.replace(" ", "_").lower() if name else email.split("@")[0]
90
 
91
  if not email:
92
  return False
93
 
94
  with app.app_context():
95
+ user = User.find_by_email(email)
96
 
97
  if not user:
98
+ if User.find_by_username(username):
99
  username = username + "_g"
100
 
101
+ user = User(
102
+ username=username,
103
+ email=email,
104
+ profile_pic=picture
105
+ )
106
  user.set_password(os.urandom(24).hex())
107
+ user.save()
108
+ else:
109
+ if picture and user.profile_pic != picture:
110
+ user.profile_pic = picture
111
+ user.save()
112
 
113
  login_user(user)
114
 
 
117
 
118
  return False
119
 
120
+ @app.route("/upload_profile_pic", methods=["POST"])
121
+ @login_required
122
+ def upload_profile_pic():
123
+ try:
124
+ if "profile_pic" not in request.files:
125
+ return jsonify({"error": "No file found"}), 400
126
+
127
+ file = request.files["profile_pic"]
128
+
129
+ if file.filename == "":
130
+ return jsonify({"error": "No file selected"}), 400
131
+
132
+ # ── Check file type ──
133
+ allowed = {"png", "jpg", "jpeg", "gif", "webp"}
134
+ ext = file.filename.rsplit(".", 1)[1].lower()
135
+ if ext not in allowed:
136
+ return jsonify({"error": "Only image files allowed"}), 400
137
+
138
+ # ── Save profile pic ──
139
+ pic_folder = os.path.join("static", "profile_pics")
140
+ os.makedirs(pic_folder, exist_ok=True)
141
+
142
+ filename = f"{current_user.username}.{ext}"
143
+ filepath = os.path.join(pic_folder, filename)
144
+ file.save(filepath)
145
+
146
+ current_user.profile_pic = f"/static/profile_pics/{filename}"
147
+ current_user.save()
148
+
149
+ return jsonify({
150
+ "message": "Profile picture updated!",
151
+ "profile_pic": current_user.profile_pic
152
+ }), 200
153
+
154
+ except Exception as e:
155
+ return jsonify({"error": str(e)}), 500
156
+
157
+
158
+ @app.route("/get_profile", methods=["GET"])
159
+ @login_required
160
+ def get_profile():
161
+ return jsonify({
162
+ "username": current_user.username,
163
+ "profile_pic": current_user.profile_pic or ""
164
+ }), 200
165
+
166
  # ── Helper Functions ──────────────────────────────────
167
  def allowed_file(filename):
168
  return "." in filename and filename.rsplit(".", 1)[1].lower() in {"pdf", "docx", "txt"}
 
192
  email = data.get("email")
193
  password = data.get("password")
194
 
195
+ if User.find_by_username(username):
196
  return render_template("register.html", error="Username already exists!")
197
 
198
+ if User.find_by_email(email):
199
  return render_template("register.html", error="Email already exists!")
200
 
201
  user = User(username=username, email=email)
202
  user.set_password(password)
203
+ user.save()
 
204
  return redirect(url_for("login"))
205
 
206
  return render_template("register.html")
 
212
  username = data.get("username")
213
  password = data.get("password")
214
 
215
+ user = User.find_by_username(username)
216
 
217
  if not user or not user.check_password(password):
218
  return render_template("login.html", error="Invalid username or password!")
 
235
  def chat():
236
  return render_template("chat.html", username=current_user.username)
237
 
238
+ @app.route("/admin", methods=["GET"])
239
+ @login_required
240
+ def admin_dashboard():
241
+ if not current_user.is_admin:
242
+ return "Unauthorized", 403
243
+ users = User.get_all()
244
+ user_files = {}
245
+ for user in users:
246
+ folder = get_user_upload_folder(user.username)
247
+ if os.path.exists(folder):
248
+ user_files[user.username] = [f for f in os.listdir(folder) if f.endswith((".pdf", ".docx", ".txt"))]
249
+ else:
250
+ user_files[user.username] = []
251
+
252
+ return render_template("admin.html", users=users, user_files=user_files)
253
+
254
+ @app.route("/download/<username>/<filename>")
255
+ @login_required
256
+ def download_file(username, filename):
257
+ # Only the owner or an admin can download
258
+ if current_user.username != username and not current_user.is_admin:
259
+ return "Unauthorized", 403
260
+
261
+ folder = get_user_upload_folder(username)
262
+ filepath = os.path.join(folder, filename)
263
+ if not os.path.exists(filepath):
264
+ return "File not found", 404
265
+
266
+ from flask import send_file
267
+ return send_file(filepath, as_attachment=True)
268
+
269
+ @app.route("/profile", methods=["GET"])
270
+ @login_required
271
+ def profile():
272
+ return render_template("profile.html", current_user=current_user)
273
+
274
+ @app.route("/update_settings", methods=["POST"])
275
+ @login_required
276
+ def update_settings():
277
+ try:
278
+ data = request.get_json()
279
+ current_user.preferred_model = data.get("preferred_model", "groq")
280
+
281
+ # Determine if we are deleting keys or setting new ones
282
+ groq_req = data.get("groq_key", "").strip()
283
+ gemini_req = data.get("gemini_key", "").strip()
284
+
285
+ if groq_req == "DELETE":
286
+ current_user.set_groq_key(None)
287
+ elif groq_req:
288
+ current_user.set_groq_key(groq_req)
289
+
290
+ if gemini_req == "DELETE":
291
+ current_user.set_gemini_key(None)
292
+ elif gemini_req:
293
+ current_user.set_gemini_key(gemini_req)
294
+
295
+ current_user.save()
296
+ return jsonify({"message": "Settings updated successfully!"}), 200
297
+ except Exception as e:
298
+ return jsonify({"error": str(e)}), 500
299
+
300
  @app.route("/files", methods=["GET"])
301
  @login_required
302
  def get_files():
 
311
  @login_required
312
  def upload():
313
  try:
314
+ if not current_user.get_groq_key() and not current_user.get_gemini_key():
315
+ return jsonify({"error": "⚠️ Please add your Groq or Gemini API key in the Profile page to upload and chat."}), 400
316
+
317
  if "pdf" not in request.files:
318
  return jsonify({"error": "No file found"}), 400
319
 
 
342
  @login_required
343
  def ask():
344
  try:
345
+ if not current_user.get_groq_key() and not current_user.get_gemini_key():
346
+ return jsonify({"error": "⚠️ Please add your Groq or Gemini API key in the Profile page to upload and chat."}), 400
347
+
348
  data = request.get_json()
349
  question = data.get("question", "").strip()
350
  filename = data.get("filename", "").strip()
 
354
 
355
  meta_path = get_user_meta_path(current_user.username)
356
  context_chunks = retrieve_chunks(question, filename, meta_path)
357
+ answer = generate_answer(question, context_chunks, current_user)
358
 
359
  username = current_user.username
360
  if username not in chat_history:
config.py CHANGED
@@ -1,23 +1,27 @@
1
- import os
2
  from dotenv import load_dotenv
3
 
4
  load_dotenv()
5
 
6
- OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
7
- MODEL_NAME = os.getenv("MODEL_NAME", "llama3:latest")
 
 
8
 
9
- EMBEDDING_MODEL = "all-MiniLM-L6-V2"
10
-
11
- SECRET_KEY = "your_secret_key_here"
12
- SQLALCHEMY_DATABASE_URI = "sqlite:///users.db"
13
 
 
 
 
 
14
  CHUNK_SIZE = 500
15
  CHUNK_OVERLAP = 50
16
 
17
- UPLOAD_FOLDER = "uploads"
18
- CHROMA_DB_PATH = "vectorstore"
19
-
20
- TOP_K = 10
21
 
 
22
  GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
23
- GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
 
1
+ import os
2
  from dotenv import load_dotenv
3
 
4
  load_dotenv()
5
 
6
+ # ── App Config ───────────────────────────────────────
7
+ SECRET_KEY = os.getenv("SECRET_KEY", "your_secret_key_here")
8
+ ENCRYPTION_KEY = os.getenv("ENCRYPTION_KEY", b"T4tQj_3jK7z_gBqxZ1j_aGj8sFpXv_f4jZ8Rj9sPqG0=")
9
+ MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:27017/rag_app")
10
 
11
+ # ── Upload Config ────────────────────────────────────
12
+ UPLOAD_FOLDER = "uploads"
13
+ ALLOWED_EXTENSIONS = {"pdf", "docx", "txt"}
 
14
 
15
+ # ── Embedding Config ─────────────────────────────────
16
+ EMBEDDING_MODEL = "all-MiniLM-L6-v2"
17
+ CHROMA_DB_PATH = "vectorstore"
18
+ TOP_K = 50
19
  CHUNK_SIZE = 500
20
  CHUNK_OVERLAP = 50
21
 
22
+ # ── Groq Config ──────────────────────────────────────
23
+ GROQ_MODEL = "llama-3.3-70b-versatile"
 
 
24
 
25
+ # ── Google OAuth Config ──────────────────────────────
26
  GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
27
+ GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
instance/users.db ADDED
Binary file (16.4 kB). View file
 
make_admin.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ load_dotenv()
4
+
5
+ from models import User
6
+
7
+ username = input("Enter the username to make admin: ")
8
+ user = User.find_by_username(username)
9
+
10
+ if user:
11
+ user.is_admin = True
12
+ user.save()
13
+ print(f"Success! '{username}' is now an admin. You can access the Admin Dashboard at /admin via the profile links.")
14
+ else:
15
+ print(f"User '{username}' not found. Please make sure you have registered/logged in first!")
models.py CHANGED
@@ -1,20 +1,115 @@
1
- from flask_sqlalchemy import SQLAlchemy
2
  from flask_login import UserMixin
3
  from werkzeug.security import generate_password_hash, check_password_hash
 
 
 
 
4
 
5
- db = SQLAlchemy()
 
 
 
 
6
 
7
- class User(UserMixin, db.Model):
8
- id = db.Column(db.Integer, primary_key=True)
9
- username = db.Column(db.String(80), unique=True, nullable=False)
10
- email = db.Column(db.String(120), unique=True, nullable=False)
11
- password = db.Column(db.String(200), nullable=False)
12
- # ── Google OAuth ──
13
- google_id = db.Column(db.String(200), nullable=True)
14
- profile_pic = db.Column(db.String(200), nullable=True)
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  def set_password(self, password):
17
  self.password = generate_password_hash(password)
18
 
19
  def check_password(self, password):
20
- return check_password_hash(self.password, password)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from flask_login import UserMixin
2
  from werkzeug.security import generate_password_hash, check_password_hash
3
+ from cryptography.fernet import Fernet
4
+ import pymongo
5
+ from bson.objectid import ObjectId
6
+ from config import ENCRYPTION_KEY, MONGO_URI
7
 
8
+ # Connect to MongoDB
9
+ mongo_client = pymongo.MongoClient(MONGO_URI)
10
+ db = mongo_client.get_default_database()
11
+ if not db.name:
12
+ db = mongo_client["rag_app"]
13
 
14
+ users_collection = db["users"]
15
+ cipher_suite = Fernet(ENCRYPTION_KEY)
 
 
 
 
 
 
16
 
17
+ class User(UserMixin):
18
+ def __init__(self, username, email, password=None, _id=None, google_id=None, profile_pic=None, groq_api_key=None, gemini_api_key=None, preferred_model="groq", is_admin=False):
19
+ self.username = username
20
+ self.email = email
21
+ self.password = password
22
+ self.google_id = google_id
23
+ self.profile_pic = profile_pic
24
+ self.groq_api_key = groq_api_key
25
+ self.gemini_api_key = gemini_api_key
26
+ self.preferred_model = preferred_model
27
+ self.is_admin = is_admin
28
+ if _id:
29
+ self.id = str(_id)
30
+ else:
31
+ self.id = None
32
+
33
+ def get_id(self):
34
+ return self.id or self.username # fallback to username if id is not yet set
35
+
36
+ def save(self):
37
+ user_data = {
38
+ "username": self.username,
39
+ "email": self.email,
40
+ "password": self.password,
41
+ "google_id": self.google_id,
42
+ "profile_pic": self.profile_pic,
43
+ "groq_api_key": self.groq_api_key,
44
+ "gemini_api_key": self.gemini_api_key,
45
+ "preferred_model": self.preferred_model,
46
+ "is_admin": self.is_admin
47
+ }
48
+
49
+ if self.id:
50
+ users_collection.update_one({"_id": ObjectId(self.id)}, {"$set": user_data})
51
+ else:
52
+ result = users_collection.insert_one(user_data)
53
+ self.id = str(result.inserted_id)
54
+
55
  def set_password(self, password):
56
  self.password = generate_password_hash(password)
57
 
58
  def check_password(self, password):
59
+ return check_password_hash(self.password, password)
60
+
61
+ def set_groq_key(self, api_key):
62
+ if api_key:
63
+ self.groq_api_key = cipher_suite.encrypt(api_key.encode('utf-8')).decode('utf-8')
64
+ else:
65
+ self.groq_api_key = None
66
+
67
+ def get_groq_key(self):
68
+ if self.groq_api_key:
69
+ try:
70
+ return cipher_suite.decrypt(self.groq_api_key.encode('utf-8')).decode('utf-8')
71
+ except Exception:
72
+ return None
73
+ return None
74
+
75
+ def set_gemini_key(self, api_key):
76
+ if api_key:
77
+ self.gemini_api_key = cipher_suite.encrypt(api_key.encode('utf-8')).decode('utf-8')
78
+ else:
79
+ self.gemini_api_key = None
80
+
81
+ def get_gemini_key(self):
82
+ if self.gemini_api_key:
83
+ try:
84
+ return cipher_suite.decrypt(self.gemini_api_key.encode('utf-8')).decode('utf-8')
85
+ except Exception:
86
+ return None
87
+ return None
88
+
89
+ @classmethod
90
+ def get(cls, user_id):
91
+ try:
92
+ data = users_collection.find_one({"_id": ObjectId(user_id)})
93
+ if data:
94
+ return cls(**data)
95
+ return None
96
+ except Exception:
97
+ return None
98
+
99
+ @classmethod
100
+ def find_by_username(cls, username):
101
+ data = users_collection.find_one({"username": username})
102
+ if data:
103
+ return cls(**data)
104
+ return None
105
+
106
+ @classmethod
107
+ def find_by_email(cls, email):
108
+ data = users_collection.find_one({"email": email})
109
+ if data:
110
+ return cls(**data)
111
+ return None
112
+
113
+ @classmethod
114
+ def get_all(cls):
115
+ return [cls(**data) for data in users_collection.find()]
rag/generator.py CHANGED
@@ -1,51 +1,72 @@
1
- import ollama
2
- from config import MODEL_NAME
 
3
 
4
- def build_prompt(question, chunks):
5
- context = ""
6
 
7
- for i, chunk in enumerate(chunks):
8
- context += f"\nchunk {i+1} (From: {chunk['filename']} Page: {chunk['page']}):\n"
9
- context += chunk["text"]
10
- context += "\n"
11
 
 
 
 
 
12
 
13
- prompt = f"""
14
- You are a helpful assistant.
15
- Use the following context to answer the question.
16
- If you don't know the answer, say "I don't know".
17
- Do not make up answers.
 
 
 
 
18
 
19
- Context:
20
- {context}
21
 
22
- Question:
23
- {question}
24
 
25
- Answer:
26
- """
27
 
28
- return prompt
29
-
 
 
 
30
 
31
- def call_llm(prompt):
32
- response = ollama.chat(
33
- model=MODEL_NAME,
34
- messages=[
35
- {
36
- "role": "user",
37
- "content": prompt
38
- }
39
- ]
40
- )
41
 
42
- return response["message"]["content"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
- def generate_answer(question, chunks):
45
- if not chunks:
46
- return "no context found in the document."
47
-
48
- prompt = build_prompt(question, chunks)
49
- answer = call_llm(prompt)
50
-
51
- return answer
 
1
+ import os
2
+ import google.generativeai as genai
3
+ from dotenv import load_dotenv
4
 
5
+ load_dotenv() # ← Load .env file first
 
6
 
7
+ from groq import Groq
8
+ from config import GROQ_MODEL
 
 
9
 
10
+ def get_groq_client(user_key):
11
+ if not user_key:
12
+ raise ValueError("No Groq API Key available. Please add it to your profile.")
13
+ return Groq(api_key=user_key)
14
 
15
+ def generate_answer(question, context_chunks, user=None):
16
+ try:
17
+ if not context_chunks:
18
+ context = "No specific document context found for this query."
19
+ else:
20
+ context = "\n\n".join([
21
+ f"πŸ“„ File: {chunk['filename']} | Page: {chunk['page']}\n{chunk['text']}"
22
+ for chunk in context_chunks
23
+ ])
24
 
25
+ prompt = f"""You are a helpful AI assistant. Answer the user's question based on the provided document context.
 
26
 
27
+ Document Context:
28
+ {context}
29
 
30
+ User Question: {question}
 
31
 
32
+ Instructions:
33
+ - If the question is a greeting or general chat (like "hi" or "how are you"), just reply naturally and explain you are here to help with their PDF documents.
34
+ - If the question is about the document, use the provided context to answer.
35
+ - If the context doesn't contain the answer, just say you couldn't find it in the document.
36
+ - Try to be clear, helpful, and concise.
37
 
38
+ Answer:"""
 
 
 
 
 
 
 
 
 
39
 
40
+ pref_model = user.preferred_model if user else "groq"
41
+
42
+ if pref_model == "gemini":
43
+ key = user.get_gemini_key() if user else None
44
+ if not key:
45
+ return "❌ No Gemini API key available. Please add it in your Profile settings."
46
+
47
+ genai.configure(api_key=key)
48
+ model = genai.GenerativeModel("gemini-1.5-flash") # Fast & Free Gemini Model
49
+ response = model.generate_content(prompt)
50
+ return response.text
51
+ else:
52
+ key = user.get_groq_key() if user else None
53
+ client = get_groq_client(key)
54
+ response = client.chat.completions.create(
55
+ model=GROQ_MODEL,
56
+ messages=[
57
+ {
58
+ "role": "system",
59
+ "content": "You are a helpful document assistant."
60
+ },
61
+ {
62
+ "role": "user",
63
+ "content": prompt
64
+ }
65
+ ],
66
+ temperature=0.3,
67
+ max_tokens=1024,
68
+ )
69
+ return response.choices[0].message.content
70
 
71
+ except Exception as e:
72
+ return f"❌ Error generating answer: {str(e)}"
 
 
 
 
 
 
rag/retriever.py CHANGED
@@ -26,13 +26,13 @@ def retrieve_chunks(query, filename=None, meta_path=None):
26
  with open(meta_path, "rb") as f:
27
  metadata = pickle.load(f)
28
 
29
- # ── Fix: use min to avoid out of range ──
30
- n_results = min(TOP_K, len(metadata))
31
 
32
- if n_results == 0:
33
  return []
34
 
35
- distances, indices = index.search(query_embedding, n_results)
36
 
37
  # ── Fix: check distances is not empty ──
38
  if len(distances) == 0 or len(distances[0]) == 0:
@@ -63,4 +63,18 @@ def retrieve_chunks(query, filename=None, meta_path=None):
63
  if len(chunks) == TOP_K:
64
  break
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  return chunks
 
26
  with open(meta_path, "rb") as f:
27
  metadata = pickle.load(f)
28
 
29
+ # ── Fix: search a larger pool to allow filtering by filename ──
30
+ n_search = min(100, len(metadata))
31
 
32
+ if n_search == 0:
33
  return []
34
 
35
+ distances, indices = index.search(query_embedding, n_search)
36
 
37
  # ── Fix: check distances is not empty ──
38
  if len(distances) == 0 or len(distances[0]) == 0:
 
63
  if len(chunks) == TOP_K:
64
  break
65
 
66
+ # fallback: if no specific good match, and the user asks a very generic question
67
+ # and we have chunks for this file, just return the first chunk of the file
68
+ if not chunks and filename:
69
+ for idx in range(len(metadata)):
70
+ if metadata[idx]["filename"] == filename:
71
+ chunks.append({
72
+ "text": metadata[idx]["text"],
73
+ "filename": metadata[idx]["filename"],
74
+ "page": metadata[idx]["page"],
75
+ "score": 0.0,
76
+ "confidence": 0.0
77
+ })
78
+ break
79
+
80
  return chunks
requirements.txt CHANGED
@@ -3,9 +3,11 @@ python-dotenv
3
  pymupdf
4
  faiss-cpu
5
  sentence-transformers
6
- ollama
7
  flask-login
8
- flask-sqlalchemy
9
  werkzeug
 
10
  python-docx
11
- flask-dance
 
 
 
3
  pymupdf
4
  faiss-cpu
5
  sentence-transformers
 
6
  flask-login
7
+ pymongo
8
  werkzeug
9
+ flask-dance
10
  python-docx
11
+ groq
12
+ requests
13
+ requests-oauthlib
static/default_avatar.png ADDED

Git LFS Details

  • SHA256: 0e1327eb4a69d88499c3a815c98f4ad1072b3ef2f6a20db23c81d7cd9f774d94
  • Pointer size: 131 Bytes
  • Size of remote file: 397 kB
static/script.js CHANGED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Check for saved user preference, if any, on load of the website
2
+ document.addEventListener("DOMContentLoaded", () => {
3
+ const currentTheme = localStorage.getItem("theme");
4
+ if (currentTheme === "light") {
5
+ document.body.classList.add("light-mode");
6
+ }
7
+ });
8
+
9
+ // Expose toggle function to global scope
10
+ function toggleLightMode() {
11
+ document.body.classList.toggle("light-mode");
12
+ let theme = "dark";
13
+ if (document.body.classList.contains("light-mode")) {
14
+ theme = "light";
15
+ }
16
+ localStorage.setItem("theme", theme);
17
+ }
static/style.css CHANGED
@@ -1,402 +1,564 @@
1
- /* ── Global ─────────────────────────────────────────── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  * {
3
  margin: 0;
4
  padding: 0;
5
  box-sizing: border-box;
6
- font-family: Arial, sans-serif;
7
  }
8
 
9
  body {
10
- background-color: #f5f5f5;
11
- color: #333;
 
 
 
12
  }
13
 
 
14
  header {
15
  display: flex;
16
  justify-content: space-between;
17
  align-items: center;
18
- padding: 20px;
19
- background-color: #2c3e50;
20
- color: white;
 
 
 
 
 
21
  }
22
 
23
- .header-right {
 
 
 
 
 
 
 
24
  display: flex;
25
  align-items: center;
26
- gap: 15px;
27
- font-size: 14px;
28
  }
29
 
30
- .logout-btn {
31
- background-color: #e74c3c;
32
- padding: 6px 14px;
33
- font-size: 13px;
 
 
34
  }
35
 
36
- .logout-btn:hover {
37
- background-color: #c0392b;
38
  }
39
 
40
- header h1 {
41
- font-size: 28px;
42
- margin-bottom: 5px;
 
43
  }
44
 
45
- header p {
46
- font-size: 14px;
47
- color: #ccc;
 
 
 
 
 
48
  }
49
 
50
- header a {
51
- color: #ccc;
52
- text-decoration: none;
53
- font-size: 14px;
54
- }
55
 
56
- header a:hover {
57
- color: white;
 
 
58
  }
59
 
60
- /* ── Container ──────────────────────────────────────── */
61
- .container {
62
- max-width: 800px;
63
- margin: 30px auto;
64
- padding: 20px;
 
 
 
 
 
 
 
 
 
 
65
  }
66
 
67
- /* ── Upload Box ─────────────────────────────────────── */
68
- .upload-box {
69
- background-color: white;
70
- padding: 30px;
71
- border-radius: 10px;
72
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
73
- text-align: center;
74
  }
75
 
76
- .upload-box h2 {
 
 
 
77
  margin-bottom: 20px;
78
- color: #2c3e50;
 
 
79
  }
80
 
81
- .upload-box input[type="file"] {
 
82
  display: block;
83
- margin: 0 auto 15px auto;
84
- padding: 10px;
85
- }
86
-
87
- /* ── Buttons ────────────────────────────────────────── */
88
- button {
89
- background-color: #2c3e50;
90
- color: white;
91
- border: none;
92
- padding: 10px 20px;
93
- border-radius: 5px;
94
  cursor: pointer;
95
- font-size: 14px;
96
- margin: 5px;
97
  }
98
 
99
- button:hover {
100
- background-color: #1a252f;
 
101
  }
102
 
103
- /* ── Nav Button ─────────────────────────────────────── */
104
- .nav-btn {
105
- text-align: center;
106
- margin-top: 20px;
 
 
 
 
 
 
 
 
 
107
  }
108
 
109
- .nav-btn a {
110
- text-decoration: none;
 
 
 
111
  }
112
 
113
- /* ── Upload Status ──────────────────────────────────── */
114
- #uploadStatus {
115
- margin-top: 15px;
 
 
 
 
 
116
  font-size: 14px;
117
- font-weight: bold;
 
 
118
  }
119
 
120
- /* ── Chat Box ───────────────────────────────────────── */
121
- .chat-box {
122
- background-color: white;
123
- border-radius: 10px;
124
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
125
- padding: 20px;
126
- height: 450px;
127
- overflow-y: auto;
128
- margin-bottom: 15px;
129
  }
130
 
131
- /* ── Messages ───────────────────────────────────────── */
132
- .message {
133
- padding: 10px 15px;
134
  border-radius: 8px;
135
- margin-bottom: 10px;
136
- line-height: 1.5;
137
  }
138
 
139
- .message.user {
140
- background-color: #d6eaf8;
141
- text-align: right;
 
142
  }
143
 
144
- .message.bot {
145
- background-color: #eafaf1;
146
- text-align: left;
147
  }
148
 
149
- .message.error {
150
- background-color: #fadbd8;
151
- text-align: left;
 
 
 
 
152
  }
153
 
154
- /* ── Sources ────────────────────────────────────────── */
155
- .sources {
156
- background-color: #fef9e7;
157
- border-left: 4px solid #f39c12;
158
- padding: 10px 15px;
159
- border-radius: 5px;
160
- margin-bottom: 10px;
161
- font-size: 13px;
162
  }
163
 
164
- .sources ul {
165
- margin-top: 5px;
166
- padding-left: 20px;
 
 
 
 
 
167
  }
168
 
169
- .sources ul li {
170
- margin-top: 3px;
 
 
171
  }
172
- /* ...existing code... */
173
 
174
- .confidence {
175
- font-size: 12px;
176
- font-weight: bold;
177
- margin-left: 10px;
 
 
178
  }
179
 
180
- .sources li {
181
- display: flex;
182
- align-items: center;
183
- justify-content: space-between;
184
- padding: 5px 0;
185
- border-bottom: 1px solid #f0e0b0;
186
  }
187
 
188
- .sources li:last-child {
189
- border-bottom: none;
190
- }
191
- .input-area {
192
  display: flex;
193
- gap: 10px;
194
- }
195
-
196
- .input-area input {
197
- flex: 1;
198
- padding: 10px 15px;
199
- border: 1px solid #ccc;
200
- border-radius: 5px;
201
- font-size: 14px;
202
- outline: none;
203
  }
204
 
205
- .input-area input:focus {
206
- border-color: #2c3e50;
 
207
  }
208
 
209
- #loader {
 
210
  text-align: center;
211
- font-size: 14px;
212
- color: #888;
213
- margin-top: 10px;
214
  }
215
 
216
- /* ...existing code... */
217
-
218
- /* ── Files Box ──────────────────────────────────────── */
219
- .files-box {
220
- background-color: white;
221
- padding: 20px;
222
- border-radius: 10px;
223
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
224
- margin-top: 20px;
225
  }
226
 
227
- .files-box h2 {
228
- margin-bottom: 15px;
229
- color: #2c3e50;
 
 
 
230
  }
231
 
 
232
  .files-box ul {
233
  list-style: none;
234
- padding: 0;
235
  }
236
 
237
-
238
  .file-item {
239
  display: flex;
240
  justify-content: space-between;
241
  align-items: center;
242
- padding: 10px 15px;
243
- border-bottom: 1px solid #eee;
244
- font-size: 14px;
245
- color: #333;
 
 
 
 
 
 
 
 
246
  }
247
 
248
  .file-item:last-child {
249
- border-bottom: none;
250
  }
251
 
252
  .file-item:hover {
253
- background-color: #f5f5f5;
254
- border-radius: 5px;
255
  }
256
 
257
- .delete-btn {
258
- background-color: #e74c3c;
259
- color: white;
260
- border: none;
261
- padding: 5px 10px;
262
- border-radius: 5px;
263
- cursor: pointer;
264
- font-size: 12px;
265
  }
266
 
267
- .delete-btn:hover {
268
- background-color: #c0392b;
 
 
 
 
 
 
 
269
  }
270
 
 
 
 
 
271
 
272
- /* ── Clear Box ───────────────────────────────────────── */
273
- .clear-box {
274
- background-color: white;
275
- padding: 20px;
276
  border-radius: 10px;
277
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
278
- margin-top: 20px;
279
- text-align: center;
280
  }
281
 
282
- .clear-box h2 {
283
- margin-bottom: 10px;
284
- color: #2c3e50;
285
  }
286
 
287
- .clear-box p {
288
- font-size: 13px;
289
- color: #888;
290
- margin-bottom: 15px;
291
  }
292
 
293
- /* ── Clear Vector Button ─────────────────────────────── */
294
- .clear-vector-btn {
295
- background-color: #e67e22;
296
- color: white;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  border: none;
298
- padding: 10px 20px;
299
- border-radius: 5px;
300
- cursor: pointer;
 
 
301
  font-size: 14px;
 
 
 
 
 
302
  }
303
 
304
- .clear-vector-btn:hover {
305
- background-color: #d35400;
 
 
 
 
 
 
 
 
 
 
306
  }
307
 
308
- #clearStatus {
 
309
  margin-top: 10px;
310
- font-size: 14px;
311
- font-weight: bold;
 
 
 
 
312
  }
313
 
314
- /* ...existing code... */
 
 
 
 
315
 
316
- /* ── Auth Box ────────────────────────────────────────── */
317
- .auth-box {
318
- background-color: white;
319
- padding: 30px;
320
- border-radius: 10px;
321
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
322
- max-width: 400px;
323
- margin: 40px auto;
324
- text-align: center;
325
  }
326
 
327
- .auth-box h2 {
328
- margin-bottom: 20px;
329
- color: #2c3e50;
330
  }
331
 
332
- .auth-box input {
333
- width: 100%;
334
- padding: 10px 15px;
335
- margin-bottom: 15px;
336
- border: 1px solid #ccc;
337
- border-radius: 5px;
338
- font-size: 14px;
339
- outline: none;
340
- box-sizing: border-box;
341
  }
342
 
343
- .auth-box input:focus {
344
- border-color: #2c3e50;
 
 
 
 
 
 
 
 
 
 
345
  }
346
 
347
- .auth-box button {
348
- width: 100%;
349
- padding: 10px;
350
- font-size: 15px;
 
351
  }
352
 
353
  .auth-box p {
354
- margin-top: 15px;
355
- font-size: 13px;
356
- color: #888;
357
  }
358
 
359
  .auth-box a {
360
- color: #2c3e50;
361
- font-weight: bold;
362
  text-decoration: none;
 
363
  }
364
 
365
  .auth-box a:hover {
 
366
  text-decoration: underline;
367
  }
368
 
369
  .error-msg {
370
- background-color: #fadbd8;
371
- color: #e74c3c;
372
- padding: 10px;
373
- border-radius: 5px;
374
- margin-bottom: 15px;
375
- font-size: 13px;
376
- }
377
-
378
- .google-btn {
379
- background-color: #4285f4;
380
- color: white;
381
- width: 100%;
382
- padding: 10px;
383
- font-size: 15px;
384
- border: none;
385
- border-radius: 5px;
386
- cursor: pointer;
387
- margin-top: 5px;
388
- }
389
-
390
- .google-btn:hover {
391
- background-color: #357abd;
392
  }
393
 
394
- /* ── Divider ─────────────────────────────────────────── */
395
  .divider {
396
  display: flex;
397
  align-items: center;
398
- margin: 15px 0;
399
- color: #888;
400
  font-size: 13px;
401
  }
402
 
@@ -404,6 +566,49 @@ button:hover {
404
  .divider::after {
405
  content: "";
406
  flex: 1;
407
- border-bottom: 1px solid #ccc;
408
- margin: 0 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  }
 
1
+ /* ── Google Fonts ───────────────────────────────────── */
2
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap');
3
+
4
+ /* ── Global Variables ───────────────────────────────── */
5
+ :root {
6
+ --bg-gradient: #09090b;
7
+ --glass-bg: rgba(255, 255, 255, 0.02);
8
+ --glass-border: rgba(255, 255, 255, 0.08);
9
+ --glass-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
10
+
11
+ --primary-color: #fafafa;
12
+ --primary-hover: #e4e4e7;
13
+ --primary-text: #09090b;
14
+ --accent-color: #3b82f6;
15
+ --danger-color: #ef4444;
16
+ --danger-hover: #dc2626;
17
+ --success-color: #10b981;
18
+
19
+ --text-main: #fafafa;
20
+ --text-muted: #a1a1aa;
21
+
22
+ --transition-speed: 0.25s;
23
+
24
+ --header-bg: rgba(9, 9, 11, 0.85);
25
+ --input-bg: rgba(255, 255, 255, 0.03);
26
+ --item-hover-bg: rgba(255, 255, 255, 0.05);
27
+ }
28
+
29
+ body.light-mode {
30
+ --bg-gradient: #ffffff;
31
+ --glass-bg: #ffffff;
32
+ --glass-border: rgba(0, 0, 0, 0.1);
33
+ --glass-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
34
+
35
+ --primary-color: #18181b;
36
+ --primary-hover: #27272a;
37
+ --primary-text: #fafafa;
38
+
39
+ --text-main: #18181b;
40
+ --text-muted: #71717a;
41
+
42
+ --header-bg: rgba(255, 255, 255, 0.85);
43
+ --input-bg: #fdfdfd;
44
+ --item-hover-bg: #f4f4f5;
45
+ }
46
+
47
+ /* ── Global Styles ──────────────────────────────────── */
48
  * {
49
  margin: 0;
50
  padding: 0;
51
  box-sizing: border-box;
52
+ font-family: 'Outfit', sans-serif;
53
  }
54
 
55
  body {
56
+ background: var(--bg-gradient);
57
+ color: var(--text-main);
58
+ min-height: 100vh;
59
+ display: flex;
60
+ flex-direction: column;
61
  }
62
 
63
+ /* ── Header ─────────────────────────────────────────── */
64
  header {
65
  display: flex;
66
  justify-content: space-between;
67
  align-items: center;
68
+ padding: 15px 40px;
69
+ background: var(--header-bg);
70
+ backdrop-filter: blur(12px);
71
+ border-bottom: 1px solid var(--glass-border);
72
+ box-shadow: var(--glass-shadow);
73
+ position: sticky;
74
+ top: 0;
75
+ z-index: 100;
76
  }
77
 
78
+ header h1 {
79
+ font-size: 24px;
80
+ font-weight: 600;
81
+ margin: 0;
82
+ background: linear-gradient(to right, #8b5cf6, #0ea5e9);
83
+ -webkit-background-clip: text;
84
+ background-clip: text;
85
+ -webkit-text-fill-color: transparent;
86
  display: flex;
87
  align-items: center;
88
+ gap: 8px;
 
89
  }
90
 
91
+ header a {
92
+ color: var(--text-muted);
93
+ text-decoration: none;
94
+ font-size: 15px;
95
+ font-weight: 500;
96
+ transition: color var(--transition-speed) ease;
97
  }
98
 
99
+ header a:hover {
100
+ color: var(--text-main);
101
  }
102
 
103
+ .header-right {
104
+ display: flex;
105
+ align-items: center;
106
+ gap: 16px;
107
  }
108
 
109
+ /* ── Container ──────────────────────────────────────── */
110
+ .container {
111
+ max-width: 850px;
112
+ width: 100%;
113
+ margin: 40px auto;
114
+ padding: 0 20px;
115
+ flex-grow: 1;
116
+ animation: fadeIn 0.6s ease-out;
117
  }
118
 
119
+ @keyframes fadeIn {
120
+ from {
121
+ opacity: 0;
122
+ transform: translateY(10px);
123
+ }
124
 
125
+ to {
126
+ opacity: 1;
127
+ transform: translateY(0);
128
+ }
129
  }
130
 
131
+ /* ── Glassmorphism Cards ────────────────────────────── */
132
+ .upload-box,
133
+ .files-box,
134
+ .clear-box,
135
+ .auth-box,
136
+ .chat-box {
137
+ background: var(--glass-bg);
138
+ backdrop-filter: blur(16px);
139
+ -webkit-backdrop-filter: blur(16px);
140
+ border: 1px solid var(--glass-border);
141
+ border-radius: 16px;
142
+ padding: 30px;
143
+ box-shadow: var(--glass-shadow);
144
+ margin-bottom: 25px;
145
+ transition: transform var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
146
  }
147
 
148
+ .upload-box:hover,
149
+ .files-box:hover,
150
+ .clear-box:hover {
151
+ transform: translateY(-2px);
152
+ box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.45);
 
 
153
  }
154
 
155
+ .upload-box h2,
156
+ .files-box h2,
157
+ .clear-box h2,
158
+ .auth-box h2 {
159
  margin-bottom: 20px;
160
+ color: var(--text-main);
161
+ font-weight: 600;
162
+ font-size: 20px;
163
  }
164
 
165
+ /* ── Form Inputs & Uploads ──────────────────────────── */
166
+ input[type="file"] {
167
  display: block;
168
+ width: 100%;
169
+ margin: 0 auto 20px auto;
170
+ padding: 12px;
171
+ background: var(--input-bg);
172
+ border: 1px dashed var(--glass-border);
173
+ border-radius: 8px;
174
+ color: var(--text-muted);
 
 
 
 
175
  cursor: pointer;
176
+ transition: all var(--transition-speed);
 
177
  }
178
 
179
+ input[type="file"]:hover {
180
+ border-color: var(--primary-color);
181
+ background: var(--item-hover-bg);
182
  }
183
 
184
+ input[type="text"],
185
+ input[type="password"],
186
+ input[type="email"] {
187
+ width: 100%;
188
+ padding: 14px 16px;
189
+ margin-bottom: 16px;
190
+ background: var(--input-bg);
191
+ border: 1px solid var(--glass-border);
192
+ border-radius: 10px;
193
+ color: var(--text-main);
194
+ font-size: 15px;
195
+ outline: none;
196
+ transition: all var(--transition-speed) ease;
197
  }
198
 
199
+ input[type="text"]:focus,
200
+ input[type="password"]:focus,
201
+ input[type="email"]:focus {
202
+ border-color: var(--primary-color);
203
+ box-shadow: 0 0 0 2px var(--glass-border);
204
  }
205
 
206
+ /* ── Buttons ────────────────────────────────────────── */
207
+ button {
208
+ background: var(--primary-color);
209
+ color: var(--primary-text);
210
+ border: 1px solid var(--glass-border);
211
+ padding: 12px 24px;
212
+ border-radius: 8px;
213
+ cursor: pointer;
214
  font-size: 14px;
215
+ font-weight: 500;
216
+ transition: all var(--transition-speed) ease;
217
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
218
  }
219
 
220
+ button:hover {
221
+ transform: translateY(-1px);
222
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
223
+ background: var(--primary-hover);
 
 
 
 
 
224
  }
225
 
226
+ #askBtn {
 
 
227
  border-radius: 8px;
228
+ padding: 12px 20px;
 
229
  }
230
 
231
+ #clearBtn {
232
+ background: rgba(255, 255, 255, 0.1);
233
+ box-shadow: none;
234
+ color: var(--text-muted);
235
  }
236
 
237
+ #clearBtn:hover {
238
+ background: rgba(255, 255, 255, 0.15);
239
+ color: var(--text-main);
240
  }
241
 
242
+ .logout-btn {
243
+ background: var(--danger-color);
244
+ color: white;
245
+ border-color: transparent;
246
+ box-shadow: none;
247
+ padding: 8px 16px;
248
+ font-size: 14px;
249
  }
250
 
251
+ .logout-btn:hover {
252
+ background: var(--danger-hover);
253
+ box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
 
 
 
 
 
254
  }
255
 
256
+ .delete-btn {
257
+ background: transparent;
258
+ color: var(--danger-color);
259
+ border: 1px solid rgba(239, 68, 68, 0.3);
260
+ padding: 6px 12px;
261
+ border-radius: 6px;
262
+ font-size: 13px;
263
+ box-shadow: none;
264
  }
265
 
266
+ .delete-btn:hover {
267
+ background: var(--danger-color);
268
+ color: white;
269
+ transform: none;
270
  }
 
271
 
272
+ .clear-vector-btn {
273
+ background: rgba(239, 68, 68, 0.1);
274
+ color: var(--danger-color);
275
+ border: 1px solid rgba(239, 68, 68, 0.3);
276
+ box-shadow: none;
277
+ margin-top: 10px;
278
  }
279
 
280
+ .clear-vector-btn:hover {
281
+ background: var(--danger-color);
282
+ color: white;
 
 
 
283
  }
284
 
285
+ .google-btn {
286
+ background: white;
287
+ color: #333;
288
+ width: 100%;
289
  display: flex;
290
+ justify-content: center;
291
+ align-items: center;
292
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
 
 
 
 
 
 
 
293
  }
294
 
295
+ .google-btn:hover {
296
+ background: #f1f5f9;
297
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
298
  }
299
 
300
+ /* ── Nav Button ─────────────────────────────────────── */
301
+ .nav-btn {
302
  text-align: center;
303
+ margin-top: 30px;
 
 
304
  }
305
 
306
+ .nav-btn button {
307
+ padding: 14px 32px;
308
+ font-size: 16px;
309
+ border-radius: 30px;
 
 
 
 
 
310
  }
311
 
312
+ /* ── Status Texts ───────────────────────────────────── */
313
+ #uploadStatus,
314
+ #clearStatus {
315
+ margin-top: 15px;
316
+ font-size: 14px;
317
+ font-weight: 500;
318
  }
319
 
320
+ /* ── Files List ─────────────────────────────────────── */
321
  .files-box ul {
322
  list-style: none;
 
323
  }
324
 
 
325
  .file-item {
326
  display: flex;
327
  justify-content: space-between;
328
  align-items: center;
329
+ padding: 14px 18px;
330
+ background: var(--input-bg);
331
+ border: 1px solid var(--glass-border);
332
+ margin-bottom: 10px;
333
+ border-radius: 10px;
334
+ font-size: 15px;
335
+ transition: all var(--transition-speed) ease;
336
+ }
337
+
338
+ .file-item.active-file {
339
+ background: rgba(139, 92, 246, 0.25);
340
+ border-color: var(--primary-color);
341
  }
342
 
343
  .file-item:last-child {
344
+ margin-bottom: 0;
345
  }
346
 
347
  .file-item:hover {
348
+ background: var(--item-hover-bg);
349
+ border-color: rgba(139, 92, 246, 0.3);
350
  }
351
 
352
+ .file-item span {
353
+ display: flex;
354
+ align-items: center;
355
+ gap: 10px;
 
 
 
 
356
  }
357
 
358
+ /* ── Chat Box ───────────────────────────────────────── */
359
+ .chat-box {
360
+ height: 500px;
361
+ overflow-y: auto;
362
+ display: flex;
363
+ flex-direction: column;
364
+ padding: 20px 25px;
365
+ gap: 16px;
366
+ margin-bottom: 20px;
367
  }
368
 
369
+ /* Custom Scrollbar for Chat */
370
+ .chat-box::-webkit-scrollbar {
371
+ width: 6px;
372
+ }
373
 
374
+ .chat-box::-webkit-scrollbar-track {
375
+ background: rgba(0, 0, 0, 0.1);
 
 
376
  border-radius: 10px;
 
 
 
377
  }
378
 
379
+ .chat-box::-webkit-scrollbar-thumb {
380
+ background: var(--text-muted);
381
+ border-radius: 10px;
382
  }
383
 
384
+ .chat-box::-webkit-scrollbar-thumb:hover {
385
+ background: var(--primary-color);
 
 
386
  }
387
 
388
+ /* ── Messages ───────────────────────────────────────── */
389
+ .message {
390
+ padding: 14px 18px;
391
+ border-radius: 14px;
392
+ line-height: 1.6;
393
+ max-width: 85%;
394
+ font-size: 15px;
395
+ animation: messagePop 0.3s ease-out;
396
+ }
397
+
398
+ @keyframes messagePop {
399
+ from {
400
+ opacity: 0;
401
+ transform: scale(0.95) translateY(10px);
402
+ }
403
+
404
+ to {
405
+ opacity: 1;
406
+ transform: scale(1) translateY(0);
407
+ }
408
+ }
409
+
410
+ .message.user {
411
+ background: var(--primary-color);
412
+ color: var(--primary-text);
413
+ border: 1px solid var(--glass-border);
414
+ align-self: flex-end;
415
+ border-bottom-right-radius: 4px;
416
+ }
417
+
418
+ .message.bot {
419
+ background: var(--input-bg);
420
+ border: 1px solid var(--glass-border);
421
+ align-self: flex-start;
422
+ border-bottom-left-radius: 4px;
423
+ }
424
+
425
+ .message.error {
426
+ background: rgba(239, 68, 68, 0.1);
427
+ border: 1px solid rgba(239, 68, 68, 0.3);
428
+ color: var(--danger-color);
429
+ align-self: flex-start;
430
+ }
431
+
432
+ /* ── Input Area ─────────────────────────────────────── */
433
+ .input-area {
434
+ display: flex;
435
+ gap: 12px;
436
+ background: var(--glass-bg);
437
+ padding: 15px;
438
+ border-radius: 16px;
439
+ border: 1px solid var(--glass-border);
440
+ backdrop-filter: blur(16px);
441
+ }
442
+
443
+ .input-area input {
444
+ flex: 1;
445
+ margin-bottom: 0;
446
  border: none;
447
+ background: var(--input-bg);
448
+ }
449
+
450
+ #loader {
451
+ text-align: center;
452
  font-size: 14px;
453
+ color: var(--primary-color);
454
+ margin-top: -10px;
455
+ margin-bottom: 10px;
456
+ font-weight: 500;
457
+ animation: pulse 1.5s infinite;
458
  }
459
 
460
+ @keyframes pulse {
461
+ 0% {
462
+ opacity: 0.6;
463
+ }
464
+
465
+ 50% {
466
+ opacity: 1;
467
+ }
468
+
469
+ 100% {
470
+ opacity: 0.6;
471
+ }
472
  }
473
 
474
+ /* ── Sources & Badge ────────────────────────────────── */
475
+ .sources {
476
  margin-top: 10px;
477
+ background: var(--input-bg);
478
+ border-left: 3px solid var(--accent-color);
479
+ padding: 12px 16px;
480
+ border-radius: 6px;
481
+ font-size: 13px;
482
+ color: var(--text-muted);
483
  }
484
 
485
+ .sources ul {
486
+ margin-top: 8px;
487
+ padding-left: 0;
488
+ list-style: none;
489
+ }
490
 
491
+ .sources li {
492
+ display: flex;
493
+ justify-content: space-between;
494
+ padding: 6px 0;
495
+ border-bottom: 1px solid var(--glass-border);
 
 
 
 
496
  }
497
 
498
+ .sources li:last-child {
499
+ border-bottom: none;
500
+ padding-bottom: 0;
501
  }
502
 
503
+ .confidence {
504
+ color: var(--success-color);
505
+ font-weight: 600;
 
 
 
 
 
 
506
  }
507
 
508
+ .file-badge {
509
+ text-align: center;
510
+ padding: 10px 15px;
511
+ border-radius: 20px;
512
+ display: inline-block;
513
+ margin: 0 auto 20px;
514
+ font-size: 14px;
515
+ font-weight: 500;
516
+ background: rgba(16, 185, 129, 0.2);
517
+ border: 1px solid rgba(16, 185, 129, 0.3);
518
+ color: var(--success-color);
519
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
520
  }
521
 
522
+ /* ── Auth Box ───────────────────────────────────────── */
523
+ .auth-box {
524
+ max-width: 420px;
525
+ margin: 60px auto;
526
+ text-align: center;
527
  }
528
 
529
  .auth-box p {
530
+ margin-top: 20px;
531
+ font-size: 14px;
532
+ color: var(--text-muted);
533
  }
534
 
535
  .auth-box a {
536
+ color: var(--primary-color);
537
+ font-weight: 600;
538
  text-decoration: none;
539
+ transition: color var(--transition-speed);
540
  }
541
 
542
  .auth-box a:hover {
543
+ color: var(--primary-hover);
544
  text-decoration: underline;
545
  }
546
 
547
  .error-msg {
548
+ background: rgba(239, 68, 68, 0.1);
549
+ color: var(--danger-color);
550
+ padding: 12px;
551
+ border-radius: 8px;
552
+ margin-bottom: 20px;
553
+ font-size: 14px;
554
+ border: 1px solid rgba(239, 68, 68, 0.3);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
  }
556
 
 
557
  .divider {
558
  display: flex;
559
  align-items: center;
560
+ margin: 20px 0;
561
+ color: var(--text-muted);
562
  font-size: 13px;
563
  }
564
 
 
566
  .divider::after {
567
  content: "";
568
  flex: 1;
569
+ border-bottom: 1px solid var(--glass-border);
570
+ margin: 0 15px;
571
+ }
572
+
573
+ /* ── Profile Pic ────────────────────────────────────── */
574
+ .profile-container {
575
+ position: relative;
576
+ display: inline-block;
577
+ }
578
+
579
+ .profile-pic {
580
+ width: 44px;
581
+ height: 44px;
582
+ border-radius: 50%;
583
+ object-fit: cover;
584
+ border: 2px solid var(--primary-color);
585
+ cursor: pointer;
586
+ transition: all 0.3s ease;
587
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
588
+ }
589
+
590
+ .profile-pic:hover {
591
+ transform: scale(1.05);
592
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
593
+ filter: brightness(0.8);
594
+ }
595
+
596
+ .profile-pic:hover::after {
597
+ content: "Edit";
598
+ position: absolute;
599
+ bottom: 0;
600
+ left: 0;
601
+ background: var(--input-bg);
602
+ color: var(--text-main);
603
+ font-size: 11px;
604
+ width: 100%;
605
+ text-align: center;
606
+ border-radius: 0 0 50% 50%;
607
+ pointer-events: none;
608
+ }
609
+
610
+ .username-text {
611
+ font-size: 16px;
612
+ color: var(--text-main);
613
+ font-weight: 500;
614
  }
templates/admin.html ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Admin Dashboard</title>
8
+ <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
9
+ <script src="{{ url_for('static', filename='script.js') }}"></script>
10
+ <style>
11
+ .admin-table {
12
+ width: 100%;
13
+ border-collapse: collapse;
14
+ margin-top: 20px;
15
+ color: var(--text-main);
16
+ }
17
+
18
+ .admin-table th,
19
+ .admin-table td {
20
+ padding: 12px;
21
+ border: 1px solid var(--glass-border);
22
+ text-align: left;
23
+ }
24
+
25
+ .admin-table th {
26
+ background: rgba(139, 92, 246, 0.2);
27
+ }
28
+
29
+ .admin-table td {
30
+ background: var(--input-bg);
31
+ }
32
+
33
+ .file-list {
34
+ margin: 0;
35
+ padding-left: 20px;
36
+ }
37
+
38
+ .file-link {
39
+ color: var(--primary-color);
40
+ text-decoration: none;
41
+ font-weight: bold;
42
+ }
43
+
44
+ .file-link:hover {
45
+ text-decoration: underline;
46
+ }
47
+ </style>
48
+ </head>
49
+
50
+ <body>
51
+ <header>
52
+ <h1>πŸ›‘οΈ Admin Dashboard</h1>
53
+ <div class="header-right">
54
+ <button class="logout-btn" style="background-color: var(--primary-color);"
55
+ onclick="toggleLightMode()">πŸŒ—</button>
56
+ <a href="/">← Dashboard</a>
57
+ <a href="/logout"><button class="logout-btn">Logout</button></a>
58
+ </div>
59
+ </header>
60
+
61
+ <div class="container" style="max-width: 1000px;">
62
+ <div class="upload-box" style="text-align: left;">
63
+ <h2>πŸ‘₯ Registered Users & Files</h2>
64
+ <table class="admin-table">
65
+ <thead>
66
+ <tr>
67
+ <th>ID</th>
68
+ <th>User</th>
69
+ <th>Email</th>
70
+ <th>Preferred Model</th>
71
+ <th>Files (Click to Download)</th>
72
+ </tr>
73
+ </thead>
74
+ <tbody>
75
+ {% for user in users %}
76
+ <tr>
77
+ <td>{{ user.id }}</td>
78
+ <td>
79
+ {% if user.is_admin %}⭐{% endif %}
80
+ {{ user.username }}
81
+ </td>
82
+ <td>{{ user.email }}</td>
83
+ <td>{{ user.preferred_model }}</td>
84
+ <td>
85
+ {% if user_files[user.username] %}
86
+ <ul class="file-list">
87
+ {% for f in user_files[user.username] %}
88
+ <li>
89
+ πŸ“„ <a href="/download/{{ user.username }}/{{ f }}" class="file-link"
90
+ title="Download {{ f }}">{{ f }}</a>
91
+ </li>
92
+ {% endfor %}
93
+ </ul>
94
+ {% else %}
95
+ <span style="color: var(--text-muted);">No files</span>
96
+ {% endif %}
97
+ </td>
98
+ </tr>
99
+ {% endfor %}
100
+ </tbody>
101
+ </table>
102
+ </div>
103
+ </div>
104
+ </body>
105
+
106
+ </html>
templates/chat.html CHANGED
@@ -1,17 +1,25 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>RAG Chat</title>
7
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
 
8
  </head>
 
9
  <body>
10
 
11
  <!-- ── Header ── -->
12
  <header>
13
  <h1>πŸ’¬ RAG PDF Assistant</h1>
14
- <a href="/">← Upload New PDF</a>
 
 
 
 
 
15
  </header>
16
 
17
  <!-- ── Chat Box ── -->
@@ -28,18 +36,14 @@
28
 
29
  <!-- ── Input Area ── -->
30
  <div class="input-area">
31
- <input
32
- type="text"
33
- id="questionInput"
34
- placeholder="Ask a question..."
35
- />
36
  <button id="askBtn">Ask</button>
37
  <button id="clearBtn">Clear History</button>
38
  </div>
39
 
40
  <!-- ── Loading Spinner ── -->
41
  <div id="loader" style="display:none;">
42
- ⏳ Thinking...
43
  </div>
44
  </div>
45
 
@@ -71,7 +75,13 @@
71
  }
72
 
73
  if (!currentFile) {
74
- alert("Please upload a PDF first!")
 
 
 
 
 
 
75
  return
76
  }
77
 
@@ -90,7 +100,7 @@
90
  const response = await fetch("/ask", {
91
  method: "POST",
92
  headers: { "Content-Type": "application/json" },
93
- body: JSON.stringify({
94
  question: question,
95
  filename: currentFile
96
  })
@@ -106,35 +116,13 @@
106
  <strong>Assistant:</strong> ${data.answer}
107
  </div>
108
  `
109
-
110
- // ── Show sources with confidence score ──
111
- if (data.sources && data.sources.length > 0) {
112
- let sourcesHtml = `<div class="sources"><strong>πŸ“Œ Sources:</strong><ul>`
113
- data.sources.forEach(src => {
114
- let confidenceColor = src.confidence >= 70 ? "green"
115
- : src.confidence >= 40 ? "orange"
116
- : "red"
117
- sourcesHtml += `
118
- <li>
119
- πŸ“„ ${src.filename} - Page ${src.page}
120
- <span class="confidence" style="color:${confidenceColor}">
121
- ● ${src.confidence}% match
122
- </span>
123
- </li>
124
- `
125
- })
126
- sourcesHtml += `</ul></div>`
127
- chatBox.innerHTML += sourcesHtml
128
- }
129
-
130
  } else {
131
  chatBox.innerHTML += `
132
  <div class="message error">
133
- ❌ ${data.error}
134
  </div>
135
  `
136
  }
137
-
138
  } catch (error) {
139
  loader.style.display = "none"
140
  chatBox.innerHTML += `
@@ -166,4 +154,5 @@
166
  </script>
167
 
168
  </body>
 
169
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
  <title>RAG Chat</title>
8
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
9
+ <script src="{{ url_for('static', filename='script.js') }}"></script>
10
  </head>
11
+
12
  <body>
13
 
14
  <!-- ── Header ── -->
15
  <header>
16
  <h1>πŸ’¬ RAG PDF Assistant</h1>
17
+ <div class="header-right">
18
+ <button class="logout-btn" style="background-color: var(--primary-color);"
19
+ onclick="toggleLightMode()">πŸŒ—</button>
20
+ <a href="/profile">πŸ‘€ Profile</a>
21
+ <a href="/">← Dashboard</a>
22
+ </div>
23
  </header>
24
 
25
  <!-- ── Chat Box ── -->
 
36
 
37
  <!-- ── Input Area ── -->
38
  <div class="input-area">
39
+ <input type="text" id="questionInput" placeholder="Ask a question..." />
 
 
 
 
40
  <button id="askBtn">Ask</button>
41
  <button id="clearBtn">Clear History</button>
42
  </div>
43
 
44
  <!-- ── Loading Spinner ── -->
45
  <div id="loader" style="display:none;">
46
+ Generating response...
47
  </div>
48
  </div>
49
 
 
75
  }
76
 
77
  if (!currentFile) {
78
+ chatBox.innerHTML += `
79
+ <div class="message error">
80
+ ❌ Please go back and upload a PDF first!
81
+ </div>
82
+ `
83
+ document.getElementById("questionInput").value = ""
84
+ chatBox.scrollTop = chatBox.scrollHeight
85
  return
86
  }
87
 
 
100
  const response = await fetch("/ask", {
101
  method: "POST",
102
  headers: { "Content-Type": "application/json" },
103
+ body: JSON.stringify({
104
  question: question,
105
  filename: currentFile
106
  })
 
116
  <strong>Assistant:</strong> ${data.answer}
117
  </div>
118
  `
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  } else {
120
  chatBox.innerHTML += `
121
  <div class="message error">
122
+ ❌ Error: ${data.error || "Unknown error"}
123
  </div>
124
  `
125
  }
 
126
  } catch (error) {
127
  loader.style.display = "none"
128
  chatBox.innerHTML += `
 
154
  </script>
155
 
156
  </body>
157
+
158
  </html>
templates/index.html CHANGED
@@ -1,20 +1,55 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>RAG Application</title>
7
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
 
8
  </head>
 
9
  <body>
10
 
11
  <!-- ── Header ── -->
12
  <header>
13
- <h1>πŸ“„ RAG PDF Assistant</h1>
14
- <div class="header-right">
15
- <span>πŸ‘€ {{ username }}</span>
16
- <a href="/logout"><button class="logout-btn">Logout</button></a>
17
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  </header>
19
 
20
  <div class="container">
@@ -36,6 +71,7 @@
36
  </ul>
37
  </div>
38
 
 
39
  <div class="clear-box">
40
  <h2>πŸ”„ Reset Vector Store</h2>
41
  <p>This will delete all stored embeddings</p>
@@ -71,13 +107,19 @@
71
  let icon = "πŸ“„"
72
  if (file.endsWith(".docx")) icon = "πŸ“"
73
  if (file.endsWith(".txt")) icon = "πŸ“ƒ"
74
-
 
 
 
75
  filesList.innerHTML += `
76
- <li class="file-item">
77
- <span>πŸ“„ ${file}</span>
 
 
 
78
  <button
79
  class="delete-btn"
80
- onclick="deleteFile('${file}')">
81
  πŸ—‘οΈ Delete
82
  </button>
83
  </li>
@@ -85,6 +127,33 @@
85
  })
86
  }
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  // ── Upload PDF ────────────────────────────────────
89
  document.getElementById("uploadForm").addEventListener("submit", async (e) => {
90
  e.preventDefault()
@@ -115,55 +184,56 @@
115
  })
116
 
117
  document.getElementById("clearVectorBtn").addEventListener("click", async () => {
118
- if (!confirm("Are you sure? This will delete ALL embeddings!")) return
119
 
120
- const clearStatus = document.getElementById("clearStatus")
121
- clearStatus.innerText = "Clearing..."
122
 
123
- const response = await fetch("/clear_vectorstore", {
124
- method: "POST"
125
- })
126
 
127
- const data = await response.json()
128
 
129
- if (response.ok) {
130
- clearStatus.innerText = "βœ… " + data.message
131
- clearStatus.style.color = "green"
132
- localStorage.removeItem("currentFile")
133
- loadFiles()
134
- } else {
135
- clearStatus.innerText = "❌ " + data.error
136
- clearStatus.style.color = "red"
137
- }
138
- })
139
 
140
  // ── Load Files on Page Load ───────────────────────
141
  async function deleteFile(filename) {
142
- if (!confirm(`Are you sure you want to delete ${filename}?`)) return
143
 
144
- const response = await fetch("/delete", {
145
- method: "POST",
146
- headers: { "Content-Type": "application/json" },
147
- body: JSON.stringify({ filename: filename })
148
- })
 
 
149
 
150
- const data = await response.json()
 
151
 
152
- if (response.ok) {
153
- alert("βœ… " + data.message)
 
 
154
 
155
- // Clear localStorage if deleted file was selected
156
- if (localStorage.getItem("currentFile") === filename) {
157
- localStorage.removeItem("currentFile")
158
  }
159
-
160
- loadFiles() // Refresh list
161
- } else {
162
- alert("❌ " + data.error)
163
  }
164
- }
165
  loadFiles()
166
  </script>
167
 
168
  </body>
 
169
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
  <title>RAG Application</title>
8
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
9
+ <script src="{{ url_for('static', filename='script.js') }}"></script>
10
  </head>
11
+
12
  <body>
13
 
14
  <!-- ── Header ── -->
15
  <header>
16
+ <h1>πŸ“„ RAG Assistant</h1>
17
+ <div class="header-right">
18
+
19
+ <!-- ── Profile Pic ── -->
20
+ <div class="profile-container">
21
+ <img id="profilePic"
22
+ src="{{ current_user.profile_pic or url_for('static', filename='default_avatar.png') }}"
23
+ alt="Profile" class="profile-pic" onclick="document.getElementById('picInput').click()"
24
+ title="Click to change profile picture" />
25
+ <input type="file" id="picInput" accept="image/*" style="display:none"
26
+ onchange="uploadProfilePic(this)" />
27
+ </div>
28
+
29
+ <!-- ── Username ── -->
30
+ <a href="/profile" style="text-decoration: none; display: flex; align-items: center;">
31
+ <span class="username-text" style="cursor: pointer;" title="Go to Profile">{{ current_user.username
32
+ }}</span>
33
+ </a>
34
+
35
+ <!-- ── Admin Button ── -->
36
+ {% if current_user.is_admin %}
37
+ <a href="/admin">
38
+ <button class="logout-btn" style="background-color: var(--danger-color); margin-right: 10px;">πŸ›‘οΈ Admin
39
+ Node</button>
40
+ </a>
41
+ {% endif %}
42
+
43
+ <!-- ── Light Mode Toggle ── -->
44
+ <button class="logout-btn" style="background-color: var(--primary-color);"
45
+ onclick="toggleLightMode()">πŸŒ—</button>
46
+
47
+ <!-- ── Logout ── -->
48
+ <a href="/logout">
49
+ <button class="logout-btn">Logout</button>
50
+ </a>
51
+
52
+ </div>
53
  </header>
54
 
55
  <div class="container">
 
71
  </ul>
72
  </div>
73
 
74
+
75
  <div class="clear-box">
76
  <h2>πŸ”„ Reset Vector Store</h2>
77
  <p>This will delete all stored embeddings</p>
 
107
  let icon = "πŸ“„"
108
  if (file.endsWith(".docx")) icon = "πŸ“"
109
  if (file.endsWith(".txt")) icon = "πŸ“ƒ"
110
+
111
+ const isSelected = localStorage.getItem("currentFile") === file ? "checked" : ""
112
+ const activeClass = localStorage.getItem("currentFile") === file ? "active-file" : ""
113
+
114
  filesList.innerHTML += `
115
+ <li class="file-item ${activeClass}" style="cursor: pointer;" onclick="selectFile('${file}')">
116
+ <span>
117
+ <input type="radio" name="selectedFile" value="${file}" ${isSelected} class="file-radio" onclick="event.stopPropagation(); selectFile('${file}')">
118
+ ${icon} ${file}
119
+ </span>
120
  <button
121
  class="delete-btn"
122
+ onclick="event.stopPropagation(); deleteFile('${file}')">
123
  πŸ—‘οΈ Delete
124
  </button>
125
  </li>
 
127
  })
128
  }
129
 
130
+ function selectFile(filename) {
131
+ localStorage.setItem("currentFile", filename)
132
+ loadFiles() // Refresh list to show active selection
133
+ }
134
+
135
+ async function uploadProfilePic(input) {
136
+ const file = input.files[0]
137
+ if (!file) return
138
+
139
+ const formData = new FormData()
140
+ formData.append("profile_pic", file)
141
+
142
+ const response = await fetch("/upload_profile_pic", {
143
+ method: "POST",
144
+ body: formData
145
+ })
146
+
147
+ const data = await response.json()
148
+
149
+ if (response.ok) {
150
+ // ── Update pic instantly ──
151
+ document.getElementById("profilePic").src = data.profile_pic + "?t=" + Date.now()
152
+ } else {
153
+ alert("❌ " + data.error)
154
+ }
155
+ }
156
+
157
  // ── Upload PDF ────────────────────────────────────
158
  document.getElementById("uploadForm").addEventListener("submit", async (e) => {
159
  e.preventDefault()
 
184
  })
185
 
186
  document.getElementById("clearVectorBtn").addEventListener("click", async () => {
187
+ if (!confirm("Are you sure? This will delete ALL embeddings!")) return
188
 
189
+ const clearStatus = document.getElementById("clearStatus")
190
+ clearStatus.innerText = "Clearing..."
191
 
192
+ const response = await fetch("/clear_vectorstore", {
193
+ method: "POST"
194
+ })
195
 
196
+ const data = await response.json()
197
 
198
+ if (response.ok) {
199
+ clearStatus.innerText = "βœ… " + data.message
200
+ clearStatus.style.color = "green"
201
+ localStorage.removeItem("currentFile")
202
+ loadFiles()
203
+ } else {
204
+ clearStatus.innerText = "❌ " + data.error
205
+ clearStatus.style.color = "red"
206
+ }
207
+ })
208
 
209
  // ── Load Files on Page Load ───────────────────────
210
  async function deleteFile(filename) {
211
+ if (!confirm(`Are you sure you want to delete ${filename}?`)) return
212
 
213
+ const response = await fetch("/delete", {
214
+ method: "POST",
215
+ headers: { "Content-Type": "application/json" },
216
+ body: JSON.stringify({ filename: filename })
217
+ })
218
+
219
+ const data = await response.json()
220
 
221
+ if (response.ok) {
222
+ alert("βœ… " + data.message)
223
 
224
+ // Clear localStorage if deleted file was selected
225
+ if (localStorage.getItem("currentFile") === filename) {
226
+ localStorage.removeItem("currentFile")
227
+ }
228
 
229
+ loadFiles() // Refresh list
230
+ } else {
231
+ alert("❌ " + data.error)
232
  }
 
 
 
 
233
  }
 
234
  loadFiles()
235
  </script>
236
 
237
  </body>
238
+
239
  </html>
templates/login.html CHANGED
@@ -1,14 +1,16 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Login</title>
7
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
8
  </head>
 
9
  <body>
10
  <header>
11
- <h1>πŸ”‘ Login</h1>
12
  </header>
13
 
14
  <div class="container">
@@ -31,7 +33,7 @@
31
  </span>
32
  </div>
33
 
34
- <a href="{{ url_for('google.login') }}">
35
  <button class="google-btn">
36
  🌐 Login with Google
37
  </button>
@@ -40,5 +42,11 @@
40
  <p>Don't have an account? <a href="/register">Register</a></p>
41
  </div>
42
  </div>
 
 
 
 
 
43
  </body>
 
44
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
  <title>Login</title>
8
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
9
  </head>
10
+
11
  <body>
12
  <header>
13
+ <h1>πŸ“„ PDF Assistant RAG</h1>
14
  </header>
15
 
16
  <div class="container">
 
33
  </span>
34
  </div>
35
 
36
+ <a href="{{ url_for('google.login') }}">
37
  <button class="google-btn">
38
  🌐 Login with Google
39
  </button>
 
42
  <p>Don't have an account? <a href="/register">Register</a></p>
43
  </div>
44
  </div>
45
+
46
+ <!-- ── Footer ── -->
47
+ <footer style="text-align: center; padding: 20px; color: var(--text-muted); margin-top: 40px; font-size: 14px;">
48
+ <p>&copy; 2026 RAG Assistant. Param20h</p>
49
+ </footer>
50
  </body>
51
+
52
  </html>
templates/profile.html ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>User Profile</title>
8
+ <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
9
+ <script src="{{ url_for('static', filename='script.js') }}"></script>
10
+ </head>
11
+
12
+ <body>
13
+
14
+ <!-- ── Header ── -->
15
+ <header>
16
+ <h1>πŸ‘€ My Profile</h1>
17
+ <div class="header-right">
18
+ <button class="logout-btn" style="background-color: var(--primary-color);"
19
+ onclick="toggleLightMode()">πŸŒ—</button>
20
+ <a href="/">← Back to Dashboard</a>
21
+ <a href="/logout"><button class="logout-btn">Logout</button></a>
22
+ </div>
23
+ </header>
24
+
25
+ <div class="container">
26
+
27
+ <!-- ── User Profile ── -->
28
+ <div class="upload-box" style="display: flex; align-items: center; gap: 20px; text-align: left;">
29
+ <div class="profile-container" style="display:inline-block;">
30
+ <img id="profilePic"
31
+ src="{{ current_user.profile_pic or url_for('static', filename='default_avatar.png') }}"
32
+ alt="Profile" class="profile-pic" style="width: 80px; height: 80px;"
33
+ onclick="document.getElementById('picInput').click()" title="Click to change profile picture" />
34
+ <input type="file" id="picInput" accept="image/*" style="display:none"
35
+ onchange="uploadProfilePic(this)" />
36
+ </div>
37
+ <div>
38
+ <h2 style="margin-bottom: 5px;">{{ current_user.username }}</h2>
39
+ <p style="color: var(--text-muted);">{{ current_user.email }}</p>
40
+ </div>
41
+ </div>
42
+
43
+ <!-- ── Settings / Custom APIs ── -->
44
+ <div class="upload-box" style="text-align: left;">
45
+ <h2>βš™οΈ API Settings</h2>
46
+ <form id="settingsForm">
47
+ <label style="color: var(--text-main); font-weight: 500; font-size: 14px;">Select Preferred
48
+ Model</label>
49
+ <select id="preferredModel" name="preferred_model"
50
+ style="width: 100%; padding: 12px; margin: 10px 0 20px; border-radius: 8px; border: 1px solid var(--glass-border); background: var(--input-bg); color: var(--text-main); outline: none;">
51
+ <option value="groq" {% if current_user.preferred_model=='groq' %}selected{% endif %}>Groq (Llama 3)
52
+ </option>
53
+ <option value="gemini" {% if current_user.preferred_model=='gemini' %}selected{% endif %}>Gemini
54
+ (Google)</option>
55
+ </select>
56
+
57
+ <label style="color: var(--text-main); font-weight: 500; font-size: 14px;">Groq API Key
58
+ (Optional)</label>
59
+ <input type="password" id="groqKey" name="groq_key" placeholder="Enter your Groq API Key..."
60
+ value="{{ current_user.groq_api_key or '' }}" style="margin-top: 10px;">
61
+
62
+ <label style="color: var(--text-main); font-weight: 500; font-size: 14px;">Gemini API Key
63
+ (Optional)</label>
64
+ <input type="password" id="geminiKey" name="gemini_key" placeholder="Enter your Gemini API Key..."
65
+ value="{{ current_user.gemini_api_key or '' }}" style="margin-top: 10px;">
66
+
67
+ <button type="submit" style="width: 100%; margin-top: 15px;">Save Settings</button>
68
+ </form>
69
+ <div id="settingsStatus" style="margin-top: 15px; font-weight: 500;"></div>
70
+ </div>
71
+
72
+ <!-- ── Uploaded Files List ── -->
73
+ <div class="files-box">
74
+ <h2>πŸ“‹ My Uploaded Files</h2>
75
+ <ul id="filesList">
76
+ <!-- Files appear here -->
77
+ </ul>
78
+ </div>
79
+ </div>
80
+
81
+ <script>
82
+ async function uploadProfilePic(input) {
83
+ const file = input.files[0]
84
+ if (!file) return
85
+
86
+ const formData = new FormData()
87
+ formData.append("profile_pic", file)
88
+
89
+ const response = await fetch("/upload_profile_pic", {
90
+ method: "POST",
91
+ body: formData
92
+ })
93
+
94
+ const data = await response.json()
95
+
96
+ if (response.ok) {
97
+ document.getElementById("profilePic").src = data.profile_pic + "?t=" + Date.now()
98
+ } else {
99
+ alert("❌ " + data.error)
100
+ }
101
+ }
102
+
103
+ document.getElementById("settingsForm").addEventListener("submit", async (e) => {
104
+ e.preventDefault()
105
+ const statusDiv = document.getElementById("settingsStatus")
106
+ statusDiv.innerText = "Saving..."
107
+ statusDiv.style.color = "var(--primary-color)"
108
+
109
+ const preferredModel = document.getElementById("preferredModel").value
110
+ const groqKey = document.getElementById("groqKey").value
111
+ const geminiKey = document.getElementById("geminiKey").value
112
+
113
+ const response = await fetch("/update_settings", {
114
+ method: "POST",
115
+ headers: { "Content-Type": "application/json" },
116
+ body: JSON.stringify({ preferred_model: preferredModel, groq_key: groqKey, gemini_key: geminiKey })
117
+ })
118
+
119
+ const data = await response.json()
120
+ if (response.ok) {
121
+ statusDiv.innerText = "βœ… " + data.message
122
+ statusDiv.style.color = "var(--success-color)"
123
+ } else {
124
+ statusDiv.innerText = "❌ " + data.error
125
+ statusDiv.style.color = "var(--danger-color)"
126
+ }
127
+ })
128
+
129
+ async function loadFiles() {
130
+ const response = await fetch("/files")
131
+ const data = await response.json()
132
+ const filesList = document.getElementById("filesList")
133
+ filesList.innerHTML = ""
134
+
135
+ if (data.files.length === 0) {
136
+ filesList.innerHTML = "<li>No files uploaded yet</li>"
137
+ return
138
+ }
139
+
140
+ data.files.forEach(file => {
141
+ let icon = "πŸ“„"
142
+ if (file.endsWith(".docx")) icon = "πŸ“"
143
+ if (file.endsWith(".txt")) icon = "πŸ“ƒ"
144
+
145
+ filesList.innerHTML += `
146
+ <li class="file-item">
147
+ <span>${icon} ${file}</span>
148
+ <button class="delete-btn" onclick="deleteFile('${file}')">πŸ—‘οΈ Delete</button>
149
+ </li>
150
+ `
151
+ })
152
+ }
153
+
154
+ async function deleteFile(filename) {
155
+ if (!confirm(`Are you sure you want to delete ${filename}?`)) return
156
+ const response = await fetch("/delete", {
157
+ method: "POST",
158
+ headers: { "Content-Type": "application/json" },
159
+ body: JSON.stringify({ filename: filename })
160
+ })
161
+ const data = await response.json()
162
+ if (response.ok) {
163
+ if (localStorage.getItem("currentFile") === filename) {
164
+ localStorage.removeItem("currentFile")
165
+ }
166
+ loadFiles()
167
+ } else {
168
+ alert("❌ " + data.error)
169
+ }
170
+ }
171
+
172
+ loadFiles()
173
+ </script>
174
+ </body>
175
+
176
+ </html>
templates/register.html CHANGED
@@ -1,14 +1,16 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Register</title>
7
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
8
  </head>
 
9
  <body>
10
  <header>
11
- <h1>πŸ“ Register</h1>
12
  </header>
13
 
14
  <div class="container">
@@ -29,5 +31,11 @@
29
  <p>Already have an account? <a href="/login">Login</a></p>
30
  </div>
31
  </div>
 
 
 
 
 
32
  </body>
 
33
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
  <title>Register</title>
8
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
9
  </head>
10
+
11
  <body>
12
  <header>
13
+ <h1>πŸ“„ PDF Assistant RAG</h1>
14
  </header>
15
 
16
  <div class="container">
 
31
  <p>Already have an account? <a href="/login">Login</a></p>
32
  </div>
33
  </div>
34
+
35
+ <!-- ── Footer ── -->
36
+ <footer style="text-align: center; padding: 20px; color: var(--text-muted); margin-top: 40px; font-size: 14px;">
37
+ <p>&copy; 2026 RAG Assistant. Param20h</p>
38
+ </footer>
39
  </body>
40
+
41
  </html>
users.db ADDED
File without changes