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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -200
app.py CHANGED
@@ -9,7 +9,6 @@ from typing import List, Tuple, Dict, Optional
9
 
10
  import gradio as gr
11
 
12
- # Optional readers
13
  try:
14
  from pypdf import PdfReader
15
  except Exception:
@@ -21,9 +20,6 @@ except Exception:
21
  docx = None
22
 
23
 
24
- # =========================================================
25
- # CONFIG
26
- # =========================================================
27
  APP_TITLE = "BrainChat"
28
  APP_SUBTITLE = "Neurology & neuroanatomy tutor"
29
  NOT_FOUND_TEXT = "Not found in the course material."
@@ -48,9 +44,6 @@ STOPWORDS = {
48
  }
49
 
50
 
51
- # =========================================================
52
- # HELPERS
53
- # =========================================================
54
  def normalize_spaces(text: str) -> str:
55
  return re.sub(r"\s+", " ", text).strip()
56
 
@@ -64,8 +57,7 @@ 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]:
@@ -170,10 +162,7 @@ def file_to_data_uri(path: Optional[Path]) -> Optional[str]:
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:
@@ -197,9 +186,6 @@ LOGO_FILE = find_asset([
197
  LOGO_URI = file_to_data_uri(LOGO_FILE)
198
 
199
 
200
- # =========================================================
201
- # KNOWLEDGE BASE
202
- # =========================================================
203
  class LocalKnowledgeBase:
204
  def __init__(self) -> None:
205
  self.docs: List[Dict] = []
@@ -218,13 +204,12 @@ class LocalKnowledgeBase:
218
  self.docs.append({"doc_id": doc_id, "source": source_name, "text": text})
219
 
220
  for idx, chunk in enumerate(chunk_text(text)):
221
- tokens = set(tokenize(chunk))
222
  self.chunks.append({
223
  "doc_id": doc_id,
224
  "source": source_name,
225
  "chunk_id": idx,
226
  "text": chunk,
227
- "tokens": tokens
228
  })
229
 
230
  def load_from_directories(self) -> None:
@@ -251,7 +236,7 @@ class LocalKnowledgeBase:
251
  if text.strip():
252
  self.add_document(path.name, text)
253
 
254
- def search(self, query: str, top_k: int = 5) -> List[Dict]:
255
  q_tokens = set(tokenize(query))
256
  if not q_tokens:
257
  return []
@@ -261,7 +246,6 @@ class LocalKnowledgeBase:
261
  overlap = len(q_tokens.intersection(item["tokens"]))
262
  if overlap == 0:
263
  continue
264
-
265
  score = overlap / max(1, len(q_tokens))
266
  scored.append((score, item))
267
 
@@ -289,10 +273,7 @@ KB = LocalKnowledgeBase()
289
  KB.load_from_directories()
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
 
@@ -326,13 +307,7 @@ 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:
@@ -340,11 +315,11 @@ def detailed_teaching_style(query: str, text: str) -> str:
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))
@@ -355,7 +330,7 @@ def detailed_teaching_style(query: str, text: str) -> str:
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
 
@@ -379,7 +354,7 @@ def build_flashcards(query: str, text: str, count: int = 5) -> str:
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}")
@@ -394,19 +369,17 @@ def build_case_study(query: str, text: str) -> str:
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
 
@@ -429,7 +402,7 @@ def build_quiz(query: str, text: str, n_questions: int = 5) -> str:
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
 
@@ -448,26 +421,19 @@ def build_revision_questions(query: str, text: str, n_questions: int = 5) -> str
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":
@@ -504,97 +470,86 @@ def format_answer(answer_text: str, sources: List[str], show_sources: bool) -> s
504
  return answer_text
505
 
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
513
- ) -> Tuple[str, List[str]]:
514
  msg = (message or "").strip()
515
  if not msg:
516
  return "Please type a question.", []
517
 
518
  lower_msg = msg.lower().strip()
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
- # =========================================================
532
- # UI
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 {
@@ -607,41 +562,36 @@ html, body, .gradio-container {
607
 
608
  #brand_subtitle {
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 {
@@ -652,37 +602,27 @@ button {
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
  }
@@ -708,19 +648,20 @@ def build_header_html() -> str:
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
 
726
 
@@ -735,27 +676,7 @@ def reload_materials():
735
  return f"Materials reloaded. Loaded {len(KB.docs)} document(s)."
736
 
737
 
738
- def upload_files(files):
739
- if not files:
740
- return "No file uploaded."
741
-
742
- added = 0
743
- for f in files:
744
- try:
745
- path = Path(f.name)
746
- text = extract_text_from_file(path)
747
- if text.strip():
748
- KB.add_document(path.name, text)
749
- added += 1
750
- except Exception:
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:
759
  with gr.Column(elem_id="main_shell"):
760
  gr.HTML(build_header_html())
761
 
@@ -774,18 +695,6 @@ with gr.Blocks(css=CUSTOM_CSS, title=APP_TITLE) as demo:
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",
@@ -804,26 +713,18 @@ with gr.Blocks(css=CUSTOM_CSS, title=APP_TITLE) as demo:
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
  )
811
 
812
- with gr.Row():
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(
820
- placeholder="Ask a question about neurology or neuroanatomy...",
821
- show_label=False,
822
- scale=6
823
- )
824
 
825
  with gr.Row():
826
- upload_btn = gr.Button("Upload Files", elem_id="upload_btn")
827
  reload_btn = gr.Button("Reload Materials", elem_id="reload_btn")
828
  clear_btn = gr.Button("Clear Chat", elem_id="clear_btn")
829
  send_btn = gr.Button("Send", elem_id="send_btn")
@@ -832,13 +733,13 @@ with gr.Blocks(css=CUSTOM_CSS, title=APP_TITLE) as demo:
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
 
@@ -852,11 +753,5 @@ with gr.Blocks(css=CUSTOM_CSS, title=APP_TITLE) as demo:
852
  outputs=[status_box]
853
  )
854
 
855
- upload_btn.click(
856
- upload_files,
857
- inputs=[file_input],
858
- outputs=[status_box]
859
- )
860
-
861
  if __name__ == "__main__":
862
- demo.launch()
 
9
 
10
  import gradio as gr
11
 
 
12
  try:
13
  from pypdf import PdfReader
14
  except Exception:
 
20
  docx = None
21
 
22
 
 
 
 
23
  APP_TITLE = "BrainChat"
24
  APP_SUBTITLE = "Neurology & neuroanatomy tutor"
25
  NOT_FOUND_TEXT = "Not found in the course material."
 
44
  }
45
 
46
 
 
 
 
47
  def normalize_spaces(text: str) -> str:
48
  return re.sub(r"\s+", " ", text).strip()
49
 
 
57
  text = normalize_spaces(text)
58
  if not text:
59
  return []
60
+ return [p.strip() for p in re.split(r"(?<=[.!?])\s+", text) if p.strip()]
 
61
 
62
 
63
  def chunk_text(text: str, chunk_size: int = 900, overlap: int = 150) -> List[str]:
 
162
  data = path.read_bytes()
163
  mime, _ = mimetypes.guess_type(str(path))
164
  if mime is None:
165
+ mime = "image/svg+xml" if path.suffix.lower() == ".svg" else "image/png"
 
 
 
166
  encoded = base64.b64encode(data).decode("utf-8")
167
  return f"data:{mime};base64,{encoded}"
168
  except Exception:
 
186
  LOGO_URI = file_to_data_uri(LOGO_FILE)
187
 
188
 
 
 
 
189
  class LocalKnowledgeBase:
190
  def __init__(self) -> None:
191
  self.docs: List[Dict] = []
 
204
  self.docs.append({"doc_id": doc_id, "source": source_name, "text": text})
205
 
206
  for idx, chunk in enumerate(chunk_text(text)):
 
207
  self.chunks.append({
208
  "doc_id": doc_id,
209
  "source": source_name,
210
  "chunk_id": idx,
211
  "text": chunk,
212
+ "tokens": set(tokenize(chunk))
213
  })
214
 
215
  def load_from_directories(self) -> None:
 
236
  if text.strip():
237
  self.add_document(path.name, text)
238
 
239
+ def search(self, query: str, top_k: int = 6) -> List[Dict]:
240
  q_tokens = set(tokenize(query))
241
  if not q_tokens:
242
  return []
 
246
  overlap = len(q_tokens.intersection(item["tokens"]))
247
  if overlap == 0:
248
  continue
 
249
  score = overlap / max(1, len(q_tokens))
250
  scored.append((score, item))
251
 
 
273
  KB.load_from_directories()
274
 
275
 
276
+ def collect_context(hits: List[Dict], max_chars: int = 2400) -> Tuple[str, List[str]]:
 
 
 
277
  if not hits:
278
  return "", []
279
 
 
307
  sentences = split_sentences(text)
308
  if not sentences:
309
  return NOT_FOUND_TEXT
310
+ return "\n\n".join(sentences[:3] if short else sentences[:6])
 
 
 
 
 
 
311
 
312
 
313
  def detailed_teaching_style(query: str, text: str) -> str:
 
315
  if not sentences:
316
  return NOT_FOUND_TEXT
317
 
318
+ out = [f"**Topic:** {query.strip()}"]
319
+
320
  first = sentences[:2]
321
  rest = sentences[2:6]
322
 
 
 
323
  if first:
324
  out.append("\n**Simple explanation:**")
325
  out.append(" ".join(first))
 
330
  out.append(f"- {s}")
331
 
332
  out.append("\n**Why this matters:**")
333
+ out.append("This concept is important because it helps students connect structure, function, and clinical understanding.")
334
 
335
  return "\n".join(out)
336
 
 
354
 
355
  out = ["**Flash Cards**"]
356
  for i, term in enumerate(terms, 1):
357
+ meaning = sentences[min(i - 1, len(sentences) - 1)] if sentences else f"{term} is an important concept."
358
  out.append(f"\n**Card {i}**")
359
  out.append(f"- **Term:** {term}")
360
  out.append(f"- **Meaning:** {meaning}")
 
369
 
370
  return (
371
  "**Case Study**\n\n"
372
+ f"A student is studying **{query}**. From the material, the student learns that: {base}\n\n"
 
373
  "**Scenario:**\n"
374
+ "A patient shows symptoms that may involve this part of the nervous system. "
375
  "The student must explain what structure or concept is involved, what it normally does, "
376
  "and what may happen when it is affected.\n\n"
377
  "**Questions to think about:**\n"
378
  "1. What is the main structure or idea here?\n"
379
  "2. What is its normal function?\n"
380
+ "3. What symptoms may appear if it is damaged?\n"
381
  "4. Why is this topic important in neuroanatomy or neurology?\n\n"
382
+ "**Note:** This is for classroom learning, not real diagnosis."
 
383
  )
384
 
385
 
 
402
  out = [f"**Quiz: {query.strip()}**"]
403
  for i, w in enumerate(unique_words[:n_questions], 1):
404
  out.append(f"{i}. What is **{w}**?")
405
+ out.append(f"{i}a. Why is **{w}** important?")
406
  return "\n".join(out)
407
 
408
 
 
421
  out.append(f"{i}. Give one example related to {query.strip()}.")
422
  else:
423
  if sentences:
424
+ out.append(f'{i}. Explain this statement: "{sentences[min(i-5, len(sentences)-1)]}"')
425
  else:
426
  out.append(f"{i}. Write short notes on {query.strip()}.")
427
 
428
  return "\n".join(out)
429
 
430
 
431
+ def make_learning_output(query: str, learning_mode: str, quiz_questions: str, hits: List[Dict]) -> Tuple[str, List[str]]:
 
 
 
 
 
 
432
  if not hits:
433
  return NOT_FOUND_TEXT, []
434
 
435
  context, sources = collect_context(hits, max_chars=2400)
436
  mode = (learning_mode or "Detailed Explanation").strip().lower()
 
437
  qn = 5 if str(quiz_questions).lower() == "auto" else int(quiz_questions)
438
 
439
  if mode == "normal answer":
 
470
  return answer_text
471
 
472
 
473
+ def get_answer_and_sources(message: str, learning_mode: str, quiz_questions: str) -> Tuple[str, List[str]]:
 
 
 
 
 
 
474
  msg = (message or "").strip()
475
  if not msg:
476
  return "Please type a question.", []
477
 
478
  lower_msg = msg.lower().strip()
 
479
  if lower_msg in {"hi", "hello", "hey"}:
480
  return (
481
  "Hello. Ask me anything from your neurology or neuroanatomy material. "
482
+ "You can choose flash cards, case study, quiz, short explanation, or detailed explanation.",
483
  []
484
  )
485
 
486
  hits = KB.search(msg, top_k=6)
487
+ return make_learning_output(msg, learning_mode, quiz_questions, hits)
488
 
489
 
 
 
 
490
  CUSTOM_CSS = """
491
  :root{
492
+ --wa-bg: #efeae2;
493
+ --wa-panel: #f7f5f2;
494
+ --wa-header: #075e54;
495
+ --wa-header-2: #128c7e;
496
+ --wa-text: #1f2c34;
497
+ --wa-muted: #667781;
498
+ --wa-border: #d8e0dc;
499
+ --wa-user: #d9fdd3;
500
+ --wa-bot: #ffffff;
501
+ --wa-btn: #25d366;
502
+ --wa-btn-dark: #128c7e;
503
+ --wa-input: #ffffff;
504
  }
505
 
506
  html, body, .gradio-container {
507
+ background: linear-gradient(180deg, #e6ddd4 0%, #efeae2 100%) !important;
508
+ color: var(--wa-text) !important;
509
  font-family: "Segoe UI", Arial, sans-serif !important;
510
  }
511
 
512
  #main_shell {
513
+ max-width: 1100px;
514
  margin: 18px auto;
515
+ padding: 0 10px 18px 10px;
516
  }
517
 
518
  #topbar {
519
+ background: linear-gradient(90deg, #075e54 0%, #128c7e 100%);
520
+ border-radius: 22px;
521
+ padding: 16px 18px;
522
+ box-shadow: 0 10px 22px rgba(7, 94, 84, 0.20);
523
+ margin-bottom: 16px;
 
524
  }
525
 
526
  #brand_row {
527
  display: flex;
528
  align-items: center;
529
+ gap: 14px;
530
  }
531
 
532
  #brand_logo {
533
+ width: 68px;
534
+ height: 68px;
535
+ border-radius: 16px;
536
  object-fit: contain;
537
+ background: white;
538
+ padding: 5px;
539
+ box-shadow: 0 4px 10px rgba(0,0,0,0.12);
540
  }
541
 
542
  #brand_fallback {
543
+ width: 68px;
544
+ height: 68px;
545
+ border-radius: 16px;
546
  display: flex;
547
  align-items: center;
548
  justify-content: center;
549
+ background: white;
550
+ color: #075e54;
551
+ font-size: 26px;
552
  font-weight: 800;
 
553
  }
554
 
555
  #brand_title {
 
562
 
563
  #brand_subtitle {
564
  font-size: 15px;
565
+ color: #dcf8c6;
566
  font-weight: 600;
567
  margin-top: 3px;
568
  }
569
 
570
  #settings_card, #chat_card {
571
+ background: rgba(247,245,242,0.96) !important;
572
+ border: 1px solid var(--wa-border) !important;
 
 
 
 
 
 
573
  border-radius: 20px !important;
574
+ box-shadow: 0 8px 20px rgba(80, 90, 90, 0.08);
575
  }
576
 
577
+ #chatbot {
578
+ background:
579
+ linear-gradient(rgba(239,234,226,0.94), rgba(239,234,226,0.94)),
580
+ repeating-linear-gradient(
581
+ 45deg,
582
+ rgba(255,255,255,0.08) 0px,
583
+ rgba(255,255,255,0.08) 12px,
584
+ rgba(0,0,0,0.02) 12px,
585
+ rgba(0,0,0,0.02) 24px
586
+ ) !important;
587
  border-radius: 18px !important;
588
  }
589
 
590
  textarea, input, .wrap textarea {
591
  border-radius: 18px !important;
592
+ border: 1px solid #d5ddd9 !important;
593
+ background: var(--wa-input) !important;
594
+ color: var(--wa-text) !important;
595
  }
596
 
597
  button {
 
602
  }
603
 
604
  #send_btn {
605
+ background: linear-gradient(90deg, #25d366, #128c7e) !important;
606
  color: white !important;
607
  }
608
 
609
  #clear_btn {
610
+ background: #ffffff !important;
611
+ color: #075e54 !important;
612
+ border: 1px solid #bfd6cf !important;
 
 
 
 
613
  }
614
 
615
  #reload_btn {
616
+ background: #e7f7ee !important;
617
+ color: #075e54 !important;
618
+ border: 1px solid #bfe6d0 !important;
619
  }
620
 
621
  .small_hint {
622
+ color: var(--wa-muted);
623
  font-size: 13px;
624
  }
625
 
 
 
 
 
 
 
626
  footer {
627
  display: none !important;
628
  }
 
648
  """
649
 
650
 
651
+ def respond(message, history, learning_mode, quiz_questions, show_sources):
652
  history = history or []
653
 
654
  answer_text, sources = get_answer_and_sources(
655
  message=message,
656
  learning_mode=learning_mode,
 
 
657
  quiz_questions=quiz_questions
658
  )
659
 
660
  final_text = format_answer(answer_text, sources, show_sources)
661
+
662
+ history.append({"role": "user", "content": message})
663
+ history.append({"role": "assistant", "content": final_text})
664
+
665
  return history, ""
666
 
667
 
 
676
  return f"Materials reloaded. Loaded {len(KB.docs)} document(s)."
677
 
678
 
679
+ with gr.Blocks(title=APP_TITLE) as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
680
  with gr.Column(elem_id="main_shell"):
681
  gr.HTML(build_header_html())
682
 
 
695
  label="Learning Mode"
696
  )
697
 
 
 
 
 
 
 
 
 
 
 
 
 
698
  quiz_questions = gr.Dropdown(
699
  ["Auto", "5", "10"],
700
  value="Auto",
 
713
 
714
  with gr.Column(elem_id="chat_card"):
715
  chatbot = gr.Chatbot(
716
+ height=500,
717
  elem_id="chatbot",
718
+ show_label=False,
719
+ type="messages"
720
  )
721
 
722
+ msg = gr.Textbox(
723
+ placeholder="Type your question here...",
724
+ show_label=False
725
+ )
 
 
 
 
 
 
 
 
726
 
727
  with gr.Row():
 
728
  reload_btn = gr.Button("Reload Materials", elem_id="reload_btn")
729
  clear_btn = gr.Button("Clear Chat", elem_id="clear_btn")
730
  send_btn = gr.Button("Send", elem_id="send_btn")
 
733
 
734
  send_btn.click(
735
  respond,
736
+ inputs=[msg, chatbot, learning_mode, quiz_questions, show_sources],
737
  outputs=[chatbot, msg]
738
  )
739
 
740
  msg.submit(
741
  respond,
742
+ inputs=[msg, chatbot, learning_mode, quiz_questions, show_sources],
743
  outputs=[chatbot, msg]
744
  )
745
 
 
753
  outputs=[status_box]
754
  )
755
 
 
 
 
 
 
 
756
  if __name__ == "__main__":
757
+ demo.launch(css=CUSTOM_CSS)