Param20h commited on
Commit
e06f93a
Β·
unverified Β·
0 Parent(s):

first commit

Browse files
.gitattributes ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ *.png filter=lfs diff=lfs merge=lfs -text
2
+ *.jpg filter=lfs diff=lfs merge=lfs -text
3
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
4
+ *.gif filter=lfs diff=lfs merge=lfs -text
5
+ *.webp filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ .env
2
+ uploads/
3
+ vectorstore/
4
+ __pycache__/
5
+ *.pyc
6
+ .venv/
7
+
8
+
README.md ADDED
File without changes
app.py ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pickle
3
+ import shutil
4
+ from flask import Flask, request, jsonify, render_template, redirect, url_for
5
+ from flask_login import LoginManager, login_user, logout_user, login_required, current_user
6
+ from dotenv import load_dotenv
7
+ from models import db, User
8
+ from rag.chunker import load_and_chunk
9
+ from rag.embeddings import store_embeddings
10
+ from rag.retriever import retrieve_chunks
11
+ from rag.generator import generate_answer
12
+ from config import SECRET_KEY, SQLALCHEMY_DATABASE_URI
13
+
14
+ # ── Init ─────────────────────────────────────────────
15
+ load_dotenv()
16
+ app = Flask(__name__)
17
+
18
+ app.config["SECRET_KEY"] = SECRET_KEY
19
+ app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
20
+ app.config["UPLOAD_FOLDER"] = "uploads"
21
+
22
+ META_PATH = os.path.join("vectorstore", "metadata.pkl")
23
+
24
+ os.makedirs("uploads", exist_ok=True)
25
+ os.makedirs("vectorstore", exist_ok=True)
26
+
27
+ # ── Database & Login Manager ──────────────────────────
28
+ db.init_app(app)
29
+ login_manager = LoginManager()
30
+ login_manager.init_app(app)
31
+ login_manager.login_view = "login"
32
+
33
+ chat_history = {}
34
+
35
+ @login_manager.user_loader
36
+ def load_user(user_id):
37
+ return User.query.get(int(user_id))
38
+
39
+ # ── Create DB ─────────────────────────────────────────
40
+ with app.app_context():
41
+ db.create_all()
42
+
43
+ # ── Helper ────────────────────────────────────────────
44
+ def allowed_file(filename):
45
+ return "." in filename and filename.rsplit(".", 1)[1].lower() in {"pdf"}
46
+
47
+ def get_user_upload_folder(username):
48
+ folder = os.path.join("uploads", username)
49
+ os.makedirs(folder, exist_ok=True)
50
+ return folder
51
+
52
+ def get_user_meta_path(username):
53
+ path = os.path.join("vectorstore", username)
54
+ os.makedirs(path, exist_ok=True)
55
+ return os.path.join(path, "metadata.pkl")
56
+
57
+ # ── Auth Routes ───────────────────────────────────────
58
+
59
+ @app.route("/")
60
+ @login_required
61
+ def index():
62
+ return render_template("index.html", username=current_user.username)
63
+
64
+ @app.route("/register", methods=["GET", "POST"])
65
+ def register():
66
+ if request.method == "POST":
67
+ data = request.form
68
+ username = data.get("username")
69
+ email = data.get("email")
70
+ password = data.get("password")
71
+
72
+ if User.query.filter_by(username=username).first():
73
+ return render_template("register.html", error="Username already exists!")
74
+
75
+ if User.query.filter_by(email=email).first():
76
+ return render_template("register.html", error="Email already exists!")
77
+
78
+ user = User(username=username, email=email)
79
+ user.set_password(password)
80
+ db.session.add(user)
81
+ db.session.commit()
82
+
83
+ return redirect(url_for("login"))
84
+
85
+ return render_template("register.html")
86
+
87
+ @app.route("/login", methods=["GET", "POST"])
88
+ def login():
89
+ if request.method == "POST":
90
+ data = request.form
91
+ username = data.get("username")
92
+ password = data.get("password")
93
+
94
+ user = User.query.filter_by(username=username).first()
95
+
96
+ if not user or not user.check_password(password):
97
+ return render_template("login.html", error="Invalid username or password!")
98
+
99
+ login_user(user)
100
+ return redirect(url_for("index"))
101
+
102
+ return render_template("login.html")
103
+
104
+ @app.route("/logout")
105
+ @login_required
106
+ def logout():
107
+ logout_user()
108
+ return redirect(url_for("login"))
109
+
110
+ # ── App Routes ────────────────────────────────────────
111
+
112
+ @app.route("/chat")
113
+ @login_required
114
+ def chat():
115
+ return render_template("chat.html", username=current_user.username)
116
+
117
+ @app.route("/files", methods=["GET"])
118
+ @login_required
119
+ def get_files():
120
+ try:
121
+ folder = get_user_upload_folder(current_user.username)
122
+ files = [f for f in os.listdir(folder) if f.endswith(".pdf")]
123
+ return jsonify({"files": files}), 200
124
+ except Exception as e:
125
+ return jsonify({"error": str(e)}), 500
126
+
127
+ @app.route("/upload", methods=["GET", "POST"])
128
+ @login_required
129
+ def upload():
130
+ try:
131
+ if "pdf" not in request.files:
132
+ return jsonify({"error": "No file found"}), 400
133
+
134
+ file = request.files["pdf"]
135
+
136
+ if file.filename == "":
137
+ return jsonify({"error": "No file selected"}), 400
138
+
139
+ if not allowed_file(file.filename):
140
+ return jsonify({"error": "Only PDF files allowed"}), 400
141
+
142
+ folder = get_user_upload_folder(current_user.username)
143
+ filepath = os.path.join(folder, file.filename)
144
+ file.save(filepath)
145
+
146
+ meta_path = get_user_meta_path(current_user.username)
147
+ chunks = load_and_chunk(filepath)
148
+ store_embeddings(chunks, file.filename, meta_path)
149
+
150
+ return jsonify({"message": f"{file.filename} uploaded successfully!"}), 200
151
+
152
+ except Exception as e:
153
+ return jsonify({"error": str(e)}), 500
154
+
155
+ @app.route("/ask", methods=["POST"])
156
+ @login_required
157
+ def ask():
158
+ try:
159
+ data = request.get_json()
160
+ question = data.get("question", "").strip()
161
+ filename = data.get("filename", "").strip()
162
+
163
+ if not question:
164
+ return jsonify({"error": "Question cannot be empty"}), 400
165
+
166
+ meta_path = get_user_meta_path(current_user.username)
167
+ context_chunks = retrieve_chunks(question, filename, meta_path)
168
+ answer = generate_answer(question, context_chunks)
169
+
170
+ username = current_user.username
171
+ if username not in chat_history:
172
+ chat_history[username] = []
173
+
174
+ chat_history[username].append({
175
+ "question": question,
176
+ "answer": answer
177
+ })
178
+
179
+ return jsonify({
180
+ "answer": answer,
181
+ "sources": context_chunks
182
+ }), 200
183
+
184
+ except Exception as e:
185
+ return jsonify({"error": str(e)}), 500
186
+
187
+ @app.route("/history", methods=["GET"])
188
+ @login_required
189
+ def history():
190
+ try:
191
+ username = current_user.username
192
+ return jsonify({"history": chat_history.get(username, [])}), 200
193
+ except Exception as e:
194
+ return jsonify({"error": str(e)}), 500
195
+
196
+ @app.route("/clear", methods=["POST"])
197
+ @login_required
198
+ def clear():
199
+ try:
200
+ username = current_user.username
201
+ chat_history[username] = []
202
+ return jsonify({"message": "Chat history cleared!"}), 200
203
+ except Exception as e:
204
+ return jsonify({"error": str(e)}), 500
205
+
206
+ @app.route("/delete", methods=["POST"])
207
+ @login_required
208
+ def delete():
209
+ try:
210
+ data = request.get_json()
211
+ filename = data.get("filename", "")
212
+
213
+ if not filename:
214
+ return jsonify({"error": "Filename not provided"}), 400
215
+
216
+ folder = get_user_upload_folder(current_user.username)
217
+ filepath = os.path.join(folder, filename)
218
+
219
+ if not os.path.exists(filepath):
220
+ return jsonify({"error": "File not found"}), 404
221
+
222
+ os.remove(filepath)
223
+
224
+ meta_path = get_user_meta_path(current_user.username)
225
+ if os.path.exists(meta_path):
226
+ with open(meta_path, "rb") as f:
227
+ metadata = pickle.load(f)
228
+ new_metadata = [m for m in metadata if m["filename"] != filename]
229
+ with open(meta_path, "wb") as f:
230
+ pickle.dump(new_metadata, f)
231
+
232
+ return jsonify({"message": f"{filename} deleted successfully!"}), 200
233
+
234
+ except Exception as e:
235
+ return jsonify({"error": str(e)}), 500
236
+
237
+ @app.route("/clear_vectorstore", methods=["POST"])
238
+ @login_required
239
+ def clear_vectorstore():
240
+ try:
241
+ username = current_user.username
242
+ vectorstore_path = os.path.join("vectorstore", username)
243
+
244
+ if os.path.exists(vectorstore_path):
245
+ shutil.rmtree(vectorstore_path)
246
+ os.makedirs(vectorstore_path, exist_ok=True)
247
+
248
+ return jsonify({"message": "Vector store cleared successfully!"}), 200
249
+
250
+ except Exception as e:
251
+ return jsonify({"error": str(e)}), 500
252
+
253
+ # ── Run ───────────────────────────────────────────────
254
+ if __name__ == "__main__":
255
+ app.run(debug=True, host="0.0.0.0", port=5000)
config.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
instance/users.db ADDED
Binary file (16.4 kB). View file
 
models.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ upload_folder = db.Column(db.String(200), nullable=True)
13
+
14
+ def set_password(self, password):
15
+ self.password = generate_password_hash(password)
16
+
17
+ def check_password(self, password):
18
+ return check_password_hash(self.password, password)
rag/chunker.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fitz
2
+ from config import CHUNK_SIZE, CHUNK_OVERLAP
3
+
4
+ def load_pdf(filepath):
5
+ doc = fitz.open(filepath)
6
+ text_pages = []
7
+
8
+ for page_num, page in enumerate(doc):
9
+ text = page.get_text()
10
+ text_pages.append({
11
+ "text": text,
12
+ "page": page_num + 1
13
+ })
14
+
15
+ doc.close()
16
+ return text_pages
17
+
18
+ def split_text(text, page_num):
19
+ chunks = []
20
+ start = 0
21
+
22
+ while start < len(text):
23
+ end = start + CHUNK_SIZE
24
+ chunk = text[start:end]
25
+
26
+ if chunk.strip():
27
+ chunks.append({
28
+ "text": chunk,
29
+ "page": page_num
30
+ })
31
+ start = end - CHUNK_OVERLAP
32
+ return chunks
33
+
34
+ def load_and_chunk(filepath):
35
+ pages = load_pdf(filepath)
36
+ all_chunks = []
37
+
38
+ for page in pages:
39
+ chunks = split_text(page["text"], page["page"])
40
+ all_chunks.extend(chunks)
41
+
42
+ return all_chunks
rag/embeddings.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import faiss
2
+ import numpy as np
3
+ import pickle
4
+ import os
5
+ from sentence_transformers import SentenceTransformer
6
+ from config import EMBEDDING_MODEL, CHROMA_DB_PATH
7
+
8
+ # ── Load Model ───────────────────────────────────────
9
+ embedding_model = SentenceTransformer(EMBEDDING_MODEL)
10
+
11
+ INDEX_PATH = CHROMA_DB_PATH + "/index.faiss"
12
+
13
+ def embed_text(text):
14
+ return embedding_model.encode(text)
15
+
16
+ # ── Updated to accept meta_path ──────────────────────
17
+ def store_embeddings(chunks, filename, meta_path):
18
+ embeddings = []
19
+ metadata = []
20
+
21
+ for i, chunk in enumerate(chunks):
22
+ emb = embed_text(chunk["text"])
23
+ embeddings.append(emb)
24
+ metadata.append({
25
+ "text": chunk["text"],
26
+ "filename": filename,
27
+ "page": chunk["page"],
28
+ "chunk_index": i
29
+ })
30
+
31
+ embeddings_np = np.array(embeddings).astype("float32")
32
+ dimension = embeddings_np.shape[1]
33
+
34
+ index_path = os.path.join(os.path.dirname(meta_path), "index.faiss")
35
+
36
+ if os.path.exists(index_path):
37
+ index = faiss.read_index(index_path)
38
+ with open(meta_path, "rb") as f:
39
+ existing_metadata = pickle.load(f)
40
+ else:
41
+ index = faiss.IndexFlatL2(dimension)
42
+ existing_metadata = []
43
+
44
+ index.add(embeddings_np)
45
+ existing_metadata.extend(metadata)
46
+
47
+ faiss.write_index(index, index_path)
48
+ with open(meta_path, "wb") as f:
49
+ pickle.dump(existing_metadata, f)
rag/generator.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
rag/retriever.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import faiss
2
+ import numpy as np
3
+ import pickle
4
+ import os
5
+ from sentence_transformers import SentenceTransformer
6
+ from config import EMBEDDING_MODEL, TOP_K
7
+
8
+ embedding_model = SentenceTransformer(EMBEDDING_MODEL)
9
+
10
+ def embed_query(query):
11
+ return embedding_model.encode(query)
12
+
13
+ def retrieve_chunks(query, filename=None, meta_path=None):
14
+ if meta_path is None or not os.path.exists(meta_path):
15
+ return []
16
+
17
+ index_path = os.path.join(os.path.dirname(meta_path), "index.faiss")
18
+
19
+ if not os.path.exists(index_path):
20
+ return []
21
+
22
+ query_embedding = np.array([embed_query(query)]).astype("float32")
23
+
24
+ index = faiss.read_index(index_path)
25
+
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:
39
+ return []
40
+
41
+ max_distance = float(distances[0].max()) if distances[0].max() > 0 else 1
42
+
43
+ chunks = []
44
+ for i, idx in enumerate(indices[0]):
45
+ # ── Fix: skip invalid indices ──
46
+ if idx == -1 or idx >= len(metadata):
47
+ continue
48
+
49
+ if filename and metadata[idx]["filename"] != filename:
50
+ continue
51
+
52
+ raw_score = float(distances[0][i])
53
+ confidence = round((1 - (raw_score / max_distance)) * 100, 2)
54
+
55
+ chunks.append({
56
+ "text": metadata[idx]["text"],
57
+ "filename": metadata[idx]["filename"],
58
+ "page": metadata[idx]["page"],
59
+ "score": raw_score,
60
+ "confidence": confidence
61
+ })
62
+
63
+ if len(chunks) == TOP_K:
64
+ break
65
+
66
+ return chunks
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ flask
2
+ python-dotenv
3
+ pymupdf
4
+ faiss-cpu
5
+ sentence-transformers
6
+ ollama
7
+ flask-login
8
+ flask-sqlalchemy
9
+ werkzeug
static/script.js ADDED
File without changes
static/style.css ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
templates/chat.html ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ── -->
18
+ <div class="container">
19
+
20
+ <!-- ── Current File Badge ── -->
21
+ <div id="currentFileBadge" class="file-badge">
22
+ πŸ“„ No file selected
23
+ </div>
24
+
25
+ <div class="chat-box" id="chatBox">
26
+ <!-- Messages appear here -->
27
+ </div>
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
+
46
+ <script>
47
+ // ── Store Current Filename ───────────────────────
48
+ let currentFile = localStorage.getItem("currentFile") || ""
49
+
50
+ // ── Show Current File Badge ──────────────────────
51
+ const badge = document.getElementById("currentFileBadge")
52
+ if (currentFile) {
53
+ badge.innerText = `πŸ“„ Asking from: ${currentFile}`
54
+ badge.style.backgroundColor = "#eafaf1"
55
+ badge.style.color = "green"
56
+ } else {
57
+ badge.innerText = "⚠️ No file selected - Go back and upload a PDF"
58
+ badge.style.backgroundColor = "#fadbd8"
59
+ badge.style.color = "red"
60
+ }
61
+
62
+ // ── Ask Question ─────────────────────────────────
63
+ document.getElementById("askBtn").addEventListener("click", async () => {
64
+ const question = document.getElementById("questionInput").value.trim()
65
+ const chatBox = document.getElementById("chatBox")
66
+ const loader = document.getElementById("loader")
67
+
68
+ if (!question) {
69
+ alert("Please enter a question!")
70
+ return
71
+ }
72
+
73
+ if (!currentFile) {
74
+ alert("Please upload a PDF first!")
75
+ return
76
+ }
77
+
78
+ // ── Show user message ──
79
+ chatBox.innerHTML += `
80
+ <div class="message user">
81
+ <strong>You:</strong> ${question}
82
+ </div>
83
+ `
84
+
85
+ document.getElementById("questionInput").value = ""
86
+ loader.style.display = "block"
87
+ chatBox.scrollTop = chatBox.scrollHeight
88
+
89
+ try {
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
+ })
97
+ })
98
+
99
+ const data = await response.json()
100
+ loader.style.display = "none"
101
+
102
+ if (response.ok) {
103
+ // ── Show answer ──
104
+ chatBox.innerHTML += `
105
+ <div class="message bot">
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 += `
141
+ <div class="message error">
142
+ ❌ Something went wrong. Please try again.
143
+ </div>
144
+ `
145
+ }
146
+
147
+ // ── Auto scroll to bottom ──
148
+ chatBox.scrollTop = chatBox.scrollHeight
149
+ })
150
+
151
+
152
+ // ── Clear History ─────────────────────────────────
153
+ document.getElementById("clearBtn").addEventListener("click", async () => {
154
+ if (!confirm("Are you sure you want to clear chat history?")) return
155
+ await fetch("/clear", { method: "POST" })
156
+ document.getElementById("chatBox").innerHTML = ""
157
+ })
158
+
159
+
160
+ // ── Enter Key Support ─────────────────────────────
161
+ document.getElementById("questionInput").addEventListener("keypress", (e) => {
162
+ if (e.key === "Enter") {
163
+ document.getElementById("askBtn").click()
164
+ }
165
+ })
166
+ </script>
167
+
168
+ </body>
169
+ </html>
templates/index.html ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
21
+ <!-- ── Upload Section ── -->
22
+ <div class="upload-box">
23
+ <h2>Upload PDF</h2>
24
+ <form id="uploadForm">
25
+ <input type="file" id="pdfFile" accept=".pdf" required />
26
+ <button type="submit">Upload & Index</button>
27
+ </form>
28
+ <div id="uploadStatus"></div>
29
+ </div>
30
+
31
+ <!-- ── Uploaded Files List ── -->
32
+ <div class="files-box">
33
+ <h2>πŸ“‹ Uploaded Files</h2>
34
+ <ul id="filesList">
35
+ <!-- Files appear here -->
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>
42
+ <button id="clearVectorBtn" class="clear-vector-btn">
43
+ πŸ”„ Clear Vector Store
44
+ </button>
45
+ <div id="clearStatus"></div>
46
+ </div>
47
+
48
+ <!-- ── Navigate to Chat ── -->
49
+ <div class="nav-btn">
50
+ <a href="/chat">
51
+ <button>Go to Chat β†’</button>
52
+ </a>
53
+ </div>
54
+ </div>
55
+
56
+ <script>
57
+ // ── Load Files on Page Load ───────────────────────
58
+ async function loadFiles() {
59
+ const response = await fetch("/files")
60
+ const data = await response.json()
61
+ const filesList = document.getElementById("filesList")
62
+
63
+ filesList.innerHTML = ""
64
+
65
+ if (data.files.length === 0) {
66
+ filesList.innerHTML = "<li>No files uploaded yet</li>"
67
+ return
68
+ }
69
+
70
+ data.files.forEach(file => {
71
+ filesList.innerHTML += `
72
+ <li class="file-item">
73
+ <span>πŸ“„ ${file}</span>
74
+ <button
75
+ class="delete-btn"
76
+ onclick="deleteFile('${file}')">
77
+ πŸ—‘οΈ Delete
78
+ </button>
79
+ </li>
80
+ `
81
+ })
82
+ }
83
+
84
+ // ── Upload PDF ────────────────────────────────────
85
+ document.getElementById("uploadForm").addEventListener("submit", async (e) => {
86
+ e.preventDefault()
87
+
88
+ const fileInput = document.getElementById("pdfFile")
89
+ const statusDiv = document.getElementById("uploadStatus")
90
+ const formData = new FormData()
91
+
92
+ formData.append("pdf", fileInput.files[0])
93
+ statusDiv.innerText = "Uploading..."
94
+
95
+ const response = await fetch("/upload", {
96
+ method: "POST",
97
+ body: formData
98
+ })
99
+
100
+ const data = await response.json()
101
+
102
+ if (response.ok) {
103
+ localStorage.setItem("currentFile", fileInput.files[0].name)
104
+ statusDiv.innerText = "βœ… " + data.message
105
+ statusDiv.style.color = "green"
106
+ loadFiles() // Refresh files list
107
+ } else {
108
+ statusDiv.innerText = "❌ " + data.error
109
+ statusDiv.style.color = "red"
110
+ }
111
+ })
112
+
113
+ document.getElementById("clearVectorBtn").addEventListener("click", async () => {
114
+ if (!confirm("Are you sure? This will delete ALL embeddings!")) return
115
+
116
+ const clearStatus = document.getElementById("clearStatus")
117
+ clearStatus.innerText = "Clearing..."
118
+
119
+ const response = await fetch("/clear_vectorstore", {
120
+ method: "POST"
121
+ })
122
+
123
+ const data = await response.json()
124
+
125
+ if (response.ok) {
126
+ clearStatus.innerText = "βœ… " + data.message
127
+ clearStatus.style.color = "green"
128
+ localStorage.removeItem("currentFile")
129
+ loadFiles()
130
+ } else {
131
+ clearStatus.innerText = "❌ " + data.error
132
+ clearStatus.style.color = "red"
133
+ }
134
+ })
135
+
136
+ // ── Load Files on Page Load ───────────────────────
137
+ async function deleteFile(filename) {
138
+ if (!confirm(`Are you sure you want to delete ${filename}?`)) return
139
+
140
+ const response = await fetch("/delete", {
141
+ method: "POST",
142
+ headers: { "Content-Type": "application/json" },
143
+ body: JSON.stringify({ filename: filename })
144
+ })
145
+
146
+ const data = await response.json()
147
+
148
+ if (response.ok) {
149
+ alert("βœ… " + data.message)
150
+
151
+ // Clear localStorage if deleted file was selected
152
+ if (localStorage.getItem("currentFile") === filename) {
153
+ localStorage.removeItem("currentFile")
154
+ }
155
+
156
+ loadFiles() // Refresh list
157
+ } else {
158
+ alert("❌ " + data.error)
159
+ }
160
+ }
161
+ loadFiles()
162
+ </script>
163
+
164
+ </body>
165
+ </html>
templates/login.html ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
15
+ <div class="auth-box">
16
+ <h2>Welcome Back!</h2>
17
+
18
+ {% if error %}
19
+ <div class="error-msg">❌ {{ error }}</div>
20
+ {% endif %}
21
+
22
+ <form method="POST" action="/login">
23
+ <input type="text" name="username" placeholder="Username" required />
24
+ <input type="password" name="password" placeholder="Password" required />
25
+ <button type="submit">Login</button>
26
+ </form>
27
+
28
+ <p>Don't have an account? <a href="/register">Register</a></p>
29
+ </div>
30
+ </div>
31
+ </body>
32
+ </html>
templates/register.html ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
15
+ <div class="auth-box">
16
+ <h2>Create Account</h2>
17
+
18
+ {% if error %}
19
+ <div class="error-msg">❌ {{ error }}</div>
20
+ {% endif %}
21
+
22
+ <form method="POST" action="/register">
23
+ <input type="text" name="username" placeholder="Username" required />
24
+ <input type="email" name="email" placeholder="Email" required />
25
+ <input type="password" name="password" placeholder="Password" required />
26
+ <button type="submit">Register</button>
27
+ </form>
28
+
29
+ <p>Already have an account? <a href="/login">Login</a></p>
30
+ </div>
31
+ </div>
32
+ </body>
33
+ </html>