3v324v23 commited on
Commit
dc1baed
·
0 Parent(s):

Initial commit - College Enquiry Chatbot

Browse files
Files changed (7) hide show
  1. .gitignore +6 -0
  2. .gradio/certificate.pem +31 -0
  3. app.py +669 -0
  4. diagnose.py +16 -0
  5. ingest.py +47 -0
  6. requirements.txt +0 -0
  7. start.sh +2 -0
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ venv/
2
+ .env
3
+ __pycache__/
4
+ *.pyc
5
+ feedback.json
6
+ chat_log.json
.gradio/certificate.pem ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN CERTIFICATE-----
2
+ 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
31
+ -----END CERTIFICATE-----
app.py ADDED
@@ -0,0 +1,669 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import shutil
4
+ import warnings
5
+ warnings.filterwarnings("ignore")
6
+
7
+ from dotenv import load_dotenv
8
+ from langchain_community.embeddings import HuggingFaceEmbeddings
9
+ from langchain_community.vectorstores import Chroma
10
+ from langchain_groq import ChatGroq
11
+ from langchain_core.prompts import PromptTemplate
12
+ from langchain_core.output_parsers import StrOutputParser
13
+ from langchain_community.document_loaders import PyPDFLoader
14
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
15
+ from fastapi import FastAPI, UploadFile, File, Form, Request
16
+ from fastapi.responses import HTMLResponse, RedirectResponse
17
+ from pydantic import BaseModel
18
+ from typing import List
19
+ from datetime import datetime
20
+ from collections import Counter
21
+ import uvicorn
22
+
23
+ load_dotenv()
24
+
25
+ # ── Admin password (change this!) ──
26
+ ADMIN_PASSWORD = "admin123"
27
+ CHAT_LOG_FILE = "chat_log.json"
28
+ FEEDBACK_FILE = "feedback.json"
29
+
30
+ print("🔄 Loading knowledge base...")
31
+ embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
32
+ vectordb = Chroma(persist_directory="./db", embedding_function=embeddings)
33
+ retriever = vectordb.as_retriever(search_kwargs={"k": 6})
34
+
35
+ print("🤖 Connecting to Groq LLM...")
36
+ llm = ChatGroq(
37
+ model_name="llama-3.3-70b-versatile",
38
+ temperature=0.2,
39
+ api_key=os.getenv("GROQ_API_KEY")
40
+ )
41
+
42
+ prompt = PromptTemplate.from_template("""You are a helpful and friendly college enquiry assistant.
43
+ Use ONLY the context below to answer the question.
44
+ If the answer is not in the context, say "I don't have that information, please contact the college directly."
45
+ Keep your answers clear and concise.
46
+
47
+ IMPORTANT LANGUAGE RULE:
48
+ - Detect the language of the "Current Question" below.
49
+ - If the question is in Hindi (or contains Hindi/Devanagari words), respond FULLY in Hindi.
50
+ - If the question is in English, respond in English.
51
+ - Never mix languages in a single response.
52
+ - If responding in Hindi, also translate the "I don't have that information" message to Hindi.
53
+
54
+ Context:
55
+ {context}
56
+
57
+ Conversation History:
58
+ {history}
59
+
60
+ Current Question:
61
+ {question}
62
+
63
+ Answer:""")
64
+
65
+ def format_docs(docs):
66
+ return "\n\n".join(doc.page_content for doc in docs)
67
+
68
+ def format_history(history):
69
+ if not history:
70
+ return "No previous conversation."
71
+ lines = []
72
+ for h in history:
73
+ lines.append(f"Student: {h['user']}")
74
+ lines.append(f"Assistant: {h['bot']}")
75
+ return "\n".join(lines)
76
+
77
+ def ask_with_memory(question: str, history: list) -> str:
78
+ docs = retriever.invoke(question)
79
+ context = format_docs(docs)
80
+ formatted_history = format_history(history)
81
+ chain = prompt | llm | StrOutputParser()
82
+ return chain.invoke({"context": context, "history": formatted_history, "question": question})
83
+
84
+ def rebuild_knowledge_base(pdf_path: str):
85
+ global vectordb, retriever
86
+
87
+ loader = PyPDFLoader(pdf_path)
88
+ documents = loader.load()
89
+ splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
90
+ chunks = splitter.split_documents(documents)
91
+ seen, unique_chunks = set(), []
92
+ for chunk in chunks:
93
+ text = chunk.page_content.strip()
94
+ if text not in seen:
95
+ seen.add(text)
96
+ unique_chunks.append(chunk)
97
+
98
+ # ── Delete all existing docs and re-add ──
99
+ try:
100
+ vectordb.delete_collection()
101
+ except Exception:
102
+ pass
103
+
104
+ import chromadb
105
+ from langchain_community.vectorstores import Chroma as ChromaStore
106
+ vectordb = ChromaStore.from_documents(
107
+ documents=unique_chunks,
108
+ embedding=embeddings,
109
+ persist_directory="./db"
110
+ )
111
+ vectordb.persist()
112
+ retriever = vectordb.as_retriever(search_kwargs={"k": 6})
113
+ print(f"✅ Knowledge base rebuilt with {len(unique_chunks)} unique chunks")
114
+
115
+ def load_feedback():
116
+ if os.path.exists(FEEDBACK_FILE):
117
+ with open(FEEDBACK_FILE, "r") as f:
118
+ return json.load(f)
119
+ return []
120
+
121
+ def save_feedback(data):
122
+ with open(FEEDBACK_FILE, "w") as f:
123
+ json.dump(data, f, indent=2)
124
+
125
+ def load_chat_log():
126
+ if os.path.exists(CHAT_LOG_FILE):
127
+ with open(CHAT_LOG_FILE, "r") as f:
128
+ return json.load(f)
129
+ return []
130
+
131
+ def save_chat_log(data):
132
+ with open(CHAT_LOG_FILE, "w") as f:
133
+ json.dump(data, f, indent=2)
134
+
135
+ app = FastAPI()
136
+
137
+ class Message(BaseModel):
138
+ message: str
139
+ history: List[dict] = []
140
+
141
+ class Feedback(BaseModel):
142
+ question: str
143
+ answer: str
144
+ rating: str
145
+
146
+ @app.post("/ask")
147
+ async def ask(payload: Message):
148
+ if not payload.message.strip():
149
+ return {"answer": "Please ask a question! / कृपया एक प्रश्न पूछें!"}
150
+ print(f"❓ Question: {payload.message}")
151
+ answer = ask_with_memory(payload.message, payload.history)
152
+ print(f"✅ Answer: {answer}")
153
+ log = load_chat_log()
154
+ log.append({"question": payload.message, "answer": answer, "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")})
155
+ save_chat_log(log)
156
+ return {"answer": answer}
157
+
158
+ @app.post("/feedback")
159
+ async def feedback(payload: Feedback):
160
+ data = load_feedback()
161
+ data.append({"question": payload.question, "answer": payload.answer, "rating": payload.rating, "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")})
162
+ save_feedback(data)
163
+ print(f"{'👍' if payload.rating == 'up' else '👎'} Feedback: {payload.question[:50]}")
164
+ return {"status": "saved"}
165
+
166
+ # ── Admin login page ──
167
+ @app.get("/admin", response_class=HTMLResponse)
168
+ async def admin_login():
169
+ return HTMLResponse(content="""<!DOCTYPE html>
170
+ <html lang="en">
171
+ <head>
172
+ <meta charset="UTF-8"/>
173
+ <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
174
+ <title>Admin Login</title>
175
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap" rel="stylesheet"/>
176
+ <style>
177
+ *{box-sizing:border-box;margin:0;padding:0}
178
+ body{background:#f0faf4;font-family:'Plus Jakarta Sans',sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center}
179
+ .card{background:white;border:1.5px solid #bbf7d0;border-radius:24px;padding:2.5rem 2rem;width:100%;max-width:380px;box-shadow:0 4px 24px rgba(34,197,94,.1);text-align:center}
180
+ .icon{font-size:2.5rem;margin-bottom:1rem}
181
+ h1{font-size:1.4rem;font-weight:800;color:#0a2e1a;margin-bottom:.3rem}
182
+ p{font-size:.8rem;color:#6b7280;margin-bottom:1.5rem}
183
+ input{width:100%;border:1.5px solid #bbf7d0;border-radius:12px;padding:.7rem 1rem;font-family:'Plus Jakarta Sans',sans-serif;font-size:.9rem;color:#14532d;outline:none;margin-bottom:1rem;transition:border .2s}
184
+ input:focus{border-color:#22c55e}
185
+ button{width:100%;background:#16a34a;border:none;border-radius:12px;color:white;font-family:'Plus Jakarta Sans',sans-serif;font-weight:700;font-size:.9rem;padding:.75rem;cursor:pointer;transition:all .2s}
186
+ button:hover{background:#15803d;transform:translateY(-1px)}
187
+ .err{color:#dc2626;font-size:.8rem;margin-top:.5rem;display:none}
188
+ .back{display:inline-block;margin-top:1rem;font-size:.78rem;color:#16a34a;text-decoration:none;font-weight:600}
189
+ </style>
190
+ </head>
191
+ <body>
192
+ <div class="card">
193
+ <div class="icon">🔐</div>
194
+ <h1>Admin Panel</h1>
195
+ <p>Enter your admin password to continue</p>
196
+ <form method="POST" action="/admin/login">
197
+ <input type="password" name="password" placeholder="Enter admin password" required autofocus/>
198
+ <button type="submit">Login →</button>
199
+ </form>
200
+ <div class="err" id="err">Incorrect password. Try again.</div>
201
+ <a href="/" class="back">← Back to Chatbot</a>
202
+ </div>
203
+ </body>
204
+ </html>""")
205
+
206
+ @app.post("/admin/login", response_class=HTMLResponse)
207
+ async def admin_login_post(request: Request, password: str = Form(...)):
208
+ if password != ADMIN_PASSWORD:
209
+ return HTMLResponse(content="""<!DOCTYPE html>
210
+ <html><head><meta charset="UTF-8"/>
211
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@700&display=swap" rel="stylesheet"/>
212
+ <style>*{box-sizing:border-box;margin:0;padding:0}body{background:#f0faf4;font-family:'Plus Jakarta Sans',sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center}.card{background:white;border:1.5px solid #fecaca;border-radius:24px;padding:2.5rem 2rem;width:100%;max-width:380px;text-align:center}.icon{font-size:2.5rem;margin-bottom:1rem}h1{color:#dc2626;font-size:1.2rem;margin-bottom:1rem}a{display:inline-block;background:#16a34a;color:white;padding:.6rem 1.4rem;border-radius:10px;text-decoration:none;font-weight:700;font-size:.85rem}</style>
213
+ </head><body><div class="card"><div class="icon">❌</div><h1>Incorrect Password</h1><a href="/admin">Try Again</a></div></body></html>""")
214
+ return RedirectResponse(url=f"/admin/panel?pwd={password}", status_code=303)
215
+
216
+ @app.get("/admin/panel", response_class=HTMLResponse)
217
+ async def admin_panel(pwd: str = ""):
218
+ if pwd != ADMIN_PASSWORD:
219
+ return RedirectResponse(url="/admin")
220
+
221
+ feedback_data = load_feedback()
222
+ chat_data = load_chat_log()
223
+ total_chats = len(chat_data)
224
+ thumbs_up = sum(1 for d in feedback_data if d["rating"] == "up")
225
+ thumbs_down = sum(1 for d in feedback_data if d["rating"] == "down")
226
+ total_fb = len(feedback_data)
227
+ satisfaction = round((thumbs_up / total_fb * 100) if total_fb > 0 else 0)
228
+
229
+ recent_chats = chat_data[-8:][::-1]
230
+ chat_rows = ""
231
+ for d in recent_chats:
232
+ q = d['question'][:55] + ('...' if len(d['question']) > 55 else '')
233
+ a = d['answer'][:80] + ('...' if len(d['answer']) > 80 else '')
234
+ chat_rows += f"<tr><td>{d['timestamp']}</td><td>{q}</td><td>{a}</td></tr>"
235
+
236
+ chat_table = f"<table><thead><tr><th>Time</th><th>Question</th><th>Answer</th></tr></thead><tbody>{chat_rows}</tbody></table>" if chat_rows else "<div class='empty'>No chats yet!</div>"
237
+
238
+ pdf_exists = os.path.exists("data/college_info.pdf")
239
+ pdf_status = "✅ college_info.pdf found" if pdf_exists else "⚠️ No PDF found in data/"
240
+ db_exists = os.path.exists("./db") and len(os.listdir("./db")) > 0
241
+ db_status = "✅ Vector database ready" if db_exists else "⚠️ Database not built"
242
+
243
+ return HTMLResponse(content=f"""<!DOCTYPE html>
244
+ <html lang="en">
245
+ <head>
246
+ <meta charset="UTF-8"/>
247
+ <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
248
+ <title>Admin Panel</title>
249
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap" rel="stylesheet"/>
250
+ <style>
251
+ *{{box-sizing:border-box;margin:0;padding:0}}
252
+ body{{background:#f0faf4;font-family:'Plus Jakarta Sans',sans-serif;padding:2rem 1rem}}
253
+ .header{{background:#0a2e1a;color:#4ade80;padding:1.5rem 2rem;border-radius:16px;margin-bottom:1.5rem;display:flex;justify-content:space-between;align-items:center}}
254
+ .header h1{{font-size:1.4rem;font-weight:800}}
255
+ .header-links{{display:flex;gap:.6rem}}
256
+ .hlink{{background:rgba(74,222,128,.15);border:1px solid rgba(74,222,128,.3);border-radius:8px;color:#4ade80;font-size:.72rem;font-weight:700;padding:.35rem .8rem;text-decoration:none;transition:all .2s}}
257
+ .hlink:hover{{background:rgba(74,222,128,.25)}}
258
+ .grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:1rem;margin-bottom:1.5rem}}
259
+ .stat{{background:white;border:1.5px solid #bbf7d0;border-radius:16px;padding:1.2rem;text-align:center;box-shadow:0 2px 12px rgba(34,197,94,.08)}}
260
+ .stat .val{{font-size:2rem;font-weight:800;color:#16a34a}}
261
+ .stat .lbl{{font-size:.72rem;color:#6b7280;font-weight:600;text-transform:uppercase;letter-spacing:.08em;margin-top:.3rem}}
262
+ .section{{background:white;border:1.5px solid #bbf7d0;border-radius:16px;padding:1.4rem;margin-bottom:1.5rem}}
263
+ .section h2{{font-size:.85rem;font-weight:700;color:#16a34a;text-transform:uppercase;letter-spacing:.08em;margin-bottom:1rem}}
264
+ .status-row{{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1rem}}
265
+ .status-badge{{background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:.4rem .8rem;font-size:.8rem;color:#14532d;font-weight:600}}
266
+ .upload-form{{display:flex;flex-direction:column;gap:.8rem}}
267
+ .file-input{{border:2px dashed #bbf7d0;border-radius:12px;padding:1.5rem;text-align:center;cursor:pointer;color:#16a34a;font-size:.85rem;font-weight:600;transition:all .2s}}
268
+ .file-input:hover{{border-color:#22c55e;background:#f0fdf4}}
269
+ .btn{{border:none;border-radius:12px;font-family:'Plus Jakarta Sans',sans-serif;font-weight:700;font-size:.85rem;padding:.65rem 1.3rem;cursor:pointer;transition:all .2s;white-space:nowrap}}
270
+ .btn-green{{background:#16a34a;color:white;box-shadow:0 2px 10px rgba(22,163,74,.3)}}
271
+ .btn-green:hover{{background:#15803d;transform:translateY(-1px)}}
272
+ .btn-red{{background:white;border:1.5px solid #fecaca;color:#dc2626}}
273
+ .btn-red:hover{{background:#fee2e2}}
274
+ .btn-row{{display:flex;gap:.7rem;flex-wrap:wrap}}
275
+ table{{width:100%;border-collapse:collapse;font-size:.82rem}}
276
+ th{{background:#f0fdf4;color:#16a34a;font-weight:700;padding:.6rem .8rem;text-align:left;font-size:.72rem;text-transform:uppercase}}
277
+ td{{padding:.55rem .8rem;border-top:1px solid #f0fdf4;color:#374151;vertical-align:top;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}
278
+ .empty{{text-align:center;color:#9ca3af;padding:2rem;font-size:.88rem}}
279
+ .alert{{background:#dcfce7;border:1px solid #86efac;border-radius:10px;padding:.8rem 1rem;font-size:.82rem;color:#14532d;font-weight:600;display:none;margin-top:.5rem}}
280
+ .alert.show{{display:block}}
281
+ </style>
282
+ </head>
283
+ <body>
284
+ <div class="header">
285
+ <h1>🛠️ Admin Panel</h1>
286
+ <div class="header-links">
287
+ <a href="/dashboard?pwd={pwd}" class="hlink">📊 Dashboard</a>
288
+ <a href="/" class="hlink">← Chatbot</a>
289
+ </div>
290
+ </div>
291
+
292
+ <!-- Stats -->
293
+ <div class="grid">
294
+ <div class="stat"><div class="val">{total_chats}</div><div class="lbl">Total Chats</div></div>
295
+ <div class="stat"><div class="val" style="color:#16a34a">👍 {thumbs_up}</div><div class="lbl">Helpful</div></div>
296
+ <div class="stat"><div class="val" style="color:#dc2626">👎 {thumbs_down}</div><div class="lbl">Not Helpful</div></div>
297
+ <div class="stat"><div class="val">{satisfaction}%</div><div class="lbl">Satisfaction</div></div>
298
+ </div>
299
+
300
+ <!-- Knowledge Base Management -->
301
+ <div class="section">
302
+ <h2>📚 Knowledge Base Management</h2>
303
+ <div class="status-row">
304
+ <div class="status-badge">{pdf_status}</div>
305
+ <div class="status-badge">{db_status}</div>
306
+ </div>
307
+ <div class="upload-form">
308
+ <p style="font-size:.82rem;color:#6b7280;margin-bottom:.5rem">Upload a new college PDF to update the knowledge base. This will automatically rebuild the vector database.</p>
309
+ <form method="POST" action="/admin/upload?pwd={pwd}" enctype="multipart/form-data">
310
+ <input type="file" name="file" accept=".pdf" required style="border:2px dashed #bbf7d0;border-radius:12px;padding:1rem;width:100%;font-size:.85rem;color:#14532d;background:#f0fdf4;cursor:pointer;margin-bottom:.5rem"/>
311
+ <div class="btn-row" style="margin-top:.5rem">
312
+ <button type="submit" class="btn btn-green">📤 Upload & Rebuild Knowledge Base</button>
313
+ </div>
314
+ </form>
315
+ <div class="alert" id="upload-alert">✅ PDF uploaded and knowledge base rebuilt successfully!</div>
316
+ </div>
317
+ </div>
318
+
319
+ <!-- Data Management -->
320
+ <div class="section">
321
+ <h2>🗂️ Data Management</h2>
322
+ <p style="font-size:.82rem;color:#6b7280;margin-bottom:1rem">Manage stored feedback and chat logs. These actions cannot be undone.</p>
323
+ <div class="btn-row">
324
+ <form method="POST" action="/admin/clear-feedback?pwd={pwd}" onsubmit="return confirm('Clear all feedback data?')">
325
+ <button type="submit" class="btn btn-red">🗑️ Clear Feedback Data</button>
326
+ </form>
327
+ <form method="POST" action="/admin/clear-chats?pwd={pwd}" onsubmit="return confirm('Clear all chat logs?')">
328
+ <button type="submit" class="btn btn-red">🗑️ Clear Chat Logs</button>
329
+ </form>
330
+ </div>
331
+ </div>
332
+
333
+ <!-- Recent Chat Logs -->
334
+ <div class="section">
335
+ <h2>💬 Recent Chat Logs</h2>
336
+ {chat_table}
337
+ </div>
338
+
339
+ <script>
340
+ // Show upload success if redirected with success param
341
+ if(window.location.search.includes('success=1')){{
342
+ const a=document.getElementById('upload-alert');a.classList.add('show');
343
+ setTimeout(()=>a.classList.remove('show'),4000);
344
+ }}
345
+ </script>
346
+ </body>
347
+ </html>""")
348
+
349
+ @app.post("/admin/upload")
350
+ async def admin_upload(pwd: str = "", file: UploadFile = File(default=None)):
351
+ if pwd != ADMIN_PASSWORD:
352
+ return RedirectResponse(url="/admin")
353
+ if file is None or file.filename == "":
354
+ return RedirectResponse(url=f"/admin/panel?pwd={pwd}&error=1", status_code=303)
355
+ os.makedirs("data", exist_ok=True)
356
+ pdf_path = "data/college_info.pdf"
357
+ with open(pdf_path, "wb") as f:
358
+ content = await file.read()
359
+ f.write(content)
360
+ print(f"📄 New PDF uploaded: {file.filename}")
361
+ rebuild_knowledge_base(pdf_path)
362
+ return RedirectResponse(url=f"/admin/panel?pwd={pwd}&success=1", status_code=303)
363
+
364
+ @app.post("/admin/clear-feedback")
365
+ async def clear_feedback(pwd: str = ""):
366
+ if pwd != ADMIN_PASSWORD:
367
+ return RedirectResponse(url="/admin")
368
+ save_feedback([])
369
+ print("🗑️ Feedback data cleared")
370
+ return RedirectResponse(url=f"/admin/panel?pwd={pwd}", status_code=303)
371
+
372
+ @app.post("/admin/clear-chats")
373
+ async def clear_chats(pwd: str = ""):
374
+ if pwd != ADMIN_PASSWORD:
375
+ return RedirectResponse(url="/admin")
376
+ save_chat_log([])
377
+ print("🗑️ Chat logs cleared")
378
+ return RedirectResponse(url=f"/admin/panel?pwd={pwd}", status_code=303)
379
+
380
+ @app.get("/dashboard", response_class=HTMLResponse)
381
+ async def dashboard():
382
+ data = load_feedback()
383
+ total = len(data)
384
+ thumbs_up = sum(1 for d in data if d["rating"] == "up")
385
+ thumbs_down = sum(1 for d in data if d["rating"] == "down")
386
+ satisfaction = round((thumbs_up / total * 100) if total > 0 else 0)
387
+ sat_color = "#16a34a" if satisfaction >= 70 else "#f59e0b" if satisfaction >= 40 else "#dc2626"
388
+ recent = data[-10:][::-1]
389
+ rows = ""
390
+ for d in recent:
391
+ emoji = "👍" if d["rating"] == "up" else "👎"
392
+ color = "#16a34a" if d["rating"] == "up" else "#dc2626"
393
+ q = d['question'][:60] + ('...' if len(d['question']) > 60 else '')
394
+ rows += f"<tr><td>{d['timestamp']}</td><td>{q}</td><td style='color:{color};font-size:1.2rem'>{emoji}</td></tr>"
395
+ q_counts = Counter(d["question"] for d in data)
396
+ top_questions = q_counts.most_common(5)
397
+ top_q_rows = ""
398
+ for q, count in top_questions:
399
+ top_q_rows += f"<li><span>{q[:55]}{'...' if len(q)>55 else ''}</span><b>{count}x</b></li>"
400
+ table_html = f"<table><thead><tr><th>Time</th><th>Question</th><th>Rating</th></tr></thead><tbody>{rows}</tbody></table>" if rows else "<div class='empty'>No feedback yet!</div>"
401
+ top_html = f"<ul class='top-q-list'>{top_q_rows}</ul>" if top_q_rows else "<div class='empty'>No data yet!</div>"
402
+ return HTMLResponse(content=f"""<!DOCTYPE html>
403
+ <html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1.0"/>
404
+ <title>Dashboard</title>
405
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap" rel="stylesheet"/>
406
+ <style>
407
+ *{{box-sizing:border-box;margin:0;padding:0}}body{{background:#f0faf4;font-family:'Plus Jakarta Sans',sans-serif;padding:2rem 1rem}}
408
+ .header{{background:#0a2e1a;color:#4ade80;text-align:center;padding:1.5rem;border-radius:16px;margin-bottom:1.5rem}}
409
+ .header h1{{font-size:1.6rem;font-weight:800}}.header p{{color:#34d399;font-size:.75rem;letter-spacing:.1em;text-transform:uppercase;margin-top:.3rem}}
410
+ .back-btn{{display:inline-block;margin-bottom:1rem;background:white;border:1.5px solid #bbf7d0;border-radius:10px;color:#16a34a;font-weight:700;font-size:.82rem;padding:.4rem 1rem;text-decoration:none}}
411
+ .back-btn:hover{{background:#16a34a;color:white}}
412
+ .stats-grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem;margin-bottom:1.5rem}}
413
+ .stat-card{{background:white;border:1.5px solid #bbf7d0;border-radius:16px;padding:1.2rem;text-align:center;box-shadow:0 2px 12px rgba(34,197,94,.08)}}
414
+ .stat-card .value{{font-size:2.2rem;font-weight:800;color:#16a34a}}.stat-card .label{{font-size:.75rem;color:#6b7280;font-weight:600;text-transform:uppercase;letter-spacing:.08em;margin-top:.3rem}}
415
+ .section{{background:white;border:1.5px solid #bbf7d0;border-radius:16px;padding:1.4rem;margin-bottom:1.5rem}}
416
+ .section h2{{font-size:.85rem;font-weight:700;color:#16a34a;text-transform:uppercase;letter-spacing:.08em;margin-bottom:1rem}}
417
+ table{{width:100%;border-collapse:collapse;font-size:.85rem}}th{{background:#f0fdf4;color:#16a34a;font-weight:700;padding:.6rem .8rem;text-align:left;font-size:.75rem;text-transform:uppercase}}
418
+ td{{padding:.6rem .8rem;border-top:1px solid #f0fdf4;color:#374151;vertical-align:top}}
419
+ .top-q-list{{list-style:none;display:flex;flex-direction:column;gap:.6rem}}
420
+ .top-q-list li{{display:flex;justify-content:space-between;align-items:center;padding:.6rem .8rem;background:#f0fdf4;border-radius:10px;font-size:.85rem}}
421
+ .top-q-list li b{{color:#16a34a;font-weight:800;white-space:nowrap;margin-left:.5rem}}
422
+ .empty{{text-align:center;color:#9ca3af;padding:2rem;font-size:.9rem}}
423
+ .progress-bar{{background:#dcfce7;border-radius:99px;height:10px;margin-top:.5rem;overflow:hidden}}
424
+ .progress-fill{{background:{sat_color};height:100%;border-radius:99px;width:{satisfaction}%}}
425
+ </style></head><body>
426
+ <a href="/" class="back-btn">← Back to Chatbot</a>
427
+ <div class="header"><h1>📊 Analytics Dashboard</h1><p>College Enquiry Assistant — Live Feedback Stats</p></div>
428
+ <div class="stats-grid">
429
+ <div class="stat-card"><div class="value">{total}</div><div class="label">Total Responses</div></div>
430
+ <div class="stat-card"><div class="value" style="color:#16a34a">👍 {thumbs_up}</div><div class="label">Helpful</div></div>
431
+ <div class="stat-card"><div class="value" style="color:#dc2626">👎 {thumbs_down}</div><div class="label">Not Helpful</div></div>
432
+ <div class="stat-card"><div class="value" style="color:{sat_color}">{satisfaction}%</div><div class="label">Satisfaction</div><div class="progress-bar"><div class="progress-fill"></div></div></div>
433
+ </div>
434
+ <div class="section"><h2>🔥 Most Asked Questions</h2>{top_html}</div>
435
+ <div class="section"><h2>🕐 Recent Feedback</h2>{table_html}</div>
436
+ </body></html>""")
437
+
438
+ @app.get("/", response_class=HTMLResponse)
439
+ async def home():
440
+ return HTMLResponse(content="""<!DOCTYPE html>
441
+ <html lang="en">
442
+ <head>
443
+ <meta charset="UTF-8"/>
444
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
445
+ <title>College Enquiry Assistant</title>
446
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"/>
447
+ <style>
448
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
449
+ body{background:#f0faf4;font-family:'Plus Jakarta Sans',sans-serif;min-height:100vh;display:flex;flex-direction:column;align-items:center}
450
+ .header{width:100%;background:#0a2e1a;padding:1.8rem 2rem;text-align:center;border-radius:0 0 28px 28px;margin-bottom:1.5rem;position:relative}
451
+ .header h1{font-size:2rem;font-weight:800;color:#4ade80;letter-spacing:-.02em;margin-bottom:.3rem}
452
+ .header p{color:#34d399;font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;opacity:.85}
453
+ .header-links{position:absolute;top:1rem;right:1.2rem;display:flex;gap:.5rem}
454
+ .hlink{background:rgba(74,222,128,.15);border:1px solid rgba(74,222,128,.3);border-radius:8px;color:#4ade80;font-size:.68rem;font-weight:700;padding:.3rem .7rem;text-decoration:none;letter-spacing:.05em;transition:all .2s}
455
+ .hlink:hover{background:rgba(74,222,128,.25)}
456
+ .container{width:100%;max-width:780px;padding:0 1rem 2rem;display:flex;flex-direction:column;gap:1rem}
457
+ .chat-window{background:white;border:1.5px solid #bbf7d0;border-radius:20px;height:460px;overflow-y:auto;padding:1.2rem;display:flex;flex-direction:column;gap:1rem;box-shadow:0 4px 24px rgba(34,197,94,.08)}
458
+ .chat-window::-webkit-scrollbar{width:5px}.chat-window::-webkit-scrollbar-track{background:#f0fdf4}.chat-window::-webkit-scrollbar-thumb{background:#86efac;border-radius:4px}
459
+ .empty-state{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.6rem}
460
+ .empty-state .icon{font-size:2.5rem}
461
+ .empty-state p{font-size:.9rem;font-weight:500;color:#4ade80}
462
+ .msg-row{display:flex;align-items:flex-end;gap:.6rem}.msg-row.user{flex-direction:row-reverse}
463
+ .avatar{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.9rem;flex-shrink:0}
464
+ .avatar.bot{background:#0a2e1a}.avatar.user{background:#16a34a}
465
+ .bubble-wrap{display:flex;flex-direction:column;gap:.3rem;max-width:75%}.msg-row.user .bubble-wrap{align-items:flex-end}
466
+ .bubble{padding:.8rem 1.1rem;font-size:.93rem;line-height:1.7}
467
+ .bubble.user{background:#0a2e1a;color:white;border-radius:18px 18px 4px 18px}
468
+ .bubble.bot{background:#f0fdf4;color:#14532d;border-radius:18px 18px 18px 4px;border-left:3px solid #22c55e}
469
+ .feedback-row{display:flex;gap:.4rem;padding-left:.3rem;align-items:center}
470
+ .fb-btn{background:white;border:1px solid #bbf7d0;border-radius:8px;font-size:.85rem;padding:.2rem .6rem;cursor:pointer;transition:all .2s;color:#6b7280}
471
+ .fb-btn:hover{transform:scale(1.15)}.fb-btn.up.selected{background:#dcfce7;border-color:#16a34a}.fb-btn.down.selected{background:#fee2e2;border-color:#dc2626}
472
+ .fb-thanks{font-size:.72rem;color:#16a34a;font-weight:600;display:none}
473
+ .typing-bubble{background:#f0fdf4;border-radius:18px 18px 18px 4px;border-left:3px solid #22c55e;padding:.8rem 1.2rem;display:flex;gap:5px;align-items:center}
474
+ .dot{width:7px;height:7px;background:#22c55e;border-radius:50%;animation:bounce 1.2s infinite}
475
+ .dot:nth-child(2){animation-delay:.2s}.dot:nth-child(3){animation-delay:.4s}
476
+ @keyframes bounce{0%,80%,100%{transform:translateY(0)}40%{transform:translateY(-6px)}}
477
+ .input-area{background:white;border:1.5px solid #22c55e;border-radius:16px;padding:.5rem .5rem .5rem 1rem;display:flex;align-items:center;gap:.6rem;box-shadow:0 2px 16px rgba(34,197,94,.1)}
478
+ .input-area textarea{flex:1;border:none;outline:none;background:transparent;color:#14532d;font-family:'Plus Jakarta Sans',sans-serif;font-size:.95rem;resize:none;height:44px;padding-top:.6rem;line-height:1.5}
479
+ .input-area textarea::placeholder{color:#4ade80;font-weight:500}
480
+ .btn-send{background:#16a34a;border:none;border-radius:12px;color:white;font-family:'Plus Jakarta Sans',sans-serif;font-weight:700;font-size:.88rem;padding:.65rem 1.3rem;cursor:pointer;transition:all .2s;box-shadow:0 2px 10px rgba(22,163,74,.3);white-space:nowrap}
481
+ .btn-send:hover{background:#15803d;transform:translateY(-1px)}.btn-send:disabled{background:#86efac;cursor:not-allowed;transform:none}
482
+ .btn-clear{background:white;border:1.5px solid #bbf7d0;border-radius:12px;color:#16a34a;font-family:'Plus Jakarta Sans',sans-serif;font-weight:600;font-size:.85rem;padding:.65rem 1rem;cursor:pointer;transition:all .2s}
483
+ .btn-clear:hover{border-color:#f87171;color:#dc2626;background:#fff5f5}
484
+ .btn-voice{background:white;border:1.5px solid #bbf7d0;border-radius:12px;font-size:1.1rem;padding:.65rem .9rem;cursor:pointer;transition:all .2s;white-space:nowrap;line-height:1}
485
+ .btn-voice:hover{background:#f0fdf4;border-color:#22c55e}
486
+ .btn-voice.listening{background:#16a34a;border-color:#16a34a;animation:pulse 1s infinite}
487
+ @keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(22,163,74,.4)}50%{box-shadow:0 0 0 8px rgba(22,163,74,0)}}
488
+ .voice-status{display:none;text-align:center;font-size:.78rem;color:#16a34a;font-weight:600;letter-spacing:.05em;padding:.3rem 0}
489
+ .voice-status.visible{display:block}
490
+ .bottom-row{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.5rem}
491
+ .speaker-row{display:flex;align-items:center;gap:.5rem;font-size:.78rem;color:#4ade80;font-weight:600;letter-spacing:.05em;text-transform:uppercase}
492
+ .toggle-switch{position:relative;width:36px;height:20px}.toggle-switch input{opacity:0;width:0;height:0}
493
+ .toggle-slider{position:absolute;cursor:pointer;inset:0;background:#bbf7d0;border-radius:20px;transition:.3s}
494
+ .toggle-slider:before{content:"";position:absolute;width:14px;height:14px;left:3px;bottom:3px;background:white;border-radius:50%;transition:.3s}
495
+ input:checked+.toggle-slider{background:#16a34a}input:checked+.toggle-slider:before{transform:translateX(16px)}
496
+ .memory-badge{font-size:.72rem;color:#16a34a;font-weight:700;background:#dcfce7;border:1px solid #bbf7d0;border-radius:50px;padding:.25rem .7rem;letter-spacing:.04em}
497
+ .lang-row{display:flex;align-items:center;gap:.5rem}
498
+ .lang-btn{background:white;border:1.5px solid #bbf7d0;border-radius:8px;font-size:.78rem;font-weight:700;padding:.3rem .7rem;cursor:pointer;transition:all .2s;color:#6b7280}
499
+ .lang-btn.active{background:#16a34a;border-color:#16a34a;color:white}
500
+ .examples-label{color:#16a34a;font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;margin-bottom:.5rem}
501
+ .examples-row{display:flex;flex-wrap:wrap;gap:.5rem}
502
+ .chip{background:white;border:1.5px solid #bbf7d0;border-radius:50px;color:#15803d;font-family:'Plus Jakarta Sans',sans-serif;font-size:.8rem;font-weight:600;padding:.4rem 1rem;cursor:pointer;transition:all .2s}
503
+ .chip:hover{background:#16a34a;border-color:#16a34a;color:white;transform:translateY(-1px)}
504
+ </style>
505
+ </head>
506
+ <body>
507
+ <div class="header">
508
+ <h1>🎓 College Enquiry Assistant</h1>
509
+ <p>Powered by Groq &nbsp;·&nbsp; LLaMA 3.3 &nbsp;·&nbsp; RAG</p>
510
+ <div class="header-links">
511
+ <a href="/dashboard" class="hlink">📊 Dashboard</a>
512
+ <a href="/admin" class="hlink">🛠️ Admin</a>
513
+ </div>
514
+ </div>
515
+ <div class="container">
516
+ <div class="chat-window" id="chat-window">
517
+ <div class="empty-state" id="empty-state">
518
+ <div class="icon">🎓</div>
519
+ <p id="empty-text">Ask me anything about the college</p>
520
+ </div>
521
+ </div>
522
+ <div class="voice-status" id="voice-status">🎤 Listening... / सुन रहा हूँ...</div>
523
+ <div class="input-area">
524
+ <textarea id="msg-input" placeholder="Ask about courses, fees, admissions..." onkeydown="handleKey(event)"></textarea>
525
+ <button class="btn-voice" id="voice-btn" onclick="toggleVoice()" title="Click to speak">🎤</button>
526
+ <button class="btn-send" id="send-btn" onclick="sendMessage()">Send ➤</button>
527
+ <button class="btn-clear" onclick="clearChat()">✕</button>
528
+ </div>
529
+ <div class="bottom-row">
530
+ <div class="speaker-row">
531
+ <label class="toggle-switch"><input type="checkbox" id="speaker-toggle" checked><span class="toggle-slider"></span></label>
532
+ 🔊 Read aloud
533
+ </div>
534
+ <div class="lang-row">
535
+ <span style="font-size:.72rem;color:#4ade80;font-weight:700;text-transform:uppercase;letter-spacing:.05em">🌐 Lang:</span>
536
+ <button class="lang-btn active" id="btn-en" onclick="setLang('en')">EN</button>
537
+ <button class="lang-btn" id="btn-hi" onclick="setLang('hi')">हि</button>
538
+ </div>
539
+ <div class="memory-badge" id="memory-badge">🧠 Memory: 0 exchanges</div>
540
+ </div>
541
+ <div>
542
+ <div class="examples-label" id="examples-label">✦ Quick questions</div>
543
+ <div class="examples-row" id="examples-row">
544
+ <button class="chip" onclick="useExample(this)">What courses are offered?</button>
545
+ <button class="chip" onclick="useExample(this)">What are the admission requirements?</button>
546
+ <button class="chip" onclick="useExample(this)">What is the fee structure?</button>
547
+ <button class="chip" onclick="useExample(this)">Is hostel facility available?</button>
548
+ <button class="chip" onclick="useExample(this)">What is the college address?</button>
549
+ </div>
550
+ </div>
551
+ </div>
552
+ <script>
553
+ const chatWindow=document.getElementById('chat-window');
554
+ const msgInput=document.getElementById('msg-input');
555
+ const sendBtn=document.getElementById('send-btn');
556
+ const emptyState=document.getElementById('empty-state');
557
+ const voiceStatus=document.getElementById('voice-status');
558
+ const speakerToggle=document.getElementById('speaker-toggle');
559
+ const memoryBadge=document.getElementById('memory-badge');
560
+ let conversationHistory=[],lastQuestion='',currentLang='en';
561
+
562
+ const langConfig={
563
+ en:{placeholder:'Ask about courses, fees, admissions, hostel...',emptyText:'Ask me anything about the college',examplesLabel:'✦ Quick questions',voiceStatus:'🎤 Listening... speak your question',chips:['What courses are offered?','What are the admission requirements?','What is the fee structure?','Is hostel facility available?','What is the college address?'],speechLang:'en-IN',memoryText:(n)=>`🧠 Memory: ${n} exchange${n!==1?'s':''}`},
564
+ hi:{placeholder:'कोर्स, फीस, एडमिशन के बारे में पूछें...',emptyText:'कॉलेज के बारे में कुछ भी पूछें',examplesLabel:'✦ जल्दी सवाल',voiceStatus:'🎤 सुन रहा हूँ... बोलें',chips:['कौन से कोर्स उपलब्ध हैं?','एडमिशन की योग्यता क्या है?','फीस कितनी है?','क्या हॉस्टल की सुविधा है?','कॉलेज का पता क्या है?'],speechLang:'hi-IN',memoryText:(n)=>`🧠 याददाश्त: ${n} बातचीत`}
565
+ };
566
+
567
+ function setLang(lang){
568
+ currentLang=lang;const cfg=langConfig[lang];
569
+ document.getElementById('btn-en').classList.toggle('active',lang==='en');
570
+ document.getElementById('btn-hi').classList.toggle('active',lang==='hi');
571
+ msgInput.placeholder=cfg.placeholder;
572
+ document.getElementById('empty-text').textContent=cfg.emptyText;
573
+ document.getElementById('examples-label').textContent=cfg.examplesLabel;
574
+ voiceStatus.textContent=cfg.voiceStatus;
575
+ const row=document.getElementById('examples-row');row.innerHTML='';
576
+ cfg.chips.forEach(c=>{const btn=document.createElement('button');btn.className='chip';btn.textContent=c;btn.onclick=()=>useExample(btn);row.appendChild(btn);});
577
+ if(recognition)recognition.lang=cfg.speechLang;
578
+ updateMemoryBadge();
579
+ }
580
+
581
+ function updateMemoryBadge(){const n=conversationHistory.length;memoryBadge.textContent=langConfig[currentLang].memoryText(n);}
582
+ function handleKey(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMessage();}}
583
+ function useExample(btn){msgInput.value=btn.textContent;msgInput.focus();}
584
+
585
+ function clearChat(){
586
+ chatWindow.innerHTML='';chatWindow.appendChild(emptyState);emptyState.style.display='flex';
587
+ conversationHistory=[];lastQuestion='';updateMemoryBadge();
588
+ window.speechSynthesis&&window.speechSynthesis.cancel();
589
+ }
590
+
591
+ function appendMessage(role,text){
592
+ emptyState.style.display='none';
593
+ const row=document.createElement('div');row.className=`msg-row ${role}`;
594
+ const avatar=document.createElement('div');avatar.className=`avatar ${role}`;avatar.textContent=role==='bot'?'🎓':'🧑';
595
+ const wrap=document.createElement('div');wrap.className='bubble-wrap';
596
+ const bubble=document.createElement('div');bubble.className=`bubble ${role}`;bubble.textContent=text;
597
+ wrap.appendChild(bubble);
598
+ if(role==='bot'){
599
+ const fbRow=document.createElement('div');fbRow.className='feedback-row';
600
+ const upBtn=document.createElement('button');upBtn.className='fb-btn up';upBtn.textContent='👍';
601
+ const downBtn=document.createElement('button');downBtn.className='fb-btn down';downBtn.textContent='👎';
602
+ const thanks=document.createElement('span');thanks.className='fb-thanks';thanks.textContent=currentLang==='hi'?'धन्यवाद!':'Thanks!';
603
+ const q=lastQuestion;
604
+ upBtn.onclick=()=>submitFeedback(q,text,'up',upBtn,downBtn,thanks);
605
+ downBtn.onclick=()=>submitFeedback(q,text,'down',upBtn,downBtn,thanks);
606
+ fbRow.appendChild(upBtn);fbRow.appendChild(downBtn);fbRow.appendChild(thanks);wrap.appendChild(fbRow);
607
+ }
608
+ row.appendChild(avatar);row.appendChild(wrap);chatWindow.appendChild(row);chatWindow.scrollTop=chatWindow.scrollHeight;
609
+ }
610
+
611
+ async function submitFeedback(question,answer,rating,upBtn,downBtn,thanks){
612
+ upBtn.disabled=true;downBtn.disabled=true;
613
+ upBtn.classList.toggle('selected',rating==='up');downBtn.classList.toggle('selected',rating==='down');
614
+ thanks.style.display='inline';
615
+ await fetch('/feedback',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question,answer,rating})});
616
+ }
617
+
618
+ function showTyping(){
619
+ emptyState.style.display='none';
620
+ const row=document.createElement('div');row.className='msg-row bot';row.id='typing-row';
621
+ const avatar=document.createElement('div');avatar.className='avatar bot';avatar.textContent='🎓';
622
+ const typing=document.createElement('div');typing.className='typing-bubble';
623
+ typing.innerHTML='<div class="dot"></div><div class="dot"></div><div class="dot"></div>';
624
+ row.appendChild(avatar);row.appendChild(typing);chatWindow.appendChild(row);chatWindow.scrollTop=chatWindow.scrollHeight;
625
+ }
626
+
627
+ function removeTyping(){const t=document.getElementById('typing-row');if(t)t.remove();}
628
+
629
+ async function sendMessage(){
630
+ const text=msgInput.value.trim();if(!text)return;
631
+ lastQuestion=text;msgInput.value='';sendBtn.disabled=true;
632
+ appendMessage('user',text);showTyping();
633
+ try{
634
+ const res=await fetch('/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:text,history:conversationHistory})});
635
+ const data=await res.json();removeTyping();appendMessage('bot',data.answer);
636
+ conversationHistory.push({user:text,bot:data.answer});
637
+ if(conversationHistory.length>6)conversationHistory.shift();
638
+ updateMemoryBadge();
639
+ if(speakerToggle.checked)speakAnswer(data.answer);
640
+ }catch(err){removeTyping();appendMessage('bot','⚠️ Something went wrong. / कुछ गलत हो गया।');}
641
+ finally{sendBtn.disabled=false;msgInput.focus();}
642
+ }
643
+
644
+ const SpeechRecognition=window.SpeechRecognition||window.webkitSpeechRecognition;
645
+ let recognition=null,isListening=false;
646
+ if(SpeechRecognition){
647
+ recognition=new SpeechRecognition();recognition.continuous=false;recognition.interimResults=false;
648
+ recognition.lang=langConfig[currentLang].speechLang;
649
+ recognition.onresult=function(e){const t=e.results[0][0].transcript;msgInput.value=t;stopListening();sendMessage();};
650
+ recognition.onerror=function(){stopListening();};recognition.onend=function(){stopListening();};
651
+ }
652
+
653
+ function toggleVoice(){if(!recognition){alert('Use Chrome or Edge for voice.');return;}isListening?stopListening():startListening();}
654
+ function startListening(){isListening=true;if(recognition)recognition.lang=langConfig[currentLang].speechLang;const b=document.getElementById('voice-btn');b.classList.add('listening');b.textContent='🔴';voiceStatus.classList.add('visible');msgInput.placeholder=langConfig[currentLang].voiceStatus;recognition.start();}
655
+ function stopListening(){isListening=false;const b=document.getElementById('voice-btn');b.classList.remove('listening');b.textContent='🎤';voiceStatus.classList.remove('visible');msgInput.placeholder=langConfig[currentLang].placeholder;try{recognition.stop();}catch(e){}}
656
+
657
+ function speakAnswer(text){
658
+ if(!window.speechSynthesis)return;window.speechSynthesis.cancel();
659
+ const u=new SpeechSynthesisUtterance(text);
660
+ const hasHindi=/[\u0900-\u097F]/.test(text);
661
+ u.lang=hasHindi?'hi-IN':'en-IN';u.rate=0.95;u.pitch=1;
662
+ window.speechSynthesis.speak(u);
663
+ }
664
+ </script>
665
+ </body>
666
+ </html>""")
667
+
668
+ print("🚀 Launching at http://localhost:8000")
669
+ uvicorn.run(app, host="0.0.0.0", port=8000)
diagnose.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_community.embeddings import HuggingFaceEmbeddings
2
+ from langchain_community.vectorstores import Chroma
3
+
4
+ embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
5
+ vectordb = Chroma(persist_directory="./db", embedding_function=embeddings)
6
+ retriever = vectordb.as_retriever(search_kwargs={"k": 5})
7
+
8
+ # Test with a question that the bot fails to answer
9
+ query = "what is the name of college in etawah campus?" # change this to a failing question
10
+ docs = retriever.invoke(query)
11
+
12
+ print(f"Retrieved {len(docs)} chunks:\n")
13
+ for i, doc in enumerate(docs):
14
+ print(f"--- Chunk {i+1} ---")
15
+ print(doc.page_content)
16
+ print()
ingest.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import warnings
3
+ warnings.filterwarnings("ignore")
4
+ from dotenv import load_dotenv
5
+ from langchain_community.document_loaders import PyPDFLoader
6
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
7
+ from langchain_community.embeddings import HuggingFaceEmbeddings
8
+ from langchain_community.vectorstores import Chroma
9
+
10
+ load_dotenv()
11
+
12
+ print("📄 Loading your college PDF...")
13
+ loader = PyPDFLoader("data/college_info.pdf")
14
+ documents = loader.load()
15
+ print(f"✅ Loaded {len(documents)} pages")
16
+
17
+ print("✂️ Splitting into chunks...")
18
+ splitter = RecursiveCharacterTextSplitter(
19
+ chunk_size=1000, # increased from 500
20
+ chunk_overlap=100 # increased from 50
21
+ )
22
+ chunks = splitter.split_documents(documents)
23
+ print(f"📦 Created {len(chunks)} chunks before deduplication")
24
+
25
+ # ── Remove duplicate chunks ──
26
+ seen = set()
27
+ unique_chunks = []
28
+ for chunk in chunks:
29
+ text = chunk.page_content.strip()
30
+ if text not in seen:
31
+ seen.add(text)
32
+ unique_chunks.append(chunk)
33
+
34
+ print(f"✅ {len(unique_chunks)} unique chunks after deduplication (removed {len(chunks) - len(unique_chunks)} duplicates)")
35
+ chunks = unique_chunks
36
+
37
+ print("🧠 Loading embedding model...")
38
+ embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
39
+
40
+ print("💾 Storing chunks in ChromaDB...")
41
+ vectordb = Chroma.from_documents(
42
+ documents=chunks,
43
+ embedding=embeddings,
44
+ persist_directory="./db"
45
+ )
46
+ vectordb.persist()
47
+ print("✅ All done! Your college knowledge base is ready.")
requirements.txt ADDED
Binary file (5.8 kB). View file
 
start.sh ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ #!/bin/bash
2
+ python app.py