gladguy commited on
Commit
ef09325
·
1 Parent(s): f7d9748

Major refactor: simplified navigation and code structure

Browse files
Files changed (1) hide show
  1. app.py +276 -1204
app.py CHANGED
@@ -10,21 +10,17 @@ import tempfile
10
  import sqlite3
11
  from datetime import datetime
12
 
13
- # Load environment variables from .env file
14
  load_dotenv()
15
 
16
  SERPAPI_KEY = os.getenv("SERPAPI_KEY")
17
  HYPERBOLIC_API_KEY = os.getenv("HYPERBOLIC_API_KEY")
18
  ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY")
19
-
20
- # Admin password
21
  ADMIN_PASSWORD = "BT54iv!@"
22
-
23
- # Database setup
24
  DB_PATH = "students.db"
25
 
 
26
  def init_database():
27
- """Initialize the SQLite database and create students table if it doesn't exist."""
28
  conn = sqlite3.connect(DB_PATH)
29
  cursor = conn.cursor()
30
  cursor.execute("""
@@ -40,14 +36,10 @@ def init_database():
40
  conn.close()
41
 
42
  def save_student(name, medical_school, year):
43
- """Save student information to the database."""
44
  try:
45
  conn = sqlite3.connect(DB_PATH)
46
  cursor = conn.cursor()
47
- cursor.execute(
48
- "INSERT INTO students (name, medical_school, year) VALUES (?, ?, ?)",
49
- (name, medical_school, year)
50
- )
51
  conn.commit()
52
  conn.close()
53
  return True
@@ -56,1268 +48,348 @@ def save_student(name, medical_school, year):
56
  return False
57
 
58
  def get_all_students():
59
- """Retrieve all students from the database."""
60
  try:
61
  conn = sqlite3.connect(DB_PATH)
62
  cursor = conn.cursor()
63
  cursor.execute("SELECT id, name, medical_school, year, registration_date FROM students ORDER BY registration_date DESC")
64
- students = cursor.fetchall()
65
- conn.close()
66
- return students
67
- except Exception as e:
68
- print(f"Error retrieving students: {e}")
69
  return []
70
 
71
- # Initialize database on startup
72
  init_database()
73
 
74
-
75
- # Hyperbolic API configuration
76
  HYPERBOLIC_API_URL = "https://api.hyperbolic.xyz/v1/chat/completions"
77
  HYPERBOLIC_MODEL = "meta-llama/Llama-3.3-70B-Instruct"
78
-
79
- # ElevenLabs API configuration
80
  ELEVENLABS_API_URL = "https://api.elevenlabs.io/v1/text-to-speech"
81
- # Using a standard "Professor" like voice (e.g., "Brian" - a deep, authoritative British voice, or similar)
82
- # Voice ID for "Brian": nPczCjzI2devNBz1zQrb
83
  ELEVENLABS_VOICE_ID = "nPczCjzI2devNBz1zQrb"
84
 
 
85
  def generate_audio(text: str, student_name: str = None) -> str:
86
- """
87
- Generate audio from text using ElevenLabs API.
88
- If student_name is provided, prepends a personalized greeting.
89
- Returns path to temporary audio file or None if failed.
90
- """
91
- if not ELEVENLABS_API_KEY:
92
- print("⚠️ ELEVENLABS_API_KEY is missing")
93
- return None
94
-
95
- if not text:
96
- print("⚠️ No text provided for audio generation")
97
- return None
98
-
99
- # Add personalized greeting if student name is provided
100
- if student_name:
101
- text = f"Welcome to Viva, Doctor {student_name}, let's start. {text}"
102
-
103
- print(f"Generating audio for text: {text[:50]}...")
104
-
105
  try:
106
  url = f"{ELEVENLABS_API_URL}/{ELEVENLABS_VOICE_ID}"
107
- headers = {
108
- "Accept": "audio/mpeg",
109
- "Content-Type": "application/json",
110
- "xi-api-key": ELEVENLABS_API_KEY
111
- }
112
- data = {
113
- "text": text,
114
- "model_id": "eleven_turbo_v2",
115
- "voice_settings": {
116
- "stability": 0.5,
117
- "similarity_boost": 0.5
118
- }
119
- }
120
-
121
  response = requests.post(url, json=data, headers=headers)
122
-
123
  if response.status_code == 200:
124
- # Save to temp file
125
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as f:
126
  f.write(response.content)
127
- print(f"✅ Audio generated successfully: {f.name}")
128
  return f.name
129
- else:
130
- print(f"❌ ElevenLabs API Error ({response.status_code}): {response.text}")
131
- return None
132
-
133
  except Exception as e:
134
- print(f"Error generating audio: {str(e)}")
135
  return None
136
 
137
  def is_anatomy_related(query: str) -> tuple[bool, str]:
138
- """
139
- Validate if the query is anatomy-related using the LLM.
140
- Returns (is_valid, message)
141
- """
142
- validation_prompt = f"""You are an anatomy topic validator for medical students.
143
- Determine if the following question is related to human anatomy ONLY.
144
-
145
- Question: "{query}"
146
-
147
- Respond with ONLY "YES" if it's about anatomy (structures, organs, systems, blood vessels, nerves, bones, muscles, etc.)
148
- Respond with ONLY "NO" if it's not about anatomy (physiology, biochemistry, pharmacology, diseases, treatments, etc.)
149
-
150
- Response:"""
151
-
152
  try:
153
- headers = {
154
- "Content-Type": "application/json",
155
- "Authorization": f"Bearer {HYPERBOLIC_API_KEY}"
156
- }
157
-
158
- payload = {
159
- "model": HYPERBOLIC_MODEL,
160
- "messages": [{"role": "user", "content": validation_prompt}],
161
- "max_tokens": 10,
162
- "temperature": 0.1
163
- }
164
-
165
  response = requests.post(HYPERBOLIC_API_URL, headers=headers, json=payload, timeout=10)
166
- response.raise_for_status()
167
-
168
- result = response.json()
169
- answer = result["choices"][0]["message"]["content"].strip().upper()
170
-
171
- if "YES" in answer:
172
- return True, ""
173
- else:
174
- return False, "⚠️ Please ask questions related to anatomy only. This question appears to be about other medical topics."
175
-
176
- except Exception as e:
177
- # If validation fails, allow the query but log the error
178
- print(f"Validation error: {e}")
179
- return True, ""
180
-
181
 
182
  def search_anatomy_image(query: str) -> tuple[list, str]:
183
- """
184
- Search for anatomy images using SERPAPI Google Images.
185
- Returns (list_of_image_urls, error_message)
186
- """
187
  try:
188
- params = {
189
- "engine": "google_images",
190
- "q": f"{query} anatomy diagram",
191
- "api_key": SERPAPI_KEY,
192
- "num": 10, # Get more results for fallback
193
- "safe": "active"
194
- }
195
-
196
- response = requests.get("https://serpapi.com/search", params=params, timeout=15)
197
- response.raise_for_status()
198
-
199
- data = response.json()
200
-
201
- if "images_results" in data and len(data["images_results"]) > 0:
202
- # Get multiple image URLs, filter out SVG files
203
- image_urls = []
204
- for img in data["images_results"]:
205
- url = img.get("original", "")
206
- # Skip SVG files and other problematic formats
207
- if url and not url.lower().endswith('.svg'):
208
- image_urls.append(url)
209
-
210
- if image_urls:
211
- return image_urls, ""
212
- else:
213
- return [], "No supported image formats found (SVG files excluded)."
214
- else:
215
- return [], "No images found for this anatomy topic."
216
-
217
- except Exception as e:
218
- return [], f"Error searching for images: {str(e)}"
219
-
220
-
221
- def download_image(image_url: str) -> Image.Image:
222
- """
223
- Download and return PIL Image from URL.
224
- """
225
  try:
226
- # Add headers to mimic a browser request and avoid 403 errors
227
- headers = {
228
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
229
- 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
230
- 'Accept-Language': 'en-US,en;q=0.9',
231
- 'Referer': 'https://www.google.com/'
232
- }
233
- response = requests.get(image_url, headers=headers, timeout=10)
234
- response.raise_for_status()
235
- img = Image.open(BytesIO(response.content))
236
- return img
237
- except Exception as e:
238
- raise Exception(f"Error downloading image: {str(e)}")
239
-
240
 
241
  def generate_anatomy_info(query: str) -> str:
242
- """
243
- Generate educational information about the anatomy topic using Hyperbolic API.
244
- """
245
  try:
246
- headers = {
247
- "Content-Type": "application/json",
248
- "Authorization": f"Bearer {HYPERBOLIC_API_KEY}"
249
- }
250
-
251
- prompt = f"""You are an expert anatomy professor teaching MBBS students. Provide a detailed, high-level educational summary about: {query}
252
-
253
- Format your response with clear sections using these exact emoji icons:
254
-
255
- 📍 **Location & Definition:**
256
- [Precise anatomical definition, location, and relations using standard medical terminology]
257
-
258
- 🔍 **Key Anatomical Features:**
259
- - [Detailed feature 1 (e.g., attachments, blood supply, innervation)]
260
- - [Detailed feature 2]
261
- - [Detailed feature 3]
262
-
263
- 🏥 **Clinical Significance:**
264
- - [Clinical correlation 1 (e.g., pathologies, surgical relevance)]
265
- - [Clinical correlation 2]
266
-
267
- 🔗 **Related Structures:**
268
- - [Related structure 1]
269
- - [Related structure 2]
270
-
271
- 💡 **Quick Memory Tip:**
272
- [A high-yield mnemonic or tip for exams]
273
-
274
- Keep it professional, accurate, and suitable for medical school level study. Use proper anatomical terminology throughout."""
275
-
276
- payload = {
277
- "model": HYPERBOLIC_MODEL,
278
- "messages": [{"role": "user", "content": prompt}],
279
- "max_tokens": 600,
280
- "temperature": 0.7
281
- }
282
-
283
- response = requests.post(HYPERBOLIC_API_URL, headers=headers, json=payload, timeout=20)
284
- response.raise_for_status()
285
-
286
- result = response.json()
287
- info = result["choices"][0]["message"]["content"]
288
-
289
- # Add prominent header to make it stand out
290
- formatted_info = f"""## 📚 Key Learning Points
291
-
292
- {info}
293
-
294
- ---
295
- 💪 **Study Tip:** Read through these points carefully, then test yourself with VIVA mode!"""
296
-
297
- return formatted_info
298
-
299
- except Exception as e:
300
- return f"⚠️ Error generating information: {str(e)}"
301
-
302
 
303
  def generate_viva_questions(topic: str) -> list:
304
- """
305
- Generate 5 viva questions for the anatomy topic.
306
- Returns list of question dictionaries with question, hint, and expected answer.
307
- """
308
  try:
309
- headers = {
310
- "Content-Type": "application/json",
311
- "Authorization": f"Bearer {HYPERBOLIC_API_KEY}"
312
- }
313
-
314
- prompt = f"""You are a strict but fair anatomy professor conducting a VIVA exam for final year MBBS students on: {topic}
315
-
316
- Generate exactly 5 viva questions that test deep anatomical understanding, clinical application, and relations. For each question, provide:
317
- 1. The question (challenging, requiring synthesis of knowledge)
318
- 2. A helpful hint (guides thinking without giving the answer)
319
- 3. The expected key points in the answer (using proper terminology)
320
-
321
- Format your response EXACTLY as follows:
322
- Q1: [question]
323
- HINT: [hint]
324
- ANSWER: [expected answer key points]
325
-
326
- Q2: [question]
327
- HINT: [hint]
328
- ANSWER: [expected answer key points]
329
-
330
- ... and so on for all 5 questions.
331
-
332
- Make questions progressively harder. Start with detailed relations/supply, then move to complex clinical scenarios."""
333
-
334
- payload = {
335
- "model": HYPERBOLIC_MODEL,
336
- "messages": [{"role": "user", "content": prompt}],
337
- "max_tokens": 800,
338
- "temperature": 0.7
339
- }
340
-
341
- response = requests.post(HYPERBOLIC_API_URL, headers=headers, json=payload, timeout=25)
342
- response.raise_for_status()
343
-
344
- result = response.json()
345
- content = result["choices"][0]["message"]["content"]
346
-
347
- # Parse the questions
348
  questions = []
349
- lines = content.split('\n')
350
- current_q = {}
351
-
352
- for line in lines:
353
  line = line.strip()
354
  if line.startswith('Q') and ':' in line:
355
- if current_q:
356
- questions.append(current_q)
357
- current_q = {'question': line.split(':', 1)[1].strip()}
358
- elif line.startswith('HINT:'):
359
- current_q['hint'] = line.split(':', 1)[1].strip()
360
- elif line.startswith('ANSWER:'):
361
- current_q['answer'] = line.split(':', 1)[1].strip()
362
-
363
- if current_q:
364
- questions.append(current_q)
365
-
366
- return questions[:5] # Ensure exactly 5 questions
367
-
368
- except Exception as e:
369
- print(f"Error generating viva questions: {e}")
370
- return []
371
-
372
-
373
- def evaluate_viva_answer(question: str, student_answer: str, expected_answer: str) -> tuple[str, str]:
374
- """
375
- Evaluate student's answer and provide feedback.
376
- Returns (feedback, score_emoji)
377
- """
378
- if not student_answer.strip():
379
- return "⏸️ Please provide an answer to continue.", "⏸️"
380
-
381
  try:
382
- headers = {
383
- "Content-Type": "application/json",
384
- "Authorization": f"Bearer {HYPERBOLIC_API_KEY}"
385
- }
386
-
387
- prompt = f"""You are an anatomy professor evaluating an MBBS student's VIVA answer. Expect high standards and precise terminology.
388
-
389
- Question: {question}
390
- Expected key points: {expected_answer}
391
- Student's answer: {student_answer}
392
-
393
- Provide feedback in this EXACT format:
394
-
395
- [First, write one sentence evaluating the precision and depth of the answer]
396
-
397
- ✅ **What was correct:**
398
- [List correct points. Praise use of proper terminology.]
399
-
400
- ❌ **What was missing:**
401
- [List missing key points, relations, or clinical aspects. Be specific about missing terminology.]
402
-
403
- **Score:** [Choose: DISTINCTION, PASS, BORDERLINE, or FAIL]
404
-
405
- [End with a constructive comment on how to improve to a professional medical standard]
406
-
407
- Be professional and constructive. Demand accuracy."""
408
-
409
- payload = {
410
- "model": HYPERBOLIC_MODEL,
411
- "messages": [{"role": "user", "content": prompt}],
412
- "max_tokens": 400,
413
- "temperature": 0.6
414
- }
415
-
416
- response = requests.post(HYPERBOLIC_API_URL, headers=headers, json=payload, timeout=15)
417
- response.raise_for_status()
418
-
419
- result = response.json()
420
- feedback = result["choices"][0]["message"]["content"]
421
-
422
- # Determine emoji and encouragement based on feedback content
423
- feedback_upper = feedback.upper()
424
- if "DISTINCTION" in feedback_upper:
425
- emoji = "🌟"
426
- encouragement = "\n\n🎉 **Outstanding!** Distinction level answer! You're mastering this topic!"
427
- elif "PASS" in feedback_upper:
428
- emoji = "✅"
429
- encouragement = "\n\n👏 **Good Pass!** Solid understanding. Review the finer details to reach distinction level."
430
- elif "BORDERLINE" in feedback_upper:
431
- emoji = "⚠️"
432
- encouragement = "\n\n💪 **Borderline.** You have the basics, but need more precision with terminology."
433
- else:
434
- emoji = "📚"
435
- encouragement = "\n\n🌱 **Keep studying.** Focus on the key anatomical relations and clinical points."
436
-
437
- # Format the complete feedback
438
- formatted_feedback = f"{emoji} **VIVA Feedback:**\n\n{feedback}{encouragement}\n\n---\n\n📖 **Reference Answer:**\n{expected_answer}"
439
-
440
- return formatted_feedback, emoji
441
-
442
- except Exception as e:
443
- return f"⚠️ Could not evaluate answer: {str(e)}", "⚠️"
444
-
445
-
446
- def process_anatomy_query(query: str) -> tuple:
447
- """
448
- Main function to process anatomy queries.
449
- Returns (image, info_text, error_message)
450
- """
451
- if not query.strip():
452
- return None, "", "Please enter a question about anatomy."
453
-
454
- # Validate if query is anatomy-related
455
- is_valid, validation_msg = is_anatomy_related(query)
456
- if not is_valid:
457
- return None, "", validation_msg
458
-
459
- # Search for images
460
- image_urls, img_error = search_anatomy_image(query)
461
-
462
- # Generate educational information
463
  info = generate_anatomy_info(query)
464
 
465
- # Try to download images from the list until one succeeds
466
- image = None
467
- download_error = ""
468
-
469
- if image_urls:
470
- for url in image_urls[:5]: # Try up to 5 images
471
- try:
472
- image = download_image(url)
473
- download_error = "" # Success!
474
- break # Stop trying once we get a valid image
475
- except Exception as e:
476
- download_error = str(e)
477
- continue # Try next image
478
 
479
- if not image and download_error:
480
- img_error = f"Could not download images. Last error: {download_error}"
481
-
482
- # Prepare result
483
- error_message = ""
484
- if img_error:
485
- error_message = f"⚠️ {img_error}"
486
-
487
- return image, info, error_message
488
 
489
-
490
- # Book Learning Mode Functions
491
  def process_uploaded_book(pdf_file):
492
- """
493
- Process uploaded PDF book and extract all pages with images and text.
494
- Returns (list_of_tuples, status_message) where tuple is (image, caption, text)
495
- """
496
- if pdf_file is None:
497
- return [], "Please upload a PDF file."
498
-
499
  try:
500
- extracted_data = []
501
-
502
- # Save uploaded file temporarily
503
- with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_file:
504
- tmp_file.write(pdf_file)
505
- tmp_path = tmp_file.name
506
-
507
- try:
508
- # Convert all pages to images (this might take a while for large books)
509
- images = convert_from_path(tmp_path, dpi=150)
510
-
511
- # Extract text from pages
512
- reader = PyPDF2.PdfReader(tmp_path)
513
-
514
- for i, image in enumerate(images):
515
- # Get text for this page if available
516
- text_content = ""
517
- if i < len(reader.pages):
518
- try:
519
- text_content = reader.pages[i].extract_text()
520
- except:
521
- text_content = "Could not extract text from this page."
522
-
523
- # Limit text length to avoid token limits
524
- if len(text_content) > 2000:
525
- text_content = text_content[:2000] + "..."
526
-
527
- extracted_data.append((image, f"Page {i+1}", text_content))
528
-
529
- status = f"✅ Successfully processed {len(extracted_data)} pages from your anatomy textbook!"
530
- return extracted_data, status
531
-
532
- finally:
533
- # Clean up temp file
534
- if os.path.exists(tmp_path):
535
- os.unlink(tmp_path)
536
-
537
- except Exception as e:
538
- return [], f"⚠️ Error processing PDF: {str(e)}"
539
-
540
-
541
- def analyze_book_image(image, page_info, page_text=""):
542
- """
543
- Analyze selected image from book using AI to extract anatomical information.
544
- Returns formatted explanation text.
545
- """
546
- if image is None:
547
- return "Please select an image from the book."
548
-
549
  try:
550
- headers = {
551
- "Content-Type": "application/json",
552
- "Authorization": f"Bearer {HYPERBOLIC_API_KEY}"
553
- }
554
-
555
- # Include extracted text in the prompt context
556
- context_text = f"Page Content:\n{page_text}" if page_text else "No text extracted from this page."
557
-
558
- prompt = f"""You are an expert anatomy professor helping MBBS students analyze their textbook content.
559
-
560
- A student is looking at {page_info} of their anatomy textbook.
561
- {context_text}
562
-
563
- Based on the text content above, provide a high-level medical analysis:
564
-
565
- ## 📖 Page Overview
566
- [Summarize the key anatomical topic using standard medical terminology]
567
-
568
- ## 🔍 Key Concepts Explained
569
- [Explain the concepts in detail, focusing on relations, blood supply, nerve supply, and lymphatic drainage where applicable]
570
-
571
- ## 🏥 Clinical Relevance
572
- [Detailed clinical correlations, surgical landmarks, or pathological conditions mentioned or relevant]
573
-
574
- ## 💡 Study Tips
575
- [High-yield memory aids for medical exams]
576
-
577
- ## ❓ Self-Test Questions (MBBS Level)
578
- 1. [Question based on the page text]
579
- 2. [Question based on the page text]
580
- ...
581
- 15. [Question based on the page text]
582
-
583
- (Provide at least 15 distinct, challenging questions covering detailed anatomy, relations, and clinical application)
584
-
585
- Be professional, accurate, and suitable for medical school level study."""
586
-
587
- payload = {
588
- "model": HYPERBOLIC_MODEL,
589
- "messages": [{"role": "user", "content": prompt}],
590
- "max_tokens": 1200,
591
- "temperature": 0.5
592
- }
593
-
594
- response = requests.post(HYPERBOLIC_API_URL, headers=headers, json=payload, timeout=25)
595
- response.raise_for_status()
596
-
597
- result = response.json()
598
- explanation = result["choices"][0]["message"]["content"]
599
-
600
- formatted_output = f"""# 📚 Textbook Analysis: {page_info}
601
-
602
- {explanation}
603
-
604
- ---
605
-
606
- 💪 **Next Steps:** Mastered this page? Try the VIVA mode to test yourself!"""
607
-
608
- return formatted_output
609
-
610
- except Exception as e:
611
- return f"⚠️ Error analyzing image: {str(e)}"
612
-
613
-
614
- # VIVA Mode Handler Functions
615
- def start_viva_mode(topic, image, student_name=""):
616
- """Initialize VIVA mode with questions."""
617
- if not topic or not image:
618
- return (
619
- gr.update(visible=False), # viva_container
620
- "Please learn about a topic first before starting VIVA mode!", # viva_status
621
- None, None, None, None, None, None, [], None, student_name # other outputs
622
- )
623
-
624
- questions = generate_viva_questions(topic)
625
-
626
- if not questions or len(questions) == 0:
627
- return (
628
- gr.update(visible=False),
629
- "Error generating VIVA questions. Please try again.",
630
- None, None, None, None, None, gr.update(interactive=False), [], None, student_name
631
- )
632
-
633
- # Start with question 1
634
- q1 = questions[0]
635
-
636
- # Generate audio for first question with student name
637
- audio_path = generate_audio(q1['question'], student_name if student_name else None)
638
-
639
  return (
640
- gr.update(visible=True), # Show VIVA container
641
- f"**VIVA MODE ACTIVE** 📝\nTopic: {topic}", # viva_status
642
- image, # viva_image
643
- f"### Question 1 of 5\n\n**{q1['question']}**", # current_question_display
644
- f"💡 **Hint:** {q1.get('hint', 'Think about the key anatomical features.')}", # hint_display
645
- "", # Clear answer input
646
- "", # Clear feedback
647
- gr.update(interactive=True, value="Submit Answer"), # Enable submit button
648
- questions, # Store questions in state
649
- audio_path, # Return audio path
650
- student_name # Return student name to maintain in state
651
  )
652
 
653
- # Wrapper to start VIVA with personalized greeting
654
- def start_viva_with_name(name, topic, image):
655
- viva_container_out, viva_status_out, viva_image_out, cur_q_disp, hint_disp, stu_ans, fb_disp, sub_btn, viva_q_state, q_audio, student_name_out = start_viva_mode(topic, image, name)
656
- greeting = f"Doctor {name}, let's go to VIVA!"
657
- # Add greeting as separate markdown component above question
658
- viva_greeting_out = greeting
659
- return viva_container_out, viva_status_out, viva_image_out, cur_q_disp, hint_disp, stu_ans, fb_disp, sub_btn, viva_q_state, q_audio, viva_greeting_out, student_name_out
660
-
661
-
662
- def submit_viva_answer(answer, questions, current_q_idx, student_name=""):
663
- """Process student's answer and move to next question."""
664
- if not questions or current_q_idx >= len(questions):
665
- return ("VIVA Complete!", "", "", gr.update(interactive=False), current_q_idx, None)
666
-
667
- q = questions[current_q_idx]
668
- feedback_text, emoji = evaluate_viva_answer(q['question'], answer, q.get('answer', ''))
669
-
670
- # Move to next question
671
- next_idx = current_q_idx + 1
672
 
673
- if next_idx < len(questions):
674
- next_q = questions[next_idx]
675
- next_question = f"### Question {next_idx + 1} of 5\n\n**{next_q['question']}**"
676
- next_hint = f"💡 **Hint:** {next_q.get('hint', 'Think carefully about the anatomical relationships.')}"
677
-
678
- # Generate audio for next question with student name
679
- audio_path = generate_audio(next_q['question'], student_name if student_name else None)
680
-
681
- return (
682
- next_question, # Show next question
683
- next_hint, # Show next hint
684
- "", # Clear answer box
685
- feedback_text, # Show feedback for current answer
686
- gr.update(interactive=True, value="Submit Answer"), # Keep button enabled
687
- next_idx, # Update question index
688
- audio_path # Play next question audio
689
- )
690
  else:
691
- # VIVA complete
692
- completion_msg = f"### 🎉 VIVA Complete!\n\nYou've answered all 5 questions. Great job on completing your anatomy VIVA training!"
693
- return (
694
- completion_msg,
695
- "", # Clear hint
696
- "", # Clear answer
697
- feedback_text, # Final feedback
698
- gr.update(interactive=False, value="VIVA Complete"),
699
- next_idx,
700
- None # No audio
701
- )
702
-
703
-
704
- # Create Gradio interface
705
- with gr.Blocks(title="AnatomyBot - MBBS Anatomy Tutor") as demo:
706
- # State variables
707
- student_name_state = gr.State("")
708
- viva_questions_state = gr.State([])
709
- current_question_idx = gr.State(0)
710
- current_topic = gr.State("")
711
- current_image_state = gr.State(None)
712
- is_registered = gr.State(False) # Track if user has registered
713
-
714
- # Add custom CSS styling via HTML
715
- gr.HTML("""
716
- <style>
717
- /* Modal backdrop using body::after when modal exists and is visible */
718
- body:has(#registration_modal:not([style*="display: none"]))::after {
719
- content: '';
720
- position: fixed;
721
- top: 0;
722
- left: 0;
723
- right: 0;
724
- bottom: 0;
725
- background: rgba(0, 0, 0, 0.6);
726
- backdrop-filter: blur(8px);
727
- -webkit-backdrop-filter: blur(8px);
728
- z-index: 999;
729
- pointer-events: all;
730
- }
731
-
732
- /* Modal container */
733
- #registration_modal {
734
- position: fixed !important;
735
- top: 50% !important;
736
- left: 50% !important;
737
- transform: translate(-50%, -50%) !important;
738
- z-index: 1000 !important;
739
- background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%) !important;
740
- padding: 2.5rem !important;
741
- border-radius: 20px !important;
742
- border: 3px solid rgba(255,107,53,0.5) !important;
743
- box-shadow:
744
- 0 10px 40px rgba(0,0,0,0.3),
745
- 0 0 20px rgba(255,107,53,0.2),
746
- inset 0 1px 0 rgba(255,255,255,0.9) !important;
747
- max-width: 600px !important;
748
- width: 90% !important;
749
- animation: modalSlideIn 0.3s ease-out !important;
750
- }
751
-
752
- /* Modal animation */
753
- @keyframes modalSlideIn {
754
- from {
755
- opacity: 0;
756
- transform: translate(-50%, -60%);
757
- }
758
- to {
759
- opacity: 1;
760
- transform: translate(-50%, -50%);
761
- }
762
- }
763
-
764
- /* Beautify modal content */
765
- #registration_modal h1 {
766
- color: #2c3e50 !important;
767
- margin-bottom: 0.5rem !important;
768
- font-size: 2rem !important;
769
- }
770
-
771
- #registration_modal h3 {
772
- color: #7f8c8d !important;
773
- font-weight: 400 !important;
774
- font-size: 1.1rem !important;
775
- }
776
-
777
- /* ========================================
778
- RESPONSIVE NAVIGATION BAR FIX
779
- Ensures navigation stays horizontal in iframe/HF Spaces
780
- ======================================== */
781
-
782
- /* Force navigation bar to stay horizontal */
783
- #nav_bar {
784
- display: flex !important;
785
- flex-direction: row !important;
786
- flex-wrap: nowrap !important;
787
- gap: 0.5rem !important;
788
- width: 100% !important;
789
- overflow-x: auto !important;
790
- overflow-y: hidden !important;
791
- }
792
-
793
- /* Equal-width buttons that shrink gracefully */
794
- #nav_bar button {
795
- flex: 1 1 0 !important;
796
- min-width: 0 !important;
797
- white-space: nowrap !important;
798
- overflow: hidden !important;
799
- text-overflow: ellipsis !important;
800
- font-size: 0.875rem !important;
801
- padding: 0.5rem 0.75rem !important;
802
- }
803
-
804
- /* Responsive adjustments for narrower viewports */
805
- @media (max-width: 900px) {
806
- #nav_bar button {
807
- font-size: 0.75rem !important;
808
- padding: 0.4rem 0.5rem !important;
809
- }
810
- }
811
-
812
- @media (max-width: 600px) {
813
- #nav_bar button {
814
- font-size: 0.7rem !important;
815
- padding: 0.3rem 0.4rem !important;
816
- }
817
-
818
- /* Hide emojis on very small screens */
819
- #nav_bar button::before {
820
- content: none !important;
821
- }
822
- }
823
- </style>
824
- """)
825
-
826
- # Main Application (always visible now)
827
- with gr.Column() as main_app:
828
- gr.Markdown(
829
- """
830
- # 🩺 AnatomyBot - Your MBBS Anatomy Tutor
831
-
832
- Master anatomy through AI-powered learning and interactive VIVA practice!
833
- """
834
- )
835
-
836
- # Display student name
837
- student_name_display = gr.Markdown("")
838
-
839
- # Custom Navigation Bar
840
  with gr.Row(elem_id="nav_bar"):
841
- nav_learning_btn = gr.Button("📚 Learning Mode", variant="primary", scale=1)
842
- nav_viva_btn = gr.Button("🎯 VIVA Training Mode", variant="secondary", scale=1)
843
- nav_book_btn = gr.Button("📖 Book Learning Mode", variant="secondary", scale=1)
844
- nav_admin_btn = gr.Button("🔐 Admin Panel", variant="secondary", scale=1)
845
 
846
- # LEARNING MODE COLUMN
847
- with gr.Column(visible=True, elem_id="learning_col") as learning_col:
848
- # Search and examples at the top
849
- with gr.Row():
850
- query_input = gr.Textbox(
851
- label="Ask an Anatomy Question",
852
- placeholder="e.g., Show me the Circle of Willis",
853
- lines=2
854
- )
855
-
856
- # Examples
857
- gr.Examples(
858
- examples=[
859
- ["Show me the Circle of Willis"],
860
- ["Brachial plexus anatomy"],
861
- ["Carpal bones arrangement"],
862
- ["Layers of the scalp"],
863
- ["Anatomy of the heart chambers"],
864
- ["Cranial nerves and their functions"],
865
- ["Structure of the kidney nephron"],
866
- ["Branches of the abdominal aorta"],
867
- ["Rotator cuff muscles"],
868
- ["Spinal cord cross section"],
869
- ["Femoral triangle anatomy"],
870
- ["Larynx cartilages and membranes"],
871
- ["Portal venous system"],
872
- ["Anatomy of the eyeball"],
873
- ["Bronchopulmonary segments"]
874
- ],
875
- inputs=query_input
876
- )
877
-
878
- with gr.Row():
879
- submit_btn = gr.Button("🔍 Search & Learn", variant="primary", size="lg")
880
- start_viva_btn = gr.Button("🎯 Start VIVA Training", variant="secondary", size="lg")
881
-
882
- error_output = gr.Markdown(label="Status")
883
-
884
- # Main content: Key Learning Points (left) and Anatomy Diagram (right)
885
- with gr.Row():
886
- with gr.Column(scale=1):
887
- info_output = gr.Markdown(label="📚 Key Learning Points")
888
-
889
- with gr.Column(scale=1):
890
- image_output = gr.Image(label="🖼️ Anatomy Diagram", type="pil")
891
-
892
- # VIVA MODE COLUMN
893
- with gr.Column(visible=False, elem_id="viva_col") as viva_col:
894
- viva_status = gr.Markdown("Click 'Start VIVA Training' from Learning Mode after studying a topic!")
895
-
896
- # Additional greeting component (initially hidden)
897
- viva_greeting = gr.Markdown("", visible=False)
898
 
899
- with gr.Column(visible=False) as viva_container:
 
900
  with gr.Row():
901
- with gr.Column(scale=1):
902
- viva_image = gr.Image(label="Reference Image", type="pil", interactive=False)
903
-
904
- with gr.Column(scale=2):
905
- current_question_display = gr.Markdown("### Question will appear here")
906
- hint_display = gr.Markdown("💡 Hint will appear here")
907
-
908
- # Audio player for question
909
- question_audio = gr.Audio(label="🔊 Listen to Question", autoplay=True, interactive=False)
910
-
911
- student_answer = gr.Textbox(
912
- label="Your Answer",
913
- placeholder="Type your answer here...",
914
- lines=4
915
- )
916
-
917
- submit_answer_btn = gr.Button("Submit Answer", variant="primary")
918
-
919
- feedback_display = gr.Markdown("Feedback will appear here after you submit your answer")
920
-
921
- # BOOK LEARNING MODE COLUMN
922
- with gr.Column(visible=False, elem_id="book_col") as book_col:
923
- # Upload PDF
924
- pdf_upload = gr.File(label="Upload Anatomy Textbook (PDF)", file_types=[".pdf"], type="binary")
925
- upload_status = gr.Markdown()
926
-
927
- # State to hold extracted images, captions, and text
928
- book_images_state = gr.State([])
929
- page_captions_state = gr.State([])
930
- page_texts_state = gr.State([])
931
-
932
- # Dropdown to select a page after processing
933
- page_dropdown = gr.Dropdown(label="Select Page", choices=[], interactive=False)
934
-
935
- # Display selected page image
936
- selected_page_image = gr.Image(label="Selected Page", type="pil")
937
-
938
- # Analysis output
939
- analysis_output = gr.Markdown(label="Page Analysis")
940
-
941
- # Button to start VIVA from this page
942
- start_viva_book_btn = gr.Button("🎯 Start VIVA Training from this Page", variant="primary", visible=False)
943
-
944
- # Process upload
945
- def handle_book_upload(pdf_bytes):
946
- extracted_data, status_msg = process_uploaded_book(pdf_bytes)
947
- if not extracted_data:
948
- # No data extracted
949
- return [], status_msg, [], [], gr.update(choices=[], interactive=False), None, ""
950
-
951
- # Separate images, captions, and text
952
- img_list = [item[0] for item in extracted_data]
953
- caps = [item[1] for item in extracted_data]
954
- texts = [item[2] for item in extracted_data]
955
-
956
- # Update dropdown with captions and enable it
957
- dropdown_update = gr.update(choices=caps, interactive=True)
958
- return img_list, status_msg, caps, texts, dropdown_update, None, ""
959
-
960
- pdf_upload.upload(
961
- fn=handle_book_upload,
962
- inputs=[pdf_upload],
963
- outputs=[book_images_state, upload_status, page_captions_state, page_texts_state, page_dropdown, selected_page_image, analysis_output]
964
- )
965
-
966
- # When a page is selected, show image and analysis
967
- def show_page_analysis(selected_caption, images, captions, texts):
968
- if not selected_caption:
969
- return None, ""
970
- # Find index
971
- try:
972
- idx = captions.index(selected_caption)
973
- except ValueError:
974
- return None, ""
975
-
976
- img = images[idx]
977
- text = texts[idx] if idx < len(texts) else ""
978
-
979
- analysis = analyze_book_image(img, selected_caption, text)
980
-
981
- # Construct a topic string for VIVA
982
- viva_topic = f"Anatomy of {selected_caption} (from textbook)"
983
-
984
- return img, analysis, viva_topic, gr.update(visible=True)
985
-
986
- # Hidden state to store current page topic for VIVA
987
- current_book_topic = gr.State("")
988
-
989
- page_dropdown.change(
990
- fn=show_page_analysis,
991
- inputs=[page_dropdown, book_images_state, page_captions_state, page_texts_state],
992
- outputs=[selected_page_image, analysis_output, current_book_topic, start_viva_book_btn]
993
- )
994
-
995
- # Start VIVA from Book Mode handler moved to end of file to resolve NameError
996
-
997
- # ADMIN PANEL COLUMN
998
- with gr.Column(visible=False, elem_id="admin_col") as admin_col:
999
- gr.Markdown("## Admin Panel - Student Database")
1000
- gr.Markdown("Enter the admin password to view registered students.")
1001
-
1002
- # Password input
1003
- with gr.Row():
1004
- admin_password_input = gr.Textbox(
1005
- label="Admin Password",
1006
- placeholder="Enter admin password",
1007
- type="password",
1008
- scale=2
1009
- )
1010
- admin_login_btn = gr.Button("🔓 Login", variant="primary", scale=1)
1011
-
1012
- admin_status = gr.Markdown("")
1013
-
1014
- # Admin content (hidden until authenticated)
1015
- with gr.Column(visible=False) as admin_content:
1016
- gr.Markdown("### 📊 Registered Students")
1017
-
1018
- admin_stats = gr.Markdown("")
1019
 
 
1020
  with gr.Row():
1021
- refresh_btn = gr.Button("🔄 Refresh Data", variant="secondary")
1022
- logout_btn = gr.Button("🚪 Logout", variant="secondary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1023
 
1024
- students_table = gr.Dataframe(
1025
- headers=["ID", "Name", "Medical School", "Year", "Registration Date"],
1026
- label="Students Database",
1027
- interactive=False,
1028
- wrap=True
1029
- )
1030
-
1031
- # Registration Modal Popup (shown on first load)
1032
- with gr.Column(visible=True, elem_id="registration_modal") as registration_modal:
1033
- with gr.Row():
1034
- with gr.Column(scale=1):
1035
- pass # Spacer
1036
- with gr.Column(scale=2):
1037
- gr.Markdown(
1038
- """
1039
- # 👨‍⚕️ Welcome to AnatomyBot!
1040
- ### Please enter your information to get started
1041
- """
1042
- )
1043
- modal_name_input = gr.Textbox(
1044
- label="Your Name",
1045
- placeholder="Enter your name",
1046
- lines=1
1047
- )
1048
- modal_school_input = gr.Textbox(
1049
- label="Medical School",
1050
- placeholder="Enter your medical school",
1051
- lines=1
1052
- )
1053
- modal_year_input = gr.Dropdown(
1054
- label="Year/Level",
1055
- choices=["MBBS 1st Year", "MBBS 2nd Year", "MBBS 3rd Year", "MBBS Final Year", "Intern"],
1056
- value=None
1057
- )
1058
- modal_submit_btn = gr.Button(
1059
- "✅ Start Learning",
1060
- variant="primary",
1061
- size="lg"
1062
- )
1063
- with gr.Column(scale=1):
1064
- pass # Spacer
1065
-
1066
-
1067
- # Event Handlers
1068
-
1069
- # Navigation Logic - change_view function returns exactly 4 values for 4 columns
1070
- def change_view(target_view):
1071
- """
1072
- Handle navigation between views with mutual exclusivity.
1073
-
1074
- Args:
1075
- target_view: The view to display ("learning", "viva", "book", or "admin")
1076
-
1077
- Returns:
1078
- Tuple of 4 gr.update() objects for [learning_col, viva_col, book_col, admin_col]
1079
- Exactly ONE will have visible=True, the rest will have visible=False
1080
- """
1081
- if target_view == "learning":
1082
- return (
1083
- gr.update(visible=True), # learning_col
1084
- gr.update(visible=False), # viva_col
1085
- gr.update(visible=False), # book_col
1086
- gr.update(visible=False) # admin_col
1087
- )
1088
- elif target_view == "viva":
1089
- return (
1090
- gr.update(visible=False), # learning_col
1091
- gr.update(visible=True), # viva_col
1092
- gr.update(visible=False), # book_col
1093
- gr.update(visible=False) # admin_col
1094
- )
1095
- elif target_view == "book":
1096
- return (
1097
- gr.update(visible=False), # learning_col
1098
- gr.update(visible=False), # viva_col
1099
- gr.update(visible=True), # book_col
1100
- gr.update(visible=False) # admin_col
1101
- )
1102
- elif target_view == "admin":
1103
- return (
1104
- gr.update(visible=False), # learning_col
1105
- gr.update(visible=False), # viva_col
1106
- gr.update(visible=False), # book_col
1107
- gr.update(visible=True) # admin_col
1108
- )
1109
- # Default to learning mode if invalid target
1110
- return (
1111
- gr.update(visible=True), # learning_col
1112
- gr.update(visible=False), # viva_col
1113
- gr.update(visible=False), # book_col
1114
- gr.update(visible=False) # admin_col
1115
- )
1116
-
1117
- # Bind Navigation Buttons - Apply change_view logic to all four top buttons
1118
- nav_learning_btn.click(
1119
- fn=lambda: change_view("learning"),
1120
- outputs=[learning_col, viva_col, book_col, admin_col]
1121
- )
1122
-
1123
- nav_viva_btn.click(
1124
- fn=lambda: change_view("viva"),
1125
- outputs=[learning_col, viva_col, book_col, admin_col]
1126
- )
1127
-
1128
- nav_book_btn.click(
1129
- fn=lambda: change_view("book"),
1130
- outputs=[learning_col, viva_col, book_col, admin_col]
1131
- )
1132
-
1133
- nav_admin_btn.click(
1134
- fn=lambda: change_view("admin"),
1135
- outputs=[learning_col, viva_col, book_col, admin_col]
1136
  )
1137
 
1138
- # Welcome Screen Handler (now for modal)
1139
- def handle_modal_submit(name, medical_school, year):
1140
- """Handle registration modal submission."""
1141
- if not name or not name.strip():
1142
- return gr.update(), gr.update(), "" # Don't proceed if name is empty
1143
-
1144
- if not medical_school or not medical_school.strip():
1145
- return gr.update(), gr.update(), "" # Don't proceed if medical school is empty
1146
-
1147
- if not year:
1148
- return gr.update(), gr.update(), "" # Don't proceed if year is not selected
1149
-
1150
- # Save to database
1151
- save_student(name.strip(), medical_school.strip(), year)
1152
 
1153
- greeting = f"**Welcome, Doctor {name}!** 👋 from {medical_school} ({year})"
1154
- return (
1155
- gr.update(visible=False), # Hide modal
1156
- greeting, # Display greeting
1157
- name # Store name in state
1158
- )
1159
-
1160
- modal_submit_btn.click(
1161
- fn=handle_modal_submit,
1162
- inputs=[modal_name_input, modal_school_input, modal_year_input],
1163
- outputs=[registration_modal, student_name_display, student_name_state],
1164
- js="""
1165
- (name, school, year) => {
1166
- if (name && name.trim() !== "" && school && school.trim() !== "" && year) {
1167
- const modal = document.getElementById('registration_modal');
1168
- if (modal) {
1169
- modal.style.display = 'none';
1170
- }
1171
- }
1172
- }
1173
- """
1174
- )
1175
-
1176
- # Event handlers for Learning Mode
1177
- def handle_query(query):
1178
- """Handle learning mode query and store topic/image."""
1179
- img, info, error = process_anatomy_query(query)
1180
- # Reset Start VIVA button
1181
- viva_btn_update = gr.update(value="🎯 Start VIVA Training", interactive=True)
1182
- return img, info, error, query, img, viva_btn_update # Return topic, image, and button update
1183
-
1184
- submit_btn.click(
1185
- fn=handle_query,
1186
- inputs=[query_input],
1187
- outputs=[image_output, info_output, error_output, current_topic, current_image_state, start_viva_btn]
1188
- )
1189
-
1190
- query_input.submit(
1191
- fn=handle_query,
1192
- inputs=[query_input],
1193
- outputs=[image_output, info_output, error_output, current_topic, current_image_state, start_viva_btn]
1194
- )
1195
-
1196
- # Start VIVA Mode - Directly start with pre-collected name
1197
- start_viva_btn.click(
1198
- fn=lambda: gr.update(value="⏳ Processing VIVA Question...", interactive=False),
1199
- outputs=[start_viva_btn]
1200
- ).then(
1201
- fn=lambda name, topic, image: start_viva_mode(topic, image, name),
1202
- inputs=[student_name_state, current_topic, current_image_state],
1203
- outputs=[
1204
- viva_container, viva_status, viva_image,
1205
- current_question_display, hint_display,
1206
- student_answer, feedback_display, submit_answer_btn,
1207
- viva_questions_state,
1208
- question_audio, # Output audio
1209
- student_name_state # Return student name (unchanged)
1210
- ]
1211
- ).then(
1212
- fn=lambda: change_view("viva"),
1213
- outputs=[learning_col, viva_col, book_col, admin_col]
1214
  ).then(
1215
- fn=lambda: gr.update(value="🎯 Start VIVA Training", interactive=True), # Reset button
1216
- outputs=[start_viva_btn]
 
1217
  ).then(
1218
- fn=lambda: 0, # Reset question index
1219
- outputs=[current_question_idx]
1220
- )
1221
-
1222
- # Submit VIVA Answer
1223
- submit_answer_btn.click(
1224
- fn=submit_viva_answer,
1225
- inputs=[student_answer, viva_questions_state, current_question_idx, student_name_state],
1226
- outputs=[
1227
- current_question_display, hint_display, student_answer,
1228
- feedback_display, submit_answer_btn, current_question_idx,
1229
- question_audio # Output audio for next question
1230
- ]
1231
- )
1232
-
1233
- # Admin Panel Handlers
1234
- def admin_login(password):
1235
- """Verify admin password and show admin content."""
1236
- if password == ADMIN_PASSWORD:
1237
- students = get_all_students()
1238
- total_students = len(students)
1239
- stats = f"**Total Registered Students:** {total_students}"
1240
- return (
1241
- gr.update(value="✅ Login successful!", visible=True),
1242
- gr.update(visible=True), # Show admin content
1243
- stats,
1244
- students
1245
- )
1246
- else:
1247
- return (
1248
- gr.update(value="❌ Invalid password. Access denied.", visible=True),
1249
- gr.update(visible=False), # Hide admin content
1250
- "",
1251
- []
1252
- )
1253
-
1254
- def admin_logout():
1255
- """Logout from admin panel."""
1256
- return (
1257
- gr.update(value=""), # Clear password
1258
- gr.update(value=""), # Clear status
1259
- gr.update(visible=False), # Hide admin content
1260
- "", # Clear stats
1261
- [] # Clear table
1262
- )
1263
-
1264
- def refresh_students():
1265
- """Refresh the students table."""
1266
- students = get_all_students()
1267
- total_students = len(students)
1268
- stats = f"**Total Registered Students:** {total_students}"
1269
- return stats, students
1270
-
1271
- admin_login_btn.click(
1272
- fn=admin_login,
1273
- inputs=[admin_password_input],
1274
- outputs=[admin_status, admin_content, admin_stats, students_table]
1275
- )
1276
-
1277
- admin_password_input.submit(
1278
- fn=admin_login,
1279
- inputs=[admin_password_input],
1280
- outputs=[admin_status, admin_content, admin_stats, students_table]
1281
- )
1282
-
1283
- logout_btn.click(
1284
- fn=admin_logout,
1285
- outputs=[admin_password_input, admin_status, admin_content, admin_stats, students_table]
1286
  )
1287
-
1288
- refresh_btn.click(
1289
- fn=refresh_students,
1290
- outputs=[admin_stats, students_table]
 
 
1291
  )
1292
 
1293
- # Start VIVA from Book Mode - Use pre-collected name (Moved here to ensure all columns are defined)
1294
- start_viva_book_btn.click(
1295
- fn=lambda name, topic, image: start_viva_with_name(name, topic, image),
1296
- inputs=[student_name_state, current_book_topic, selected_page_image],
1297
- outputs=[
1298
- viva_container, viva_status, viva_image,
1299
- current_question_display, hint_display,
1300
- student_answer, feedback_display, submit_answer_btn,
1301
- viva_questions_state,
1302
- question_audio, viva_greeting, student_name_state
1303
- ]
1304
- ).then(
1305
- fn=lambda: change_view("viva"),
1306
- outputs=[learning_col, viva_col, book_col, admin_col]
 
 
 
 
 
 
 
 
1307
  ).then(
1308
- fn=lambda: 0,
1309
- outputs=[current_question_idx]
1310
  )
1311
 
1312
- if __name__ == "__main__":
1313
- # Check if API keys are configured
1314
- if not SERPAPI_KEY or SERPAPI_KEY == "your_serpapi_key_here":
1315
- print("⚠️ WARNING: SERPAPI_KEY not configured in .env file")
1316
- if not HYPERBOLIC_API_KEY or HYPERBOLIC_API_KEY == "your_hyperbolic_api_key_here":
1317
- print("⚠️ WARNING: HYPERBOLIC_API_KEY not configured in .env file")
1318
-
1319
- # Use environment variable for port, default to 7860 for HF Spaces
1320
- port = int(os.getenv("GRADIO_SERVER_PORT", "7860"))
1321
- demo.launch(server_name="0.0.0.0", server_port=port)
1322
 
1323
- # Rebuild trigger
 
 
 
 
 
10
  import sqlite3
11
  from datetime import datetime
12
 
13
+ # Load environment variables
14
  load_dotenv()
15
 
16
  SERPAPI_KEY = os.getenv("SERPAPI_KEY")
17
  HYPERBOLIC_API_KEY = os.getenv("HYPERBOLIC_API_KEY")
18
  ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY")
 
 
19
  ADMIN_PASSWORD = "BT54iv!@"
 
 
20
  DB_PATH = "students.db"
21
 
22
+ # --- DATABASE FUNCTIONS ---
23
  def init_database():
 
24
  conn = sqlite3.connect(DB_PATH)
25
  cursor = conn.cursor()
26
  cursor.execute("""
 
36
  conn.close()
37
 
38
  def save_student(name, medical_school, year):
 
39
  try:
40
  conn = sqlite3.connect(DB_PATH)
41
  cursor = conn.cursor()
42
+ cursor.execute("INSERT INTO students (name, medical_school, year) VALUES (?, ?, ?)", (name, medical_school, year))
 
 
 
43
  conn.commit()
44
  conn.close()
45
  return True
 
48
  return False
49
 
50
  def get_all_students():
 
51
  try:
52
  conn = sqlite3.connect(DB_PATH)
53
  cursor = conn.cursor()
54
  cursor.execute("SELECT id, name, medical_school, year, registration_date FROM students ORDER BY registration_date DESC")
55
+ return cursor.fetchall()
56
+ except Exception:
 
 
 
57
  return []
58
 
 
59
  init_database()
60
 
61
+ # --- API CONFIGURATION ---
 
62
  HYPERBOLIC_API_URL = "https://api.hyperbolic.xyz/v1/chat/completions"
63
  HYPERBOLIC_MODEL = "meta-llama/Llama-3.3-70B-Instruct"
 
 
64
  ELEVENLABS_API_URL = "https://api.elevenlabs.io/v1/text-to-speech"
 
 
65
  ELEVENLABS_VOICE_ID = "nPczCjzI2devNBz1zQrb"
66
 
67
+ # --- LOGIC FUNCTIONS ---
68
  def generate_audio(text: str, student_name: str = None) -> str:
69
+ if not ELEVENLABS_API_KEY or not text: return None
70
+ if student_name: text = f"Welcome to Viva, Doctor {student_name}, let's start. {text}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  try:
72
  url = f"{ELEVENLABS_API_URL}/{ELEVENLABS_VOICE_ID}"
73
+ headers = {"Accept": "audio/mpeg", "Content-Type": "application/json", "xi-api-key": ELEVENLABS_API_KEY}
74
+ data = {"text": text, "model_id": "eleven_turbo_v2", "voice_settings": {"stability": 0.5, "similarity_boost": 0.5}}
 
 
 
 
 
 
 
 
 
 
 
 
75
  response = requests.post(url, json=data, headers=headers)
 
76
  if response.status_code == 200:
 
77
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as f:
78
  f.write(response.content)
 
79
  return f.name
80
+ return None
 
 
 
81
  except Exception as e:
82
+ print(f"Error generating audio: {e}")
83
  return None
84
 
85
  def is_anatomy_related(query: str) -> tuple[bool, str]:
86
+ prompt = f"Is this question related to human anatomy ONLY? '{query}'. Respond YES or NO."
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  try:
88
+ headers = {"Content-Type": "application/json", "Authorization": f"Bearer {HYPERBOLIC_API_KEY}"}
89
+ payload = {"model": HYPERBOLIC_MODEL, "messages": [{"role": "user", "content": prompt}], "max_tokens": 10}
 
 
 
 
 
 
 
 
 
 
90
  response = requests.post(HYPERBOLIC_API_URL, headers=headers, json=payload, timeout=10)
91
+ if "YES" in response.json()["choices"][0]["message"]["content"].upper(): return True, ""
92
+ return False, "⚠️ Please ask questions related to anatomy only."
93
+ except: return True, ""
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
  def search_anatomy_image(query: str) -> tuple[list, str]:
 
 
 
 
96
  try:
97
+ params = {"engine": "google_images", "q": f"{query} anatomy diagram", "api_key": SERPAPI_KEY, "num": 5, "safe": "active"}
98
+ data = requests.get("https://serpapi.com/search", params=params).json()
99
+ if "images_results" in data:
100
+ return [img["original"] for img in data["images_results"] if not img["original"].endswith('.svg')], ""
101
+ return [], "No images found."
102
+ except Exception as e: return [], str(e)
103
+
104
+ def download_image(url):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  try:
106
+ headers = {'User-Agent': 'Mozilla/5.0'}
107
+ return Image.open(BytesIO(requests.get(url, headers=headers, timeout=10).content))
108
+ except: return None
 
 
 
 
 
 
 
 
 
 
 
109
 
110
  def generate_anatomy_info(query: str) -> str:
 
 
 
111
  try:
112
+ headers = {"Content-Type": "application/json", "Authorization": f"Bearer {HYPERBOLIC_API_KEY}"}
113
+ prompt = f"Provide a detailed anatomy summary for medical students about: {query}. Use emojis for sections."
114
+ payload = {"model": HYPERBOLIC_MODEL, "messages": [{"role": "user", "content": prompt}], "max_tokens": 600}
115
+ return requests.post(HYPERBOLIC_API_URL, headers=headers, json=payload).json()["choices"][0]["message"]["content"]
116
+ except Exception as e: return f"Error: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
  def generate_viva_questions(topic: str) -> list:
 
 
 
 
119
  try:
120
+ headers = {"Content-Type": "application/json", "Authorization": f"Bearer {HYPERBOLIC_API_KEY}"}
121
+ prompt = f"Generate 5 hard anatomy VIVA questions on: {topic}. Format: Q1: ... HINT: ... ANSWER: ..."
122
+ payload = {"model": HYPERBOLIC_MODEL, "messages": [{"role": "user", "content": prompt}], "max_tokens": 800}
123
+ content = requests.post(HYPERBOLIC_API_URL, headers=headers, json=payload).json()["choices"][0]["message"]["content"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  questions = []
125
+ current = {}
126
+ for line in content.split('\n'):
 
 
127
  line = line.strip()
128
  if line.startswith('Q') and ':' in line:
129
+ if current: questions.append(current)
130
+ current = {'question': line.split(':', 1)[1].strip()}
131
+ elif line.startswith('HINT:'): current['hint'] = line.split(':', 1)[1].strip()
132
+ elif line.startswith('ANSWER:'): current['answer'] = line.split(':', 1)[1].strip()
133
+ if current: questions.append(current)
134
+ return questions[:5]
135
+ except: return []
136
+
137
+ def evaluate_viva_answer(question, student_ans, expected):
138
+ if not student_ans.strip(): return "Please provide an answer.", "⏸️"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  try:
140
+ headers = {"Content-Type": "application/json", "Authorization": f"Bearer {HYPERBOLIC_API_KEY}"}
141
+ prompt = f"Evaluate this VIVA answer. Question: {question}. Student: {student_ans}. Expected: {expected}. Give feedback and score."
142
+ payload = {"model": HYPERBOLIC_MODEL, "messages": [{"role": "user", "content": prompt}], "max_tokens": 400}
143
+ feedback = requests.post(HYPERBOLIC_API_URL, headers=headers, json=payload).json()["choices"][0]["message"]["content"]
144
+ emoji = "🌟" if "DISTINCTION" in feedback.upper() else "✅" if "PASS" in feedback.upper() else "⚠️"
145
+ return f"{emoji} **Feedback:**\n\n{feedback}\n\n📖 **Ref:** {expected}", emoji
146
+ except: return "Error evaluating.", "⚠️"
147
+
148
+ def process_anatomy_query(query):
149
+ if not query.strip(): return None, "", "Enter a question."
150
+ valid, msg = is_anatomy_related(query)
151
+ if not valid: return None, "", msg
152
+
153
+ urls, err = search_anatomy_image(query)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  info = generate_anatomy_info(query)
155
 
156
+ img = None
157
+ for url in urls:
158
+ img = download_image(url)
159
+ if img: break
 
 
 
 
 
 
 
 
 
160
 
161
+ return img, f"## 📚 Key Learning Points\n\n{info}", err
 
 
 
 
 
 
 
 
162
 
 
 
163
  def process_uploaded_book(pdf_file):
164
+ if not pdf_file: return [], "No file uploaded."
 
 
 
 
 
 
165
  try:
166
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp:
167
+ tmp.write(pdf_file)
168
+ tmp_path = tmp.name
169
+ images = convert_from_path(tmp_path, dpi=150)
170
+ reader = PyPDF2.PdfReader(tmp_path)
171
+ data = []
172
+ for i, img in enumerate(images):
173
+ txt = reader.pages[i].extract_text()[:2000] if i < len(reader.pages) else ""
174
+ data.append((img, f"Page {i+1}", txt))
175
+ os.unlink(tmp_path)
176
+ return data, f"Processed {len(data)} pages."
177
+ except Exception as e: return [], f"Error: {e}"
178
+
179
+ def analyze_book_image(image, page_info, page_text):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  try:
181
+ headers = {"Content-Type": "application/json", "Authorization": f"Bearer {HYPERBOLIC_API_KEY}"}
182
+ prompt = f"Analyze this anatomy book page ({page_info}): {page_text}. Give summary, clinical points, and 15 study questions."
183
+ payload = {"model": HYPERBOLIC_MODEL, "messages": [{"role": "user", "content": prompt}], "max_tokens": 1000}
184
+ return requests.post(HYPERBOLIC_API_URL, headers=headers, json=payload).json()["choices"][0]["message"]["content"]
185
+ except Exception as e: return f"Error: {e}"
186
+
187
+ def start_viva_mode(topic, image, name=""):
188
+ if not topic: return [gr.update()] * 11
189
+ qs = generate_viva_questions(topic)
190
+ if not qs: return [gr.update()] * 11
191
+
192
+ audio = generate_audio(qs[0]['question'], name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  return (
194
+ gr.update(visible=True), # Container
195
+ f"**VIVA ACTIVE:** {topic}", # Status
196
+ image, # Image
197
+ f"### Q1: {qs[0]['question']}", # Q Display
198
+ f"💡 {qs[0].get('hint','')}", # Hint
199
+ "", "", # Answer, Feedback
200
+ gr.update(interactive=True, value="Submit"), # Button
201
+ qs, audio, name
 
 
 
202
  )
203
 
204
+ def submit_viva_answer_logic(ans, qs, idx, name):
205
+ if idx >= len(qs): return "Done", "", "", "", gr.update(interactive=False), idx, None
206
+ fb, _ = evaluate_viva_answer(qs[idx]['question'], ans, qs[idx].get('answer',''))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
+ next_idx = idx + 1
209
+ if next_idx < len(qs):
210
+ nxt = qs[next_idx]
211
+ audio = generate_audio(nxt['question'], name)
212
+ return f"### Q{next_idx+1}: {nxt['question']}", f"💡 {nxt.get('hint','')}", "", fb, gr.update(), next_idx, audio
 
 
 
 
 
 
 
 
 
 
 
 
213
  else:
214
+ return "### 🎉 VIVA Complete!", "", "", fb, gr.update(interactive=False, value="Done"), next_idx, None
215
+
216
+ # --- GRADIO UI ---
217
+ with gr.Blocks(title="AnatomyBot", css="""
218
+ /* HIDE DEFAULT TABS HEADER to use Custom Nav */
219
+ .tabs > .tab-nav { display: none !important; }
220
+
221
+ #registration_modal {
222
+ position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
223
+ z-index: 1000; background: white; padding: 2rem; border-radius: 20px;
224
+ box-shadow: 0 10px 40px rgba(0,0,0,0.3); border: 2px solid #ff6b35; width: 80%; max-width: 600px;
225
+ }
226
+ body:has(#registration_modal:not([style*="display: none"]))::after {
227
+ content: ''; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 999;
228
+ }
229
+ #nav_bar { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 5px; }
230
+ #nav_bar button { flex: 1; white-space: nowrap; }
231
+ """) as demo:
232
+
233
+ # State
234
+ student_name = gr.State("")
235
+ viva_qs = gr.State([])
236
+ q_idx = gr.State(0)
237
+ cur_topic = gr.State("")
238
+ cur_img = gr.State(None)
239
+ cur_book_topic = gr.State("")
240
+
241
+ # Modal
242
+ with gr.Column(elem_id="registration_modal") as modal:
243
+ gr.Markdown("# 👨‍⚕️ Welcome, Doctor!")
244
+ m_name = gr.Textbox(label="Name")
245
+ m_school = gr.Textbox(label="Medical School")
246
+ m_year = gr.Dropdown(["Year 1", "Year 2", "Year 3", "Final Year", "Intern"], label="Year")
247
+ m_btn = gr.Button("Start Learning", variant="primary")
248
+
249
+ # Main App
250
+ with gr.Column():
251
+ gr.Markdown("# 🩺 AnatomyBot - MBBS Tutor")
252
+ welcome_msg = gr.Markdown("")
253
+
254
+ # Custom Nav
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  with gr.Row(elem_id="nav_bar"):
256
+ btn_learn = gr.Button("📚 Learning Mode", variant="primary")
257
+ btn_viva = gr.Button("🎯 VIVA Training", variant="secondary")
258
+ btn_book = gr.Button("📖 Book Mode", variant="secondary")
259
+ btn_admin = gr.Button("🔐 Admin", variant="secondary")
260
 
261
+ # TABS Container (This solves the overlapping issue)
262
+ with gr.Tabs(elem_id="main_tabs") as tabs:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
+ # TAB 1: LEARNING
265
+ with gr.TabItem("Learning", id="tab_learn"):
266
  with gr.Row():
267
+ q_in = gr.Textbox(label="Anatomy Question", placeholder="e.g. Circle of Willis")
268
+ with gr.Row():
269
+ b_search = gr.Button("🔍 Search", variant="primary")
270
+ b_to_viva = gr.Button("🎯 Start VIVA on this Topic", variant="secondary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
+ err_out = gr.Markdown()
273
  with gr.Row():
274
+ info_out = gr.Markdown()
275
+ img_out = gr.Image(type="pil", label="Diagram")
276
+
277
+ # TAB 2: VIVA
278
+ with gr.TabItem("VIVA", id="tab_viva"):
279
+ v_status = gr.Markdown("Select a topic in Learning Mode or Book Mode first!")
280
+ with gr.Column(visible=False) as v_cont:
281
+ with gr.Row():
282
+ v_img = gr.Image(interactive=False, type="pil", label="Reference")
283
+ with gr.Column():
284
+ v_q_disp = gr.Markdown("Question...")
285
+ v_hint = gr.Markdown("Hint...")
286
+ v_audio = gr.Audio(autoplay=True, interactive=False)
287
+ v_ans = gr.Textbox(label="Answer", lines=3)
288
+ v_sub = gr.Button("Submit")
289
+ v_fb = gr.Markdown()
290
+
291
+ # TAB 3: BOOK
292
+ with gr.TabItem("Book", id="tab_book"):
293
+ bk_file = gr.File(label="Upload PDF", file_types=[".pdf"], type="binary")
294
+ bk_stat = gr.Markdown()
295
 
296
+ # Book State
297
+ bk_imgs = gr.State([])
298
+ bk_caps = gr.State([])
299
+ bk_txts = gr.State([])
300
+
301
+ bk_page_sel = gr.Dropdown(label="Select Page", interactive=False)
302
+ bk_view = gr.Image(label="Page View", type="pil")
303
+ bk_anl = gr.Markdown()
304
+ b_bk_viva = gr.Button("🎯 Start VIVA from Page", visible=False)
305
+
306
+ # TAB 4: ADMIN
307
+ with gr.TabItem("Admin", id="tab_admin"):
308
+ p_in = gr.Textbox(label="Password", type="password")
309
+ b_login = gr.Button("Login")
310
+ with gr.Column(visible=False) as adm_panel:
311
+ adm_stat = gr.Markdown()
312
+ b_refresh = gr.Button("Refresh")
313
+ tbl = gr.Dataframe()
314
+
315
+ # --- EVENT HANDLERS ---
316
+
317
+ # 1. Modal Close
318
+ m_btn.click(
319
+ fn=lambda n,s,y: (gr.update(visible=False), f"**Doctor {n}** | {s}", n, save_student(n,s,y)),
320
+ inputs=[m_name, m_school, m_year],
321
+ outputs=[modal, welcome_msg, student_name, gr.State()] # dummy output for save
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  )
323
 
324
+ # 2. Navigation (Updates the Tabs 'selected' state)
325
+ btn_learn.click(lambda: gr.Tabs(selected="tab_learn"), outputs=tabs)
326
+ btn_viva.click(lambda: gr.Tabs(selected="tab_viva"), outputs=tabs)
327
+ btn_book.click(lambda: gr.Tabs(selected="tab_book"), outputs=tabs)
328
+ btn_admin.click(lambda: gr.Tabs(selected="tab_admin"), outputs=tabs)
329
+
330
+ # 3. Learning Mode
331
+ def run_search(q):
332
+ img, txt, err = process_anatomy_query(q)
333
+ return img, txt, err, q, img, gr.update(interactive=True)
 
 
 
 
334
 
335
+ b_search.click(run_search, q_in, [img_out, info_out, err_out, cur_topic, cur_img, b_to_viva])
336
+ q_in.submit(run_search, q_in, [img_out, info_out, err_out, cur_topic, cur_img, b_to_viva])
337
+
338
+ # 4. Start VIVA (Learning Mode)
339
+ b_to_viva.click(
340
+ fn=lambda: gr.update(value="Generating...", interactive=False), outputs=b_to_viva
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
  ).then(
342
+ fn=start_viva_mode,
343
+ inputs=[cur_topic, cur_img, student_name],
344
+ outputs=[v_cont, v_status, v_img, v_q_disp, v_hint, v_ans, v_fb, v_sub, viva_qs, v_audio, gr.State()]
345
  ).then(
346
+ fn=lambda: (gr.Tabs(selected="tab_viva"), 0, gr.update(value="Start VIVA", interactive=True)),
347
+ outputs=[tabs, q_idx, b_to_viva]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  )
349
+
350
+ # 5. VIVA Logic
351
+ v_sub.click(
352
+ fn=submit_viva_answer_logic,
353
+ inputs=[v_ans, viva_qs, q_idx, student_name],
354
+ outputs=[v_q_disp, v_hint, v_ans, v_fb, v_sub, q_idx, v_audio]
355
  )
356
 
357
+ # 6. Book Mode
358
+ def on_upload(f):
359
+ data, msg = process_uploaded_book(f)
360
+ if not data: return [], [], [], gr.update(choices=[]), msg
361
+ imgs, caps, txts = zip(*data)
362
+ return imgs, caps, txts, gr.update(choices=list(caps), interactive=True), msg
363
+
364
+ bk_file.upload(on_upload, bk_file, [bk_imgs, bk_caps, bk_txts, bk_page_sel, bk_stat])
365
+
366
+ def on_page_sel(sel, imgs, caps, txts):
367
+ if not sel: return None, "", "", gr.update()
368
+ idx = caps.index(sel)
369
+ anl = analyze_book_image(imgs[idx], sel, txts[idx])
370
+ return imgs[idx], anl, f"Textbook: {sel}", gr.update(visible=True)
371
+
372
+ bk_page_sel.change(on_page_sel, [bk_page_sel, bk_imgs, bk_caps, bk_txts], [bk_view, bk_anl, cur_book_topic, b_bk_viva])
373
+
374
+ # 7. Start VIVA (Book Mode)
375
+ b_bk_viva.click(
376
+ fn=start_viva_mode,
377
+ inputs=[cur_book_topic, bk_view, student_name],
378
+ outputs=[v_cont, v_status, v_img, v_q_disp, v_hint, v_ans, v_fb, v_sub, viva_qs, v_audio, gr.State()]
379
  ).then(
380
+ fn=lambda: (gr.Tabs(selected="tab_viva"), 0),
381
+ outputs=[tabs, q_idx]
382
  )
383
 
384
+ # 8. Admin
385
+ def do_login(p):
386
+ if p == ADMIN_PASSWORD:
387
+ d = get_all_students()
388
+ return gr.update(visible=True), f"Count: {len(d)}", d
389
+ return gr.update(visible=False), " Wrong Password", []
 
 
 
 
390
 
391
+ b_login.click(do_login, p_in, [adm_panel, adm_stat, tbl])
392
+ b_refresh.click(lambda: (f"Count: {len(get_all_students())}", get_all_students()), outputs=[adm_stat, tbl])
393
+
394
+ if __name__ == "__main__":
395
+ demo.launch(server_name="0.0.0.0", server_port=7860)