vivicake666 commited on
Commit
4c0ad61
Β·
verified Β·
1 Parent(s): 732676b

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +170 -28
app.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
  import pymupdf
3
  import pytesseract
4
  from PIL import Image
@@ -12,15 +11,30 @@ import uuid
12
  import hashlib
13
  from openai import OpenAI
14
 
 
 
 
 
15
  supabase: Client = create_client(
16
  os.getenv("SUPABASE_URL"),
17
  os.getenv("SUPABASE_ANON_KEY")
18
  )
19
  client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
 
 
 
 
 
 
20
  model = SentenceTransformer("all-MiniLM-L6-v2")
21
  print("Model loaded!")
22
 
 
 
 
 
23
  def extract_text_from_pdf(file_path):
 
24
  doc = pymupdf.open(file_path)
25
  text = ""
26
  for page in doc:
@@ -28,6 +42,7 @@ def extract_text_from_pdf(file_path):
28
  return text
29
 
30
  def extract_text_from_image(image_path):
 
31
  try:
32
  img = Image.open(image_path)
33
  extracted_text = pytesseract.image_to_string(img)
@@ -35,7 +50,13 @@ def extract_text_from_image(image_path):
35
  except Exception as e:
36
  return f"Error extracting text from image: {e}"
37
 
 
 
 
 
 
38
  def chunk_text(text, chunk_size=1000, overlap=200):
 
39
  chunks = []
40
  start = 0
41
  while start < len(text):
@@ -44,24 +65,44 @@ def chunk_text(text, chunk_size=1000, overlap=200):
44
  start += chunk_size - overlap
45
  return chunks
46
 
 
 
 
 
 
47
  def search_relevant_chunks(query, chunks, embeddings):
 
48
  query_vec = model.encode([query])
49
  similarities = cosine_similarity(query_vec, embeddings)[0]
50
  top_indices = np.argsort(similarities)[-3:][::-1]
51
  return [chunks[i] for i in top_indices]
52
 
 
 
 
 
 
53
  def get_file_hash(file_path):
 
54
  try:
55
  with open(file_path, "rb") as f:
56
  return hashlib.md5(f.read()).hexdigest()
57
  except:
58
  return None
59
 
 
 
 
 
 
 
60
  def generate_answer(question, context):
 
61
  if "No document provided" in context:
62
  system_prompt = "You are a helpful academic math tutor. Use the Socratic method to guide the student."
63
  else:
64
  system_prompt = f"You are an academic assistant. Based only on the following context, answer the question:\n{context}"
 
65
  prompt = f"""
66
  {system_prompt}
67
 
@@ -80,26 +121,44 @@ Answer:
80
  )
81
  return response.choices[0].message.content.strip()
82
 
 
 
 
 
 
 
83
  def chat_with_file(question, file):
 
84
  if file is None:
85
  return generate_answer(question, context="No document provided. Answer from general knowledge.")
 
86
  file_path = file.name
87
  file_extension = os.path.splitext(file_path)[1].lower()
 
88
  if file_extension == ".pdf":
89
  text = extract_text_from_pdf(file_path)
90
  elif file_extension in [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff"]:
91
  text = extract_text_from_image(file_path)
92
  else:
93
  return "Unsupported file type. Please upload a PDF or image file."
 
94
  if not text.strip():
95
  return "No text could be extracted from the file."
 
96
  chunks = chunk_text(text)
97
  embeddings = model.encode(chunks)
98
  top_chunks = search_relevant_chunks(question, chunks, embeddings)
99
  combined_context = "\n\n".join(top_chunks)
100
  return generate_answer(question, combined_context)
101
 
 
 
 
 
 
 
102
  def save_chat_to_db(user_id, session_id, question, answer, file_name=None, file_hash=None):
 
103
  try:
104
  supabase.table("chat_history").insert({
105
  "user_id": user_id,
@@ -115,12 +174,9 @@ def save_chat_to_db(user_id, session_id, question, answer, file_name=None, file_
115
  return False
116
 
117
  def load_chat_history(user_id, session_id=None, limit=50):
 
118
  try:
119
- query = supabase.table("chat_history")\
120
- .select("*")\
121
- .eq("user_id", user_id)\
122
- .order("created_at", desc=False)\
123
- .limit(limit)
124
  if session_id:
125
  query = query.eq("session_id", session_id)
126
  response = query.execute()
@@ -133,13 +189,9 @@ def load_chat_history(user_id, session_id=None, limit=50):
133
  return []
134
 
135
  def get_user_sessions(user_id, limit=10):
 
136
  try:
137
- response = supabase.table("chat_history")\
138
- .select("session_id, created_at, file_name")\
139
- .eq("user_id", user_id)\
140
- .order("created_at", desc=True)\
141
- .limit(limit * 5)\
142
- .execute()
143
  sessions = {}
144
  for msg in response.data:
145
  sid = msg["session_id"]
@@ -154,12 +206,18 @@ def get_user_sessions(user_id, limit=10):
154
  print(f"Error loading sessions: {e}")
155
  return []
156
 
 
 
 
 
 
157
  class AuthManager:
158
  def __init__(self):
159
  self.current_user = None
160
  self.session_id = None
161
 
162
  def signup(self, email, password, username):
 
163
  try:
164
  response = supabase.auth.sign_up({
165
  "email": email,
@@ -177,6 +235,7 @@ class AuthManager:
177
  return False, f"Error: {error_msg}"
178
 
179
  def login(self, email, password):
 
180
  try:
181
  response = supabase.auth.sign_in_with_password({
182
  "email": email,
@@ -185,10 +244,7 @@ class AuthManager:
185
  if response.user:
186
  self.current_user = response.user
187
  self.session_id = str(uuid.uuid4())
188
- profile = supabase.table("user_profiles")\
189
- .select("username")\
190
- .eq("id", response.user.id)\
191
- .execute()
192
  username = profile.data[0]["username"] if profile.data else "User"
193
  return True, f"Welcome back, {username}!", response.user.id
194
  else:
@@ -197,6 +253,7 @@ class AuthManager:
197
  return False, f"Login error: {str(e)}", None
198
 
199
  def logout(self):
 
200
  try:
201
  supabase.auth.sign_out()
202
  self.current_user = None
@@ -206,16 +263,26 @@ class AuthManager:
206
  return False, f"Logout error: {str(e)}"
207
 
208
  def is_authenticated(self):
 
209
  return self.current_user is not None
210
 
 
211
  auth = AuthManager()
212
 
 
 
 
 
 
213
  def chat_with_file_and_save(question, file, history, user_id, session_id):
 
214
  if not auth.is_authenticated():
215
  return history + [["", "Please login to use the chatbot."]], "", None
 
216
  answer = chat_with_file(question, file)
217
  file_name = os.path.basename(file.name) if file else None
218
  file_hash = get_file_hash(file.name) if file else None
 
219
  save_chat_to_db(
220
  user_id=user_id,
221
  session_id=session_id,
@@ -224,24 +291,41 @@ def chat_with_file_and_save(question, file, history, user_id, session_id):
224
  file_name=file_name,
225
  file_hash=file_hash
226
  )
 
227
  history = history + [[question, answer]]
228
  return history, "", None
229
 
 
 
 
 
 
 
230
  def create_interface():
231
  with gr.Blocks(title="Math Tutor Chatbot", theme=gr.themes.Soft()) as demo:
 
 
232
  user_id_state = gr.State(None)
233
  session_id_state = gr.State(None)
 
234
  gr.Markdown("# Math Tutor Chatbot")
235
  gr.Markdown("Create an account to save your chat history and get Socratic math tutoring!")
 
236
  with gr.Tabs() as tabs:
 
 
237
  with gr.Tab("Login / Sign Up", id="login_tab"):
238
  with gr.Row():
 
 
239
  with gr.Column():
240
  gr.Markdown("### Login to Existing Account")
241
  login_email = gr.Textbox(label="Email", placeholder="you@example.com")
242
  login_password = gr.Textbox(label="Password", type="password")
243
  login_btn = gr.Button("Login", variant="primary", size="lg")
244
  login_msg = gr.Markdown("")
 
 
245
  with gr.Column():
246
  gr.Markdown("### Create New Account")
247
  signup_email = gr.Textbox(label="Email", placeholder="you@example.com")
@@ -249,11 +333,17 @@ def create_interface():
249
  signup_password = gr.Textbox(label="Password", type="password")
250
  signup_btn = gr.Button("Sign Up", variant="primary", size="lg")
251
  signup_msg = gr.Markdown("")
 
 
252
  with gr.Tab("Chat", id="chat_tab"):
253
  gr.Markdown("### Upload a PDF or image and ask questions!")
 
254
  with gr.Row():
 
 
255
  with gr.Column(scale=3):
256
  chatbot = gr.Chatbot(label="Conversation", height=500, type="tuples")
 
257
  with gr.Row():
258
  question_input = gr.Textbox(
259
  show_label=False,
@@ -266,10 +356,13 @@ def create_interface():
266
  scale=1
267
  )
268
  send_btn = gr.Button("Send", scale=1, variant="primary")
 
269
  with gr.Row():
270
  new_session_btn = gr.Button("New Session", size="sm")
271
  clear_btn = gr.Button("Clear Chat", size="sm")
272
  logout_btn = gr.Button("Logout", size="sm")
 
 
273
  with gr.Column(scale=1):
274
  gr.Markdown("### Your Past Sessions")
275
  sessions_display = gr.Dataframe(
@@ -288,36 +381,48 @@ def create_interface():
288
  )
289
  load_session_btn = gr.Button("Load Selected Session", size="sm", variant="primary")
290
 
 
 
291
  def handle_login(email, password):
 
292
  success, message, uid = auth.login(email, password)
293
  if success:
294
- return f"{message}", uid, str(uuid.uuid4()), gr.update(selected="chat_tab")
295
  else:
296
- return f"{message}", None, None, gr.update()
297
 
298
  def handle_signup(email, password, username):
 
299
  success, message = auth.signup(email, password, username)
300
  return message
301
 
302
  def handle_send(question, file, history, user_id, session_id):
 
303
  if not user_id:
304
  return history + [["", "Please login first!"]], "", None
305
  return chat_with_file_and_save(question, file, history, user_id, session_id)
306
 
307
  def handle_logout():
 
308
  auth.logout()
309
  return [], "Logged out successfully", None, None, gr.update(selected="login_tab")
310
 
311
  def handle_new_session(user_id):
 
312
  return [], str(uuid.uuid4())
313
 
314
  def handle_refresh_sessions(user_id):
 
315
  if not user_id:
316
  return [["Login first", ""]], []
317
  sessions = get_user_sessions(user_id, limit=20)
318
  if not sessions:
319
  return [["No sessions yet", ""]], []
320
- df_data = [[s["created_at"][:19], s["file_name"] or "No file"] for s in sessions]
 
 
 
 
321
  dropdown_choices = [
322
  "{} - {}".format(s["created_at"][:19], (s["file_name"] or "No file")[:20])
323
  for s in sessions
@@ -325,28 +430,65 @@ def create_interface():
325
  return df_data, gr.update(choices=dropdown_choices, value=None)
326
 
327
  def handle_load_session(user_id, selected_session_dropdown):
 
328
  if not user_id or not selected_session_dropdown:
329
  return [], None, "Select a session first"
330
  sessions = get_user_sessions(user_id, limit=20)
331
  selected_date = selected_session_dropdown.split(" - ")[0]
332
  matching_session = next(
333
- (s["session_id"] for s in sessions if s["created_at"][:19] == selected_date), None
 
334
  )
335
  if matching_session:
336
  return load_chat_history(user_id, matching_session), matching_session, "Session loaded!"
337
  return [], None, "Session not found"
338
 
339
- login_btn.click(fn=handle_login, inputs=[login_email, login_password], outputs=[login_msg, user_id_state, session_id_state, tabs])
340
- signup_btn.click(fn=handle_signup, inputs=[signup_email, signup_password, signup_username], outputs=[signup_msg])
341
- send_btn.click(fn=handle_send, inputs=[question_input, file_input, chatbot, user_id_state, session_id_state], outputs=[chatbot, question_input, file_input])
342
- question_input.submit(fn=handle_send, inputs=[question_input, file_input, chatbot, user_id_state, session_id_state], outputs=[chatbot, question_input, file_input])
343
- logout_btn.click(fn=handle_logout, outputs=[chatbot, login_msg, user_id_state, session_id_state, tabs])
344
- new_session_btn.click(fn=handle_new_session, inputs=[user_id_state], outputs=[chatbot, session_id_state])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  clear_btn.click(fn=lambda: [], outputs=[chatbot])
346
- refresh_sessions_btn.click(fn=handle_refresh_sessions, inputs=[user_id_state], outputs=[sessions_display, session_dropdown])
347
- load_session_btn.click(fn=handle_load_session, inputs=[user_id_state, session_dropdown], outputs=[chatbot, session_id_state, login_msg])
 
 
 
 
 
 
 
 
 
348
  return demo
349
 
 
350
  if __name__ == "__main__":
351
  demo = create_interface()
352
  demo.launch()
 
 
1
  import pymupdf
2
  import pytesseract
3
  from PIL import Image
 
11
  import hashlib
12
  from openai import OpenAI
13
 
14
+ # =============================================================================
15
+ # CONNECTIONS: Read API keys from HF Secrets (environment variables)
16
+ # Set these in your Space: Settings > Variables and secrets
17
+ # =============================================================================
18
  supabase: Client = create_client(
19
  os.getenv("SUPABASE_URL"),
20
  os.getenv("SUPABASE_ANON_KEY")
21
  )
22
  client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
23
+
24
+ # =============================================================================
25
+ # MODEL: Load the sentence transformer for semantic search
26
+ # This runs once on startup. It finds which text chunks are most relevant
27
+ # to the user's question before sending them to GPT.
28
+ # =============================================================================
29
  model = SentenceTransformer("all-MiniLM-L6-v2")
30
  print("Model loaded!")
31
 
32
+ # =============================================================================
33
+ # FILE PROCESSING: Extract raw text from uploaded PDFs and images
34
+ # =============================================================================
35
+
36
  def extract_text_from_pdf(file_path):
37
+ """Opens a PDF and concatenates all page text into one string."""
38
  doc = pymupdf.open(file_path)
39
  text = ""
40
  for page in doc:
 
42
  return text
43
 
44
  def extract_text_from_image(image_path):
45
+ """Uses Tesseract OCR to extract text from an image file."""
46
  try:
47
  img = Image.open(image_path)
48
  extracted_text = pytesseract.image_to_string(img)
 
50
  except Exception as e:
51
  return f"Error extracting text from image: {e}"
52
 
53
+ # =============================================================================
54
+ # TEXT CHUNKING: Break long documents into overlapping pieces
55
+ # Overlap ensures we don't cut off a sentence right at a chunk boundary
56
+ # =============================================================================
57
+
58
  def chunk_text(text, chunk_size=1000, overlap=200):
59
+ """Splits text into overlapping chunks for semantic search."""
60
  chunks = []
61
  start = 0
62
  while start < len(text):
 
65
  start += chunk_size - overlap
66
  return chunks
67
 
68
+ # =============================================================================
69
+ # SEMANTIC SEARCH: Find the 3 most relevant chunks for the question
70
+ # Uses cosine similarity between the question embedding and chunk embeddings
71
+ # =============================================================================
72
+
73
  def search_relevant_chunks(query, chunks, embeddings):
74
+ """Returns the top 3 chunks most semantically similar to the query."""
75
  query_vec = model.encode([query])
76
  similarities = cosine_similarity(query_vec, embeddings)[0]
77
  top_indices = np.argsort(similarities)[-3:][::-1]
78
  return [chunks[i] for i in top_indices]
79
 
80
+ # =============================================================================
81
+ # FILE HASHING: Create a unique fingerprint for each uploaded file
82
+ # Used to track which file was used in a chat session
83
+ # =============================================================================
84
+
85
  def get_file_hash(file_path):
86
+ """Returns an MD5 hash of the file contents."""
87
  try:
88
  with open(file_path, "rb") as f:
89
  return hashlib.md5(f.read()).hexdigest()
90
  except:
91
  return None
92
 
93
+ # =============================================================================
94
+ # AI ANSWER: Send question + context to GPT-4o-mini
95
+ # Uses Socratic method: guides the student rather than just giving answers
96
+ # If no file is uploaded, answers from general knowledge
97
+ # =============================================================================
98
+
99
  def generate_answer(question, context):
100
+ """Generates a Socratic/Feynman-style answer using GPT-4o-mini."""
101
  if "No document provided" in context:
102
  system_prompt = "You are a helpful academic math tutor. Use the Socratic method to guide the student."
103
  else:
104
  system_prompt = f"You are an academic assistant. Based only on the following context, answer the question:\n{context}"
105
+
106
  prompt = f"""
107
  {system_prompt}
108
 
 
121
  )
122
  return response.choices[0].message.content.strip()
123
 
124
+ # =============================================================================
125
+ # CHAT WITH FILE: Main RAG pipeline
126
+ # Combines file reading, chunking, search, and answer generation
127
+ # Falls back to general knowledge if no file is uploaded
128
+ # =============================================================================
129
+
130
  def chat_with_file(question, file):
131
+ """Runs the full RAG pipeline: extract, chunk, search, answer."""
132
  if file is None:
133
  return generate_answer(question, context="No document provided. Answer from general knowledge.")
134
+
135
  file_path = file.name
136
  file_extension = os.path.splitext(file_path)[1].lower()
137
+
138
  if file_extension == ".pdf":
139
  text = extract_text_from_pdf(file_path)
140
  elif file_extension in [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff"]:
141
  text = extract_text_from_image(file_path)
142
  else:
143
  return "Unsupported file type. Please upload a PDF or image file."
144
+
145
  if not text.strip():
146
  return "No text could be extracted from the file."
147
+
148
  chunks = chunk_text(text)
149
  embeddings = model.encode(chunks)
150
  top_chunks = search_relevant_chunks(question, chunks, embeddings)
151
  combined_context = "\n\n".join(top_chunks)
152
  return generate_answer(question, combined_context)
153
 
154
+ # =============================================================================
155
+ # DATABASE: Save and load chat history from Supabase
156
+ # Each message is stored with user_id, session_id, question, and answer
157
+ # Sessions allow users to revisit past conversations
158
+ # =============================================================================
159
+
160
  def save_chat_to_db(user_id, session_id, question, answer, file_name=None, file_hash=None):
161
+ """Saves a single Q&A exchange to the chat_history table."""
162
  try:
163
  supabase.table("chat_history").insert({
164
  "user_id": user_id,
 
174
  return False
175
 
176
  def load_chat_history(user_id, session_id=None, limit=50):
177
+ """Loads chat history for a user, optionally filtered by session."""
178
  try:
179
+ query = supabase.table("chat_history") .select("*") .eq("user_id", user_id) .order("created_at", desc=False) .limit(limit)
 
 
 
 
180
  if session_id:
181
  query = query.eq("session_id", session_id)
182
  response = query.execute()
 
189
  return []
190
 
191
  def get_user_sessions(user_id, limit=10):
192
+ """Returns a deduplicated list of recent sessions for a user."""
193
  try:
194
+ response = supabase.table("chat_history") .select("session_id, created_at, file_name") .eq("user_id", user_id) .order("created_at", desc=True) .limit(limit * 5) .execute()
 
 
 
 
 
195
  sessions = {}
196
  for msg in response.data:
197
  sid = msg["session_id"]
 
206
  print(f"Error loading sessions: {e}")
207
  return []
208
 
209
+ # =============================================================================
210
+ # AUTH MANAGER: Handles signup, login, and logout via Supabase Auth
211
+ # Stores the current user and session ID in memory while the app is running
212
+ # =============================================================================
213
+
214
  class AuthManager:
215
  def __init__(self):
216
  self.current_user = None
217
  self.session_id = None
218
 
219
  def signup(self, email, password, username):
220
+ """Creates a new Supabase Auth user with username in metadata."""
221
  try:
222
  response = supabase.auth.sign_up({
223
  "email": email,
 
235
  return False, f"Error: {error_msg}"
236
 
237
  def login(self, email, password):
238
+ """Signs in with email and password, returns user ID on success."""
239
  try:
240
  response = supabase.auth.sign_in_with_password({
241
  "email": email,
 
244
  if response.user:
245
  self.current_user = response.user
246
  self.session_id = str(uuid.uuid4())
247
+ profile = supabase.table("user_profiles") .select("username") .eq("id", response.user.id) .execute()
 
 
 
248
  username = profile.data[0]["username"] if profile.data else "User"
249
  return True, f"Welcome back, {username}!", response.user.id
250
  else:
 
253
  return False, f"Login error: {str(e)}", None
254
 
255
  def logout(self):
256
+ """Signs out and clears local user state."""
257
  try:
258
  supabase.auth.sign_out()
259
  self.current_user = None
 
263
  return False, f"Logout error: {str(e)}"
264
 
265
  def is_authenticated(self):
266
+ """Returns True if a user is currently logged in."""
267
  return self.current_user is not None
268
 
269
+ # Create a single global auth manager instance
270
  auth = AuthManager()
271
 
272
+ # =============================================================================
273
+ # CHAT HANDLER: Combines chat_with_file with database saving
274
+ # Requires the user to be logged in before processing
275
+ # =============================================================================
276
+
277
  def chat_with_file_and_save(question, file, history, user_id, session_id):
278
+ """Processes a question, saves the result to DB, updates chat display."""
279
  if not auth.is_authenticated():
280
  return history + [["", "Please login to use the chatbot."]], "", None
281
+
282
  answer = chat_with_file(question, file)
283
  file_name = os.path.basename(file.name) if file else None
284
  file_hash = get_file_hash(file.name) if file else None
285
+
286
  save_chat_to_db(
287
  user_id=user_id,
288
  session_id=session_id,
 
291
  file_name=file_name,
292
  file_hash=file_hash
293
  )
294
+
295
  history = history + [[question, answer]]
296
  return history, "", None
297
 
298
+ # =============================================================================
299
+ # GRADIO INTERFACE: Full UI with two tabs
300
+ # Tab 1: Login / Signup
301
+ # Tab 2: Chat with file upload, session history, and session loader
302
+ # =============================================================================
303
+
304
  def create_interface():
305
  with gr.Blocks(title="Math Tutor Chatbot", theme=gr.themes.Soft()) as demo:
306
+
307
+ # Hidden state: stores user ID and session ID across interactions
308
  user_id_state = gr.State(None)
309
  session_id_state = gr.State(None)
310
+
311
  gr.Markdown("# Math Tutor Chatbot")
312
  gr.Markdown("Create an account to save your chat history and get Socratic math tutoring!")
313
+
314
  with gr.Tabs() as tabs:
315
+
316
+ # ── TAB 1: Login and Signup ────────────────────────────────────
317
  with gr.Tab("Login / Sign Up", id="login_tab"):
318
  with gr.Row():
319
+
320
+ # Left side: Login
321
  with gr.Column():
322
  gr.Markdown("### Login to Existing Account")
323
  login_email = gr.Textbox(label="Email", placeholder="you@example.com")
324
  login_password = gr.Textbox(label="Password", type="password")
325
  login_btn = gr.Button("Login", variant="primary", size="lg")
326
  login_msg = gr.Markdown("")
327
+
328
+ # Right side: Signup
329
  with gr.Column():
330
  gr.Markdown("### Create New Account")
331
  signup_email = gr.Textbox(label="Email", placeholder="you@example.com")
 
333
  signup_password = gr.Textbox(label="Password", type="password")
334
  signup_btn = gr.Button("Sign Up", variant="primary", size="lg")
335
  signup_msg = gr.Markdown("")
336
+
337
+ # ── TAB 2: Chat ────────────────────────────────────────────────
338
  with gr.Tab("Chat", id="chat_tab"):
339
  gr.Markdown("### Upload a PDF or image and ask questions!")
340
+
341
  with gr.Row():
342
+
343
+ # Left: Chat area
344
  with gr.Column(scale=3):
345
  chatbot = gr.Chatbot(label="Conversation", height=500, type="tuples")
346
+
347
  with gr.Row():
348
  question_input = gr.Textbox(
349
  show_label=False,
 
356
  scale=1
357
  )
358
  send_btn = gr.Button("Send", scale=1, variant="primary")
359
+
360
  with gr.Row():
361
  new_session_btn = gr.Button("New Session", size="sm")
362
  clear_btn = gr.Button("Clear Chat", size="sm")
363
  logout_btn = gr.Button("Logout", size="sm")
364
+
365
+ # Right: Session history panel
366
  with gr.Column(scale=1):
367
  gr.Markdown("### Your Past Sessions")
368
  sessions_display = gr.Dataframe(
 
381
  )
382
  load_session_btn = gr.Button("Load Selected Session", size="sm", variant="primary")
383
 
384
+ # ── EVENT HANDLERS ─────────────────────────────────────────────────
385
+
386
  def handle_login(email, password):
387
+ """Logs in and switches to the chat tab on success."""
388
  success, message, uid = auth.login(email, password)
389
  if success:
390
+ return message, uid, str(uuid.uuid4()), gr.update(selected="chat_tab")
391
  else:
392
+ return message, None, None, gr.update()
393
 
394
  def handle_signup(email, password, username):
395
+ """Creates a new account and returns a status message."""
396
  success, message = auth.signup(email, password, username)
397
  return message
398
 
399
  def handle_send(question, file, history, user_id, session_id):
400
+ """Sends the question through the RAG pipeline and saves result."""
401
  if not user_id:
402
  return history + [["", "Please login first!"]], "", None
403
  return chat_with_file_and_save(question, file, history, user_id, session_id)
404
 
405
  def handle_logout():
406
+ """Logs out and switches back to the login tab."""
407
  auth.logout()
408
  return [], "Logged out successfully", None, None, gr.update(selected="login_tab")
409
 
410
  def handle_new_session(user_id):
411
+ """Clears the chat and generates a fresh session ID."""
412
  return [], str(uuid.uuid4())
413
 
414
  def handle_refresh_sessions(user_id):
415
+ """Loads recent sessions from DB and populates the dropdown."""
416
  if not user_id:
417
  return [["Login first", ""]], []
418
  sessions = get_user_sessions(user_id, limit=20)
419
  if not sessions:
420
  return [["No sessions yet", ""]], []
421
+ df_data = [
422
+ [s["created_at"][:19], s["file_name"] or "No file"]
423
+ for s in sessions
424
+ ]
425
+ # Using .format() instead of f-strings to avoid quote conflicts
426
  dropdown_choices = [
427
  "{} - {}".format(s["created_at"][:19], (s["file_name"] or "No file")[:20])
428
  for s in sessions
 
430
  return df_data, gr.update(choices=dropdown_choices, value=None)
431
 
432
  def handle_load_session(user_id, selected_session_dropdown):
433
+ """Loads a previously selected session into the chat window."""
434
  if not user_id or not selected_session_dropdown:
435
  return [], None, "Select a session first"
436
  sessions = get_user_sessions(user_id, limit=20)
437
  selected_date = selected_session_dropdown.split(" - ")[0]
438
  matching_session = next(
439
+ (s["session_id"] for s in sessions if s["created_at"][:19] == selected_date),
440
+ None
441
  )
442
  if matching_session:
443
  return load_chat_history(user_id, matching_session), matching_session, "Session loaded!"
444
  return [], None, "Session not found"
445
 
446
+ # ── WIRE UP BUTTONS TO HANDLERS ────────────────────────────────────
447
+
448
+ login_btn.click(
449
+ fn=handle_login,
450
+ inputs=[login_email, login_password],
451
+ outputs=[login_msg, user_id_state, session_id_state, tabs]
452
+ )
453
+ signup_btn.click(
454
+ fn=handle_signup,
455
+ inputs=[signup_email, signup_password, signup_username],
456
+ outputs=[signup_msg]
457
+ )
458
+ send_btn.click(
459
+ fn=handle_send,
460
+ inputs=[question_input, file_input, chatbot, user_id_state, session_id_state],
461
+ outputs=[chatbot, question_input, file_input]
462
+ )
463
+ question_input.submit(
464
+ fn=handle_send,
465
+ inputs=[question_input, file_input, chatbot, user_id_state, session_id_state],
466
+ outputs=[chatbot, question_input, file_input]
467
+ )
468
+ logout_btn.click(
469
+ fn=handle_logout,
470
+ outputs=[chatbot, login_msg, user_id_state, session_id_state, tabs]
471
+ )
472
+ new_session_btn.click(
473
+ fn=handle_new_session,
474
+ inputs=[user_id_state],
475
+ outputs=[chatbot, session_id_state]
476
+ )
477
  clear_btn.click(fn=lambda: [], outputs=[chatbot])
478
+ refresh_sessions_btn.click(
479
+ fn=handle_refresh_sessions,
480
+ inputs=[user_id_state],
481
+ outputs=[sessions_display, session_dropdown]
482
+ )
483
+ load_session_btn.click(
484
+ fn=handle_load_session,
485
+ inputs=[user_id_state, session_dropdown],
486
+ outputs=[chatbot, session_id_state, login_msg]
487
+ )
488
+
489
  return demo
490
 
491
+
492
  if __name__ == "__main__":
493
  demo = create_interface()
494
  demo.launch()