Deevyankar commited on
Commit
d169ded
·
verified ·
1 Parent(s): ce69158

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +337 -131
app.py CHANGED
@@ -1,8 +1,9 @@
1
  from __future__ import annotations
2
 
3
- import os
4
- import re
5
  import html
 
 
6
  from pathlib import Path
7
  from typing import List, Tuple, Dict, Optional
8
 
@@ -34,6 +35,7 @@ SEARCH_DIRS = [
34
  ]
35
 
36
  SUPPORTED_EXTENSIONS = {".txt", ".md", ".pdf", ".docx"}
 
37
  STOPWORDS = {
38
  "the", "is", "am", "are", "was", "were", "be", "been", "being",
39
  "a", "an", "and", "or", "of", "to", "in", "on", "for", "with",
@@ -58,6 +60,14 @@ def tokenize(text: str) -> List[str]:
58
  return [w for w in words if w not in STOPWORDS and len(w) > 1]
59
 
60
 
 
 
 
 
 
 
 
 
61
  def chunk_text(text: str, chunk_size: int = 900, overlap: int = 150) -> List[str]:
62
  text = normalize_spaces(text)
63
  if not text:
@@ -135,7 +145,7 @@ def extract_text_from_file(path: Path) -> str:
135
  return ""
136
 
137
 
138
- def find_asset(possible_names: List[str]) -> Optional[str]:
139
  lowered = [x.lower() for x in possible_names]
140
 
141
  for d in SEARCH_DIRS:
@@ -143,17 +153,35 @@ def find_asset(possible_names: List[str]) -> Optional[str]:
143
  for name in possible_names:
144
  p = d / name
145
  if p.exists() and p.is_file():
146
- return str(p)
147
 
148
  for d in SEARCH_DIRS:
149
  if d.exists():
150
  for p in d.rglob("*"):
151
  if p.is_file() and p.name.lower() in lowered:
152
- return str(p)
153
  return None
154
 
155
 
156
- LOGO_PATH = find_asset([
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  "Brain chat-09.png",
158
  "BrainChat-09.png",
159
  "brain chat-09.png",
@@ -162,8 +190,12 @@ LOGO_PATH = find_asset([
162
  "brainchat_logo.png",
163
  "logo.png",
164
  "Logo.png",
 
 
165
  ])
166
 
 
 
167
 
168
  # =========================================================
169
  # KNOWLEDGE BASE
@@ -225,7 +257,6 @@ class LocalKnowledgeBase:
225
  return []
226
 
227
  scored = []
228
-
229
  for item in self.chunks:
230
  overlap = len(q_tokens.intersection(item["tokens"]))
231
  if overlap == 0:
@@ -237,12 +268,12 @@ class LocalKnowledgeBase:
237
  scored.sort(key=lambda x: x[0], reverse=True)
238
 
239
  unique = []
240
- seen_text = set()
241
  for score, item in scored:
242
  key = (item["source"], item["chunk_id"])
243
- if key in seen_text:
244
  continue
245
- seen_text.add(key)
246
 
247
  result = dict(item)
248
  result["score"] = score
@@ -259,41 +290,204 @@ KB.load_from_directories()
259
 
260
 
261
  # =========================================================
262
- # RESPONSE LOGIC
263
  # =========================================================
264
- def build_answer_from_hits(query: str, hits: List[Dict], tutor_mode: str) -> Tuple[str, List[str]]:
265
  if not hits:
266
- return NOT_FOUND_TEXT, []
267
 
268
- mode = (tutor_mode or "Detailed").lower()
269
- max_snippets = 2 if mode == "brief" else 4
270
-
271
- selected = hits[:max_snippets]
272
- snippets = []
273
  sources = []
274
-
275
  seen_sources = set()
276
- for h in selected:
277
- snippet = h["text"].strip()
278
- if len(snippet) > 420 and mode == "brief":
279
- snippet = snippet[:420].rsplit(" ", 1)[0] + "..."
280
- elif len(snippet) > 750 and mode != "brief":
281
- snippet = snippet[:750].rsplit(" ", 1)[0] + "..."
282
-
283
- snippets.append(snippet)
284
-
285
- source_label = h["source"]
286
- if source_label not in seen_sources:
287
- seen_sources.add(source_label)
288
- sources.append(source_label)
289
-
290
- if mode == "brief":
291
- answer = "\n\n".join(snippets[:2])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  else:
293
- intro = f"Here is what I found related to: “{query.strip()}”\n\n"
294
- answer = intro + "\n\n".join(snippets)
295
 
296
- return answer.strip(), sources
297
 
298
 
299
  def format_answer(answer_text: str, sources: List[str], show_sources: bool) -> str:
@@ -312,6 +506,7 @@ def format_answer(answer_text: str, sources: List[str], show_sources: bool) -> s
312
 
313
  def get_answer_and_sources(
314
  message: str,
 
315
  tutor_mode: str,
316
  answer_language: str,
317
  quiz_questions: str
@@ -324,28 +519,13 @@ def get_answer_and_sources(
324
 
325
  if lower_msg in {"hi", "hello", "hey"}:
326
  return (
327
- "Hello. Ask me anything from your uploaded neurology or neuroanatomy material.",
 
328
  []
329
  )
330
 
331
- if "quiz" in lower_msg:
332
- hits = KB.search(msg, top_k=5)
333
- if not hits:
334
- return NOT_FOUND_TEXT, []
335
-
336
- qn = 5 if str(quiz_questions).lower() == "auto" else int(quiz_questions)
337
- base_text = hits[0]["text"]
338
-
339
- quiz = [f"**Mini Quiz ({qn} questions)**"]
340
- words = [w for w in re.findall(r"[A-Za-z][A-Za-z\-]+", base_text) if len(w) > 5][:qn]
341
-
342
- for i, w in enumerate(words[:qn], 1):
343
- quiz.append(f"{i}. Explain the term **{w}** in simple words.")
344
-
345
- return "\n".join(quiz), [hits[0]["source"]]
346
-
347
- hits = KB.search(msg, top_k=5)
348
- return build_answer_from_hits(msg, hits, tutor_mode)
349
 
350
 
351
  # =========================================================
@@ -353,66 +533,68 @@ def get_answer_and_sources(
353
  # =========================================================
354
  CUSTOM_CSS = """
355
  :root{
356
- --bg-main: #f6f7ff;
 
357
  --panel: #ffffff;
358
- --text: #202545;
359
- --muted: #616889;
360
- --primary: #6d28ff;
361
- --secondary: #ff48c4;
362
- --accent: #ffe94d;
363
- --accent2: #45d8ff;
364
- --border: #dde4ff;
 
365
  }
366
 
367
  html, body, .gradio-container {
368
- background: linear-gradient(180deg, #f6f7ff 0%, #fffbe0 100%) !important;
369
  color: var(--text) !important;
370
  font-family: "Segoe UI", Arial, sans-serif !important;
371
  }
372
 
373
  #main_shell {
374
- max-width: 1200px;
375
  margin: 18px auto;
376
- padding: 0 10px 18px 10px;
377
  }
378
 
379
  #topbar {
380
- background: linear-gradient(90deg, #6d28ff 0%, #ff48c4 60%, #ffe94d 100%);
381
- border-radius: 28px;
382
- padding: 16px 18px;
383
- box-shadow: 0 12px 28px rgba(80, 64, 170, 0.22);
384
- border: 2px solid rgba(255,255,255,0.65);
385
- margin-bottom: 16px;
386
  }
387
 
388
  #brand_row {
389
  display: flex;
390
  align-items: center;
391
- gap: 14px;
392
  }
393
 
394
  #brand_logo {
395
- width: 74px;
396
- height: 74px;
397
- border-radius: 18px;
398
- object-fit: cover;
399
- background: white;
400
- padding: 4px;
401
- box-shadow: 0 6px 18px rgba(0,0,0,0.15);
402
  }
403
 
404
  #brand_fallback {
405
- width: 74px;
406
- height: 74px;
407
- border-radius: 18px;
408
  display: flex;
409
  align-items: center;
410
  justify-content: center;
411
- background: white;
412
- color: #6d28ff;
413
- font-size: 24px;
414
  font-weight: 800;
415
- box-shadow: 0 6px 18px rgba(0,0,0,0.15);
416
  }
417
 
418
  #brand_title {
@@ -427,79 +609,89 @@ html, body, .gradio-container {
427
  font-size: 15px;
428
  color: #fffdfd;
429
  font-weight: 600;
430
- margin-top: 2px;
431
  }
432
 
433
  #settings_card, #chat_card {
434
- background: rgba(255,255,255,0.95) !important;
435
- border: 2px solid var(--border) !important;
436
- border-radius: 22px !important;
437
- box-shadow: 0 10px 26px rgba(71, 89, 160, 0.10);
438
  }
439
 
440
  #chatbot {
441
- background: linear-gradient(180deg, #fffdf6 0%, #f8fbff 100%) !important;
442
- border-radius: 18px !important;
443
  }
444
 
445
  #chatbot .message.user {
446
- background: linear-gradient(90deg, #6d28ff, #9247ff) !important;
447
  color: white !important;
448
  border-radius: 18px !important;
449
  }
450
 
451
  #chatbot .message.bot {
452
- background: #fff7bf !important;
453
- color: #202545 !important;
454
- border: 1px solid #eedb66 !important;
455
  border-radius: 18px !important;
456
  }
457
 
458
  textarea, input, .wrap textarea {
459
- border-radius: 16px !important;
460
- border: 2px solid #d8def8 !important;
461
- background: white !important;
462
  color: var(--text) !important;
463
  }
464
 
465
  button {
466
- border-radius: 16px !important;
467
  border: none !important;
468
  font-weight: 700 !important;
 
469
  }
470
 
471
  #send_btn {
472
- background: linear-gradient(90deg, #6d28ff, #ff48c4) !important;
473
  color: white !important;
474
  }
475
 
476
  #clear_btn {
477
- background: linear-gradient(90deg, #ffe94d, #ffd930) !important;
478
- color: #3a3000 !important;
479
  }
480
 
481
  #upload_btn {
482
- background: linear-gradient(90deg, #45d8ff, #6ef0c0) !important;
483
- color: #07304d !important;
484
  }
485
 
486
  #reload_btn {
487
- background: linear-gradient(90deg, #ffffff, #f2f5ff) !important;
488
- color: #334 !important;
489
- border: 2px solid #dce3ff !important;
490
  }
491
 
492
  .small_hint {
493
  color: var(--muted);
494
  font-size: 13px;
495
- margin-top: -4px;
 
 
 
 
 
 
 
 
 
496
  }
497
  """
498
 
499
 
500
  def build_header_html() -> str:
501
- if LOGO_PATH:
502
- logo_html = f'<img id="brand_logo" src="/file={LOGO_PATH}" alt="BrainChat logo">'
503
  else:
504
  logo_html = '<div id="brand_fallback">BC</div>'
505
 
@@ -516,22 +708,18 @@ def build_header_html() -> str:
516
  """
517
 
518
 
519
- def respond(message, history, tutor_mode, answer_language, quiz_questions, show_sources):
520
  history = history or []
521
 
522
  answer_text, sources = get_answer_and_sources(
523
  message=message,
 
524
  tutor_mode=tutor_mode,
525
  answer_language=answer_language,
526
  quiz_questions=quiz_questions
527
  )
528
 
529
- final_text = format_answer(
530
- answer_text=answer_text,
531
- sources=sources,
532
- show_sources=show_sources
533
- )
534
-
535
  history.append((message, final_text))
536
  return history, ""
537
 
@@ -544,7 +732,7 @@ def reload_materials():
544
  global KB
545
  KB = LocalKnowledgeBase()
546
  KB.load_from_directories()
547
- return "Course materials reloaded."
548
 
549
 
550
  def upload_files(files):
@@ -563,8 +751,8 @@ def upload_files(files):
563
  continue
564
 
565
  if added == 0:
566
- return "No readable text was found in the uploaded file(s)."
567
- return f"{added} file(s) added to the course material."
568
 
569
 
570
  with gr.Blocks(css=CUSTOM_CSS, title=APP_TITLE) as demo:
@@ -572,33 +760,51 @@ with gr.Blocks(css=CUSTOM_CSS, title=APP_TITLE) as demo:
572
  gr.HTML(build_header_html())
573
 
574
  with gr.Accordion("Settings", open=False, elem_id="settings_card"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
575
  tutor_mode = gr.Dropdown(
576
  ["Brief", "Detailed"],
577
  value="Detailed",
578
  label="Tutor Mode"
579
  )
 
580
  answer_language = gr.Dropdown(
581
  ["Auto", "English", "Spanish"],
582
  value="Auto",
583
  label="Answer Language"
584
  )
 
585
  quiz_questions = gr.Dropdown(
586
  ["Auto", "5", "10"],
587
  value="Auto",
588
- label="Quiz Questions"
589
  )
 
590
  show_sources = gr.Checkbox(
591
  value=True,
592
  label="Show Sources"
593
  )
 
594
  gr.Markdown(
595
- "Sources are shown only when useful text is found.",
596
  elem_classes=["small_hint"]
597
  )
598
 
599
  with gr.Column(elem_id="chat_card"):
600
  chatbot = gr.Chatbot(
601
- height=520,
602
  elem_id="chatbot",
603
  show_label=False
604
  )
@@ -607,7 +813,7 @@ with gr.Blocks(css=CUSTOM_CSS, title=APP_TITLE) as demo:
607
  file_input = gr.File(
608
  file_count="multiple",
609
  file_types=[".txt", ".md", ".pdf", ".docx"],
610
- label="",
611
  scale=2
612
  )
613
  msg = gr.Textbox(
@@ -626,13 +832,13 @@ with gr.Blocks(css=CUSTOM_CSS, title=APP_TITLE) as demo:
626
 
627
  send_btn.click(
628
  respond,
629
- inputs=[msg, chatbot, tutor_mode, answer_language, quiz_questions, show_sources],
630
  outputs=[chatbot, msg]
631
  )
632
 
633
  msg.submit(
634
  respond,
635
- inputs=[msg, chatbot, tutor_mode, answer_language, quiz_questions, show_sources],
636
  outputs=[chatbot, msg]
637
  )
638
 
 
1
  from __future__ import annotations
2
 
3
+ import base64
 
4
  import html
5
+ import mimetypes
6
+ import re
7
  from pathlib import Path
8
  from typing import List, Tuple, Dict, Optional
9
 
 
35
  ]
36
 
37
  SUPPORTED_EXTENSIONS = {".txt", ".md", ".pdf", ".docx"}
38
+
39
  STOPWORDS = {
40
  "the", "is", "am", "are", "was", "were", "be", "been", "being",
41
  "a", "an", "and", "or", "of", "to", "in", "on", "for", "with",
 
60
  return [w for w in words if w not in STOPWORDS and len(w) > 1]
61
 
62
 
63
+ def split_sentences(text: str) -> List[str]:
64
+ text = normalize_spaces(text)
65
+ if not text:
66
+ return []
67
+ parts = re.split(r"(?<=[.!?])\s+", text)
68
+ return [p.strip() for p in parts if p.strip()]
69
+
70
+
71
  def chunk_text(text: str, chunk_size: int = 900, overlap: int = 150) -> List[str]:
72
  text = normalize_spaces(text)
73
  if not text:
 
145
  return ""
146
 
147
 
148
+ def find_asset(possible_names: List[str]) -> Optional[Path]:
149
  lowered = [x.lower() for x in possible_names]
150
 
151
  for d in SEARCH_DIRS:
 
153
  for name in possible_names:
154
  p = d / name
155
  if p.exists() and p.is_file():
156
+ return p
157
 
158
  for d in SEARCH_DIRS:
159
  if d.exists():
160
  for p in d.rglob("*"):
161
  if p.is_file() and p.name.lower() in lowered:
162
+ return p
163
  return None
164
 
165
 
166
+ def file_to_data_uri(path: Optional[Path]) -> Optional[str]:
167
+ if not path or not path.exists():
168
+ return None
169
+ try:
170
+ data = path.read_bytes()
171
+ mime, _ = mimetypes.guess_type(str(path))
172
+ if mime is None:
173
+ if path.suffix.lower() == ".svg":
174
+ mime = "image/svg+xml"
175
+ else:
176
+ mime = "image/png"
177
+ encoded = base64.b64encode(data).decode("utf-8")
178
+ return f"data:{mime};base64,{encoded}"
179
+ except Exception:
180
+ return None
181
+
182
+
183
+ LOGO_FILE = find_asset([
184
+ "Brain Chat Imagen.svg",
185
  "Brain chat-09.png",
186
  "BrainChat-09.png",
187
  "brain chat-09.png",
 
190
  "brainchat_logo.png",
191
  "logo.png",
192
  "Logo.png",
193
+ "logo.svg",
194
+ "Logo.svg",
195
  ])
196
 
197
+ LOGO_URI = file_to_data_uri(LOGO_FILE)
198
+
199
 
200
  # =========================================================
201
  # KNOWLEDGE BASE
 
257
  return []
258
 
259
  scored = []
 
260
  for item in self.chunks:
261
  overlap = len(q_tokens.intersection(item["tokens"]))
262
  if overlap == 0:
 
268
  scored.sort(key=lambda x: x[0], reverse=True)
269
 
270
  unique = []
271
+ seen_keys = set()
272
  for score, item in scored:
273
  key = (item["source"], item["chunk_id"])
274
+ if key in seen_keys:
275
  continue
276
+ seen_keys.add(key)
277
 
278
  result = dict(item)
279
  result["score"] = score
 
290
 
291
 
292
  # =========================================================
293
+ # CONTENT BUILDERS
294
  # =========================================================
295
+ def collect_context(hits: List[Dict], max_chars: int = 2000) -> Tuple[str, List[str]]:
296
  if not hits:
297
+ return "", []
298
 
299
+ chunks = []
 
 
 
 
300
  sources = []
 
301
  seen_sources = set()
302
+ total = 0
303
+
304
+ for h in hits:
305
+ txt = normalize_spaces(h["text"])
306
+ if not txt:
307
+ continue
308
+
309
+ remaining = max_chars - total
310
+ if remaining <= 0:
311
+ break
312
+
313
+ piece = txt[:remaining]
314
+ chunks.append(piece)
315
+ total += len(piece)
316
+
317
+ src = h["source"]
318
+ if src not in seen_sources:
319
+ seen_sources.add(src)
320
+ sources.append(src)
321
+
322
+ return "\n\n".join(chunks), sources
323
+
324
+
325
+ def simple_definition_style(text: str, short: bool = False) -> str:
326
+ sentences = split_sentences(text)
327
+ if not sentences:
328
+ return NOT_FOUND_TEXT
329
+
330
+ if short:
331
+ chosen = sentences[:3]
332
+ return "\n\n".join(chosen)
333
+
334
+ chosen = sentences[:6]
335
+ return "\n\n".join(chosen)
336
+
337
+
338
+ def detailed_teaching_style(query: str, text: str) -> str:
339
+ sentences = split_sentences(text)
340
+ if not sentences:
341
+ return NOT_FOUND_TEXT
342
+
343
+ first = sentences[:2]
344
+ rest = sentences[2:6]
345
+
346
+ out = [f"**Topic:** {query.strip()}"]
347
+
348
+ if first:
349
+ out.append("\n**Simple explanation:**")
350
+ out.append(" ".join(first))
351
+
352
+ if rest:
353
+ out.append("\n**More detail:**")
354
+ for s in rest:
355
+ out.append(f"- {s}")
356
+
357
+ out.append("\n**Why this matters:**")
358
+ out.append("This concept is important because it helps students connect theory with brain structure, function, and clinical understanding.")
359
+
360
+ return "\n".join(out)
361
+
362
+
363
+ def build_flashcards(query: str, text: str, count: int = 5) -> str:
364
+ sentences = split_sentences(text)
365
+ words = [w for w in re.findall(r"[A-Za-z][A-Za-z\-]+", text) if len(w) > 5]
366
+
367
+ terms = []
368
+ seen = set()
369
+ for w in words:
370
+ wl = w.lower()
371
+ if wl not in seen:
372
+ seen.add(wl)
373
+ terms.append(w)
374
+ if len(terms) >= count:
375
+ break
376
+
377
+ if not terms:
378
+ terms = [query.title()]
379
+
380
+ out = ["**Flash Cards**"]
381
+ for i, term in enumerate(terms, 1):
382
+ meaning = sentences[min(i - 1, len(sentences) - 1)] if sentences else f"{term} is an important concept from the material."
383
+ out.append(f"\n**Card {i}**")
384
+ out.append(f"- **Term:** {term}")
385
+ out.append(f"- **Meaning:** {meaning}")
386
+ out.append(f"- **Remember:** Know the role, location, or function of {term.lower()}.")
387
+
388
+ return "\n".join(out)
389
+
390
+
391
+ def build_case_study(query: str, text: str) -> str:
392
+ sentences = split_sentences(text)
393
+ base = " ".join(sentences[:3]) if sentences else f"This topic is related to {query}."
394
+
395
+ return (
396
+ "**Case Study**\n\n"
397
+ f"A student is reviewing a patient-related topic connected to **{query}**. "
398
+ f"While studying, the student learns that: {base}\n\n"
399
+ "**Scenario:**\n"
400
+ "A patient comes with symptoms that may involve this part of the nervous system. "
401
+ "The student must explain what structure or concept is involved, what it normally does, "
402
+ "and what may happen when it is affected.\n\n"
403
+ "**Questions to think about:**\n"
404
+ "1. What is the main structure or idea here?\n"
405
+ "2. What is its normal function?\n"
406
+ "3. What symptoms may appear if it is damaged or disturbed?\n"
407
+ "4. Why is this topic important in neuroanatomy or neurology?\n\n"
408
+ "**Teacher-style note:**\n"
409
+ "This case study is for understanding and classroom learning, not for real medical diagnosis."
410
+ )
411
+
412
+
413
+ def build_quiz(query: str, text: str, n_questions: int = 5) -> str:
414
+ words = [w for w in re.findall(r"[A-Za-z][A-Za-z\-]+", text) if len(w) > 5]
415
+ unique_words = []
416
+ seen = set()
417
+
418
+ for w in words:
419
+ wl = w.lower()
420
+ if wl not in seen:
421
+ seen.add(wl)
422
+ unique_words.append(w)
423
+ if len(unique_words) >= n_questions:
424
+ break
425
+
426
+ if not unique_words:
427
+ unique_words = [query.title()] * n_questions
428
+
429
+ out = [f"**Quiz: {query.strip()}**"]
430
+ for i, w in enumerate(unique_words[:n_questions], 1):
431
+ out.append(f"{i}. What is **{w}**?")
432
+ out.append(f"{i}.a Why is **{w}** important?")
433
+ return "\n".join(out)
434
+
435
+
436
+ def build_revision_questions(query: str, text: str, n_questions: int = 5) -> str:
437
+ sentences = split_sentences(text)
438
+ out = [f"**Revision Questions: {query.strip()}**"]
439
+
440
+ for i in range(1, n_questions + 1):
441
+ if i == 1:
442
+ out.append(f"{i}. Define {query.strip()} in simple words.")
443
+ elif i == 2:
444
+ out.append(f"{i}. Explain the main function or role of {query.strip()}.")
445
+ elif i == 3:
446
+ out.append(f"{i}. Why is {query.strip()} important in neurology or neuroanatomy?")
447
+ elif i == 4:
448
+ out.append(f"{i}. Give one example related to {query.strip()}.")
449
+ else:
450
+ if sentences:
451
+ out.append(f"{i}. Based on the material, explain this statement: “{sentences[min(i-5, len(sentences)-1)]}”")
452
+ else:
453
+ out.append(f"{i}. Write short notes on {query.strip()}.")
454
+
455
+ return "\n".join(out)
456
+
457
+
458
+ def make_learning_output(
459
+ query: str,
460
+ learning_mode: str,
461
+ tutor_mode: str,
462
+ quiz_questions: str,
463
+ hits: List[Dict]
464
+ ) -> Tuple[str, List[str]]:
465
+ if not hits:
466
+ return NOT_FOUND_TEXT, []
467
+
468
+ context, sources = collect_context(hits, max_chars=2400)
469
+ mode = (learning_mode or "Detailed Explanation").strip().lower()
470
+
471
+ qn = 5 if str(quiz_questions).lower() == "auto" else int(quiz_questions)
472
+
473
+ if mode == "normal answer":
474
+ answer = simple_definition_style(context, short=False)
475
+ elif mode == "short explanation":
476
+ answer = simple_definition_style(context, short=True)
477
+ elif mode == "detailed explanation":
478
+ answer = detailed_teaching_style(query, context)
479
+ elif mode == "flash cards":
480
+ answer = build_flashcards(query, context, count=min(max(qn, 3), 8))
481
+ elif mode == "case study":
482
+ answer = build_case_study(query, context)
483
+ elif mode == "quiz":
484
+ answer = build_quiz(query, context, n_questions=qn)
485
+ elif mode == "revision questions":
486
+ answer = build_revision_questions(query, context, n_questions=qn)
487
  else:
488
+ answer = detailed_teaching_style(query, context)
 
489
 
490
+ return answer, sources
491
 
492
 
493
  def format_answer(answer_text: str, sources: List[str], show_sources: bool) -> str:
 
506
 
507
  def get_answer_and_sources(
508
  message: str,
509
+ learning_mode: str,
510
  tutor_mode: str,
511
  answer_language: str,
512
  quiz_questions: str
 
519
 
520
  if lower_msg in {"hi", "hello", "hey"}:
521
  return (
522
+ "Hello. Ask me anything from your neurology or neuroanatomy material. "
523
+ "You can also choose a learning mode such as flash cards, case study, quiz, or detailed explanation.",
524
  []
525
  )
526
 
527
+ hits = KB.search(msg, top_k=6)
528
+ return make_learning_output(msg, learning_mode, tutor_mode, quiz_questions, hits)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
529
 
530
 
531
  # =========================================================
 
533
  # =========================================================
534
  CUSTOM_CSS = """
535
  :root{
536
+ --bg1: #fffdf3;
537
+ --bg2: #fff4fb;
538
  --panel: #ffffff;
539
+ --text: #2f2a4d;
540
+ --muted: #736d92;
541
+ --primary: #7c3aed;
542
+ --secondary: #ec4899;
543
+ --third: #f59e0b;
544
+ --mint: #6ee7b7;
545
+ --sky: #67e8f9;
546
+ --border: #eadcff;
547
  }
548
 
549
  html, body, .gradio-container {
550
+ background: linear-gradient(180deg, #fffdf3 0%, #fff7fb 55%, #fff2d7 100%) !important;
551
  color: var(--text) !important;
552
  font-family: "Segoe UI", Arial, sans-serif !important;
553
  }
554
 
555
  #main_shell {
556
+ max-width: 1180px;
557
  margin: 18px auto;
558
+ padding: 0 12px 18px 12px;
559
  }
560
 
561
  #topbar {
562
+ background: linear-gradient(90deg, #7c3aed 0%, #ec4899 55%, #f4d03f 100%);
563
+ border-radius: 30px;
564
+ padding: 18px 20px;
565
+ box-shadow: 0 12px 28px rgba(167, 85, 190, 0.22);
566
+ border: 2px solid rgba(255,255,255,0.7);
567
+ margin-bottom: 18px;
568
  }
569
 
570
  #brand_row {
571
  display: flex;
572
  align-items: center;
573
+ gap: 16px;
574
  }
575
 
576
  #brand_logo {
577
+ width: 78px;
578
+ height: 78px;
579
+ border-radius: 20px;
580
+ object-fit: contain;
581
+ background: rgba(255,255,255,0.96);
582
+ padding: 6px;
583
+ box-shadow: 0 6px 18px rgba(0,0,0,0.12);
584
  }
585
 
586
  #brand_fallback {
587
+ width: 78px;
588
+ height: 78px;
589
+ border-radius: 20px;
590
  display: flex;
591
  align-items: center;
592
  justify-content: center;
593
+ background: rgba(255,255,255,0.96);
594
+ color: #7c3aed;
595
+ font-size: 28px;
596
  font-weight: 800;
597
+ box-shadow: 0 6px 18px rgba(0,0,0,0.12);
598
  }
599
 
600
  #brand_title {
 
609
  font-size: 15px;
610
  color: #fffdfd;
611
  font-weight: 600;
612
+ margin-top: 3px;
613
  }
614
 
615
  #settings_card, #chat_card {
616
+ background: rgba(255,255,255,0.92) !important;
617
+ border: 2px solid #eadcff !important;
618
+ border-radius: 24px !important;
619
+ box-shadow: 0 10px 24px rgba(120, 100, 180, 0.08);
620
  }
621
 
622
  #chatbot {
623
+ background: linear-gradient(180deg, #fffefb 0%, #fff7ff 100%) !important;
624
+ border-radius: 20px !important;
625
  }
626
 
627
  #chatbot .message.user {
628
+ background: linear-gradient(90deg, #8b5cf6, #ec4899) !important;
629
  color: white !important;
630
  border-radius: 18px !important;
631
  }
632
 
633
  #chatbot .message.bot {
634
+ background: linear-gradient(180deg, #fff8d9 0%, #fffdf3 100%) !important;
635
+ color: #2f2a4d !important;
636
+ border: 1px solid #f3df8c !important;
637
  border-radius: 18px !important;
638
  }
639
 
640
  textarea, input, .wrap textarea {
641
+ border-radius: 18px !important;
642
+ border: 2px solid #e3d8ff !important;
643
+ background: #ffffff !important;
644
  color: var(--text) !important;
645
  }
646
 
647
  button {
648
+ border-radius: 18px !important;
649
  border: none !important;
650
  font-weight: 700 !important;
651
+ min-height: 46px !important;
652
  }
653
 
654
  #send_btn {
655
+ background: linear-gradient(90deg, #7c3aed, #ec4899) !important;
656
  color: white !important;
657
  }
658
 
659
  #clear_btn {
660
+ background: linear-gradient(90deg, #fde047, #facc15) !important;
661
+ color: #4b3b00 !important;
662
  }
663
 
664
  #upload_btn {
665
+ background: linear-gradient(90deg, #67e8f9, #6ee7b7) !important;
666
+ color: #17324d !important;
667
  }
668
 
669
  #reload_btn {
670
+ background: linear-gradient(90deg, #f9f5ff, #fff2fb) !important;
671
+ color: #473a69 !important;
672
+ border: 2px solid #e6d7ff !important;
673
  }
674
 
675
  .small_hint {
676
  color: var(--muted);
677
  font-size: 13px;
678
+ }
679
+
680
+ .gr-file, .gr-file * {
681
+ background: #fffaf0 !important;
682
+ color: #2f2a4d !important;
683
+ border-color: #f3df8c !important;
684
+ }
685
+
686
+ footer {
687
+ display: none !important;
688
  }
689
  """
690
 
691
 
692
  def build_header_html() -> str:
693
+ if LOGO_URI:
694
+ logo_html = f'<img id="brand_logo" src="{LOGO_URI}" alt="BrainChat logo">'
695
  else:
696
  logo_html = '<div id="brand_fallback">BC</div>'
697
 
 
708
  """
709
 
710
 
711
+ def respond(message, history, learning_mode, tutor_mode, answer_language, quiz_questions, show_sources):
712
  history = history or []
713
 
714
  answer_text, sources = get_answer_and_sources(
715
  message=message,
716
+ learning_mode=learning_mode,
717
  tutor_mode=tutor_mode,
718
  answer_language=answer_language,
719
  quiz_questions=quiz_questions
720
  )
721
 
722
+ final_text = format_answer(answer_text, sources, show_sources)
 
 
 
 
 
723
  history.append((message, final_text))
724
  return history, ""
725
 
 
732
  global KB
733
  KB = LocalKnowledgeBase()
734
  KB.load_from_directories()
735
+ return f"Materials reloaded. Loaded {len(KB.docs)} document(s)."
736
 
737
 
738
  def upload_files(files):
 
751
  continue
752
 
753
  if added == 0:
754
+ return "No readable text found in uploaded files."
755
+ return f"{added} file(s) added successfully."
756
 
757
 
758
  with gr.Blocks(css=CUSTOM_CSS, title=APP_TITLE) as demo:
 
760
  gr.HTML(build_header_html())
761
 
762
  with gr.Accordion("Settings", open=False, elem_id="settings_card"):
763
+ learning_mode = gr.Dropdown(
764
+ [
765
+ "Normal Answer",
766
+ "Short Explanation",
767
+ "Detailed Explanation",
768
+ "Flash Cards",
769
+ "Case Study",
770
+ "Quiz",
771
+ "Revision Questions",
772
+ ],
773
+ value="Detailed Explanation",
774
+ label="Learning Mode"
775
+ )
776
+
777
  tutor_mode = gr.Dropdown(
778
  ["Brief", "Detailed"],
779
  value="Detailed",
780
  label="Tutor Mode"
781
  )
782
+
783
  answer_language = gr.Dropdown(
784
  ["Auto", "English", "Spanish"],
785
  value="Auto",
786
  label="Answer Language"
787
  )
788
+
789
  quiz_questions = gr.Dropdown(
790
  ["Auto", "5", "10"],
791
  value="Auto",
792
+ label="Quiz / Revision Count"
793
  )
794
+
795
  show_sources = gr.Checkbox(
796
  value=True,
797
  label="Show Sources"
798
  )
799
+
800
  gr.Markdown(
801
+ "Choose a learning mode such as flash cards, case study, quiz, short explanation, or detailed explanation.",
802
  elem_classes=["small_hint"]
803
  )
804
 
805
  with gr.Column(elem_id="chat_card"):
806
  chatbot = gr.Chatbot(
807
+ height=470,
808
  elem_id="chatbot",
809
  show_label=False
810
  )
 
813
  file_input = gr.File(
814
  file_count="multiple",
815
  file_types=[".txt", ".md", ".pdf", ".docx"],
816
+ label="Add material files (optional)",
817
  scale=2
818
  )
819
  msg = gr.Textbox(
 
832
 
833
  send_btn.click(
834
  respond,
835
+ inputs=[msg, chatbot, learning_mode, tutor_mode, answer_language, quiz_questions, show_sources],
836
  outputs=[chatbot, msg]
837
  )
838
 
839
  msg.submit(
840
  respond,
841
+ inputs=[msg, chatbot, learning_mode, tutor_mode, answer_language, quiz_questions, show_sources],
842
  outputs=[chatbot, msg]
843
  )
844