Deevyankar commited on
Commit
cdfda82
·
verified ·
1 Parent(s): 2fead40

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +563 -586
app.py CHANGED
@@ -1,679 +1,656 @@
 
 
1
  import os
2
  import re
3
- import json
4
- import pickle
5
- from urllib.parse import quote
6
 
7
- import numpy as np
8
  import gradio as gr
9
- from rank_bm25 import BM25Okapi
10
- from sentence_transformers import SentenceTransformer
11
- from openai import OpenAI
12
-
13
- # ============================================================
14
- # Configuration
15
- # ============================================================
16
- BUILD_DIR = "brainchat_build"
17
- CHUNKS_PATH = os.path.join(BUILD_DIR, "chunks.pkl")
18
- TOKENS_PATH = os.path.join(BUILD_DIR, "tokenized_chunks.pkl")
19
- EMBED_PATH = os.path.join(BUILD_DIR, "embeddings.npy")
20
- CONFIG_PATH = os.path.join(BUILD_DIR, "config.json")
21
-
22
- # Put ONE of these logo files in your Space repo root (same folder as app.py)
23
- LOGO_CANDIDATES = [
24
- "Brain chat-09.png",
25
- "brainchat_logo.png.png",
26
- "Brain Chat Imagen.svg",
27
- "ebcbb9f5-022f-473a-bf51-7e7974f794b4.png",
 
 
 
 
 
28
  ]
29
 
30
- MODEL_NAME_TEXT = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
 
 
 
 
 
 
 
 
 
 
31
 
32
- # ============================================================
33
- # Globals (lazy loaded)
34
- # ============================================================
35
- BM25 = None
36
- CHUNKS = None
37
- EMBEDDINGS = None
38
- EMBED_MODEL = None
39
- CLIENT = None
40
 
 
 
 
 
 
41
 
42
- # ============================================================
43
- # Utilities
44
- # ============================================================
45
- def tokenize(text: str):
46
- return re.findall(r"\w+", text.lower(), flags=re.UNICODE)
47
 
 
 
 
48
 
49
- def ensure_loaded():
50
- global BM25, CHUNKS, EMBEDDINGS, EMBED_MODEL, CLIENT
51
 
52
- if CHUNKS is None:
53
- missing = [p for p in [CHUNKS_PATH, TOKENS_PATH, EMBED_PATH, CONFIG_PATH] if not os.path.exists(p)]
54
- if missing:
55
- raise FileNotFoundError(
56
- "Missing build files. Make sure you ran the build step and committed brainchat_build/.\n"
57
- + "\n".join(missing)
58
- )
59
 
60
- with open(CHUNKS_PATH, "rb") as f:
61
- CHUNKS = pickle.load(f)
 
62
 
63
- with open(TOKENS_PATH, "rb") as f:
64
- tokenized_chunks = pickle.load(f)
 
65
 
66
- EMBEDDINGS = np.load(EMBED_PATH)
 
 
 
 
 
67
 
68
- with open(CONFIG_PATH, "r", encoding="utf-8") as f:
69
- cfg = json.load(f)
70
 
71
- BM25 = BM25Okapi(tokenized_chunks)
72
- EMBED_MODEL = SentenceTransformer(cfg["embedding_model"])
73
 
74
- if CLIENT is None:
75
- api_key = os.getenv("OPENAI_API_KEY")
76
- if not api_key:
77
- raise ValueError("OPENAI_API_KEY is missing. Add it in your Space Secrets.")
78
- CLIENT = OpenAI(api_key=api_key)
79
 
 
80
 
81
- def search_hybrid(query: str, shortlist_k: int = 30, final_k: int = 5):
82
- ensure_loaded()
83
 
84
- q_tokens = tokenize(query)
85
- bm25_scores = BM25.get_scores(q_tokens)
86
- shortlist_idx = np.argsort(bm25_scores)[::-1][:shortlist_k]
 
 
 
 
 
87
 
88
- qvec = EMBED_MODEL.encode([query], normalize_embeddings=True).astype("float32")[0]
89
- shortlist_emb = EMBEDDINGS[shortlist_idx]
90
- dense_scores = shortlist_emb @ qvec
91
 
92
- rerank = np.argsort(dense_scores)[::-1][:final_k]
93
- final_idx = shortlist_idx[rerank]
94
- return [CHUNKS[int(i)] for i in final_idx]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
 
97
- def build_context(records):
98
- blocks = []
99
- for i, r in enumerate(records, start=1):
100
- blocks.append(
101
- f"""[Source {i}]
102
- Book: {r.get('book','')}
103
- Section: {r.get('section_title','')}
104
- Pages: {r.get('page_start','')}-{r.get('page_end','')}
105
- Text:
106
- {r.get('text','')}"""
107
- )
108
- return "\n\n".join(blocks)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
- def make_sources(records):
112
- seen = set()
113
- lines = []
114
- for r in records:
115
- key = (r.get("book"), r.get("section_title"), r.get("page_start"), r.get("page_end"))
116
- if key in seen:
117
- continue
118
- seen.add(key)
119
- lines.append(
120
- f"• {r.get('book','')} | {r.get('section_title','')} | pp. {r.get('page_start','')}-{r.get('page_end','')}"
121
- )
122
- return "\n".join(lines)
123
-
124
-
125
- def choose_quiz_count(user_text: str, selector: str) -> int:
126
- if selector in {"3", "5", "7"}:
127
- return int(selector)
128
-
129
- t = user_text.lower()
130
- if any(k in t for k in ["mock test", "final exam", "exam practice", "full test"]):
131
- return 7
132
- if any(k in t for k in ["detailed", "revision", "comprehensive", "study"]):
133
- return 5
134
- return 3
135
-
136
-
137
- def language_instruction(language_mode: str) -> str:
138
- if language_mode == "English":
139
- return "Answer only in English."
140
- if language_mode == "Spanish":
141
- return "Answer only in Spanish."
142
- if language_mode == "Bilingual":
143
- return "Answer first in English, then provide a Spanish version under the heading 'Español:'."
144
- return "If the user writes in Spanish, answer in Spanish; otherwise answer in English."
145
-
146
-
147
- def build_tutor_prompt(mode: str, language_mode: str, question: str, context: str) -> str:
148
- mode_map = {
149
- "Explain": (
150
- "Explain clearly like a friendly tutor using simple language. "
151
- "Use short headings if helpful."
152
- ),
153
- "Detailed": (
154
- "Give a detailed explanation. Include key terms and clinical relevance only if supported by the context."
155
- ),
156
- "Short Notes": "Write concise revision notes using bullet points.",
157
- "Flashcards": "Create 6 flashcards in Q/A format.",
158
- "Case-Based": (
159
- "Create a short clinical scenario (2–4 lines) and then explain the underlying concept using the context."
160
- ),
161
- }
162
 
163
- return f"""
164
- You are BrainChat, an interactive neurology and neuroanatomy tutor.
165
 
166
- Rules:
167
- - Use ONLY the provided context from the books.
168
- - If the answer is not supported by the context, say exactly:
169
- Not found in the course material.
170
- - Do not invent facts outside the context.
171
- - {language_instruction(language_mode)}
 
 
 
 
172
 
173
- Teaching style:
174
- {mode_map.get(mode, mode_map['Explain'])}
175
 
176
- Context:
177
- {context}
178
 
179
- Student question:
180
- {question}
181
- """.strip()
182
 
 
 
183
 
184
- def build_quiz_generation_prompt(language_mode: str, topic: str, context: str, n_questions: int) -> str:
185
- return f"""
186
- You are BrainChat, an interactive tutor.
187
-
188
- Rules:
189
- - Use ONLY the provided context.
190
- - Create exactly {n_questions} quiz questions.
191
- - Questions should be short, clear, and course-aligned.
192
- - Provide a short answer key per question.
193
- - Return VALID JSON only.
194
- - {language_instruction(language_mode)}
195
-
196
- Required JSON format:
197
- {{
198
- "title": "short quiz title",
199
- "questions": [
200
- {{"q": "question 1", "answer_key": "expected short answer"}},
201
- {{"q": "question 2", "answer_key": "expected short answer"}}
202
- ]
203
- }}
204
-
205
- Context:
206
- {context}
207
-
208
- Topic:
209
- {topic}
210
- """.strip()
211
-
212
-
213
- def build_quiz_evaluation_prompt(language_mode: str, quiz_data: dict, user_answers: str) -> str:
214
- quiz_json = json.dumps(quiz_data, ensure_ascii=False)
215
- return f"""
216
- You are BrainChat, an interactive tutor.
217
-
218
- Task:
219
- Evaluate the student's answers fairly against the answer keys.
220
- Accept semantically correct answers even if wording differs.
221
-
222
- Return VALID JSON only.
223
-
224
- Required JSON format:
225
- {{
226
- "score_obtained": 0,
227
- "score_total": 0,
228
- "summary": "short overall feedback",
229
- "results": [
230
- {{
231
- "question": "question text",
232
- "answer_key": "expected short answer",
233
- "student_answer": "student answer",
234
- "result": "Correct / Partially Correct / Incorrect",
235
- "feedback": "short explanation"
236
- }}
237
- ],
238
- "improvement_tip": "one short study suggestion"
239
- }}
240
-
241
- Quiz:
242
- {quiz_json}
243
-
244
- Student answers:
245
- {user_answers}
246
-
247
- Language:
248
- {language_instruction(language_mode)}
249
- """.strip()
250
-
251
-
252
- def chat_text(prompt: str) -> str:
253
- ensure_loaded()
254
- resp = CLIENT.chat.completions.create(
255
- model=MODEL_NAME_TEXT,
256
- temperature=0.2,
257
- messages=[
258
- {"role": "system", "content": "You are a helpful educational assistant."},
259
- {"role": "user", "content": prompt},
260
- ],
261
- )
262
- return resp.choices[0].message.content.strip()
263
-
264
-
265
- def chat_json(prompt: str) -> dict:
266
- ensure_loaded()
267
- resp = CLIENT.chat.completions.create(
268
- model=MODEL_NAME_TEXT,
269
- temperature=0.2,
270
- response_format={"type": "json_object"},
271
- messages=[
272
- {"role": "system", "content": "Return only valid JSON."},
273
- {"role": "user", "content": prompt},
274
- ],
275
- )
276
- return json.loads(resp.choices[0].message.content)
277
 
 
278
 
279
- # ============================================================
280
- # Logo + Header HTML
281
- # ============================================================
282
- def find_logo_file():
283
- for name in LOGO_CANDIDATES:
284
- if os.path.exists(name):
285
- return name
286
- return None
287
 
 
 
 
 
 
 
 
 
 
288
 
289
- def logo_img_tag(size_px: int = 88) -> str:
290
- logo_file = find_logo_file()
291
- if logo_file:
292
- url = f"/gradio_api/file={quote(logo_file)}"
293
- return f'<img src="{url}" class="bc-logo-img" width="{size_px}" height="{size_px}" alt="BrainChat logo" />'
294
- return '<div class="bc-logo-fallback">BRAIN<br>CHAT</div>'
295
 
 
 
 
 
 
296
 
297
- def render_top_banner() -> str:
298
- return f"""
299
- <div class="bc-banner">
300
- <div class="bc-banner-inner">
301
- <div class="bc-banner-logo">{logo_img_tag(64)}</div>
302
- <div class="bc-banner-text">
303
- <div class="bc-banner-title">BrainChat</div>
304
- <div class="bc-banner-subtitle">Neurology & neuroanatomy tutor (book-based)</div>
305
- </div>
306
- </div>
307
- </div>
308
- """.strip()
309
 
 
 
310
 
311
- def render_phone_logo() -> str:
312
- return f"""
313
- <div class="bc-phone-logo">
314
- {logo_img_tag(84)}
315
- </div>
316
- """.strip()
317
 
 
 
318
 
319
- # ============================================================
320
- # Chat logic (with quiz state)
321
- # ============================================================
322
- def respond(message, history, mode, language_mode, quiz_count_mode, show_sources, quiz_state):
323
- if history is None:
324
- history = []
325
- if quiz_state is None:
326
- quiz_state = {"active": False, "quiz_data": None, "language_mode": "Auto"}
327
 
328
- user_text = (message or "").strip()
329
- if not user_text:
330
- return "", history, quiz_state
331
 
332
- try:
333
- history = history + [{"role": "user", "content": user_text}]
334
-
335
- # Quiz evaluation step
336
- if quiz_state.get("active", False):
337
- evaluation_prompt = build_quiz_evaluation_prompt(
338
- quiz_state.get("language_mode", language_mode),
339
- quiz_state.get("quiz_data", {}),
340
- user_text,
341
- )
342
- evaluation = chat_json(evaluation_prompt)
343
-
344
- lines = []
345
- lines.append(f"**Score:** {evaluation.get('score_obtained', 0)}/{evaluation.get('score_total', 0)}")
346
- if evaluation.get("summary"):
347
- lines.append(f"\n**Overall:** {evaluation['summary']}")
348
- if evaluation.get("improvement_tip"):
349
- lines.append(f"\n**Tip:** {evaluation['improvement_tip']}\n")
350
-
351
- results = evaluation.get("results", [])
352
- if results:
353
- lines.append("**Question-wise feedback:**")
354
- for item in results:
355
- lines.append("")
356
- lines.append(f"**Q:** {item.get('question','')}")
357
- lines.append(f"**Your answer:** {item.get('student_answer','')}")
358
- lines.append(f"**Expected:** {item.get('answer_key','')}")
359
- lines.append(f"**Result:** {item.get('result','')}")
360
- lines.append(f"**Feedback:** {item.get('feedback','')}")
361
-
362
- assistant_text = "\n".join(lines).strip()
363
- history = history + [{"role": "assistant", "content": assistant_text}]
364
-
365
- quiz_state = {"active": False, "quiz_data": None, "language_mode": language_mode}
366
- return "", history, quiz_state
367
-
368
- # Normal retrieval
369
- records = search_hybrid(user_text, shortlist_k=30, final_k=5)
370
- context = build_context(records)
371
-
372
- # Quiz generation
373
- if mode == "Quiz Me":
374
- n_questions = choose_quiz_count(user_text, quiz_count_mode)
375
- quiz_prompt = build_quiz_generation_prompt(language_mode, user_text, context, n_questions)
376
- quiz_data = chat_json(quiz_prompt)
377
-
378
- lines = []
379
- lines.append(f"**{quiz_data.get('title','Quiz')}**")
380
- lines.append(f"\n**Total questions:** {len(quiz_data.get('questions', []))}\n")
381
- lines.append("Reply in ONE message with numbered answers, like:")
382
- lines.append("1. ...")
383
- lines.append("2. ...\n")
384
-
385
- for i, q in enumerate(quiz_data.get("questions", []), start=1):
386
- lines.append(f"**Q{i}.** {q.get('q','')}")
387
-
388
- if show_sources:
389
- lines.append("\n\n**Sources used to create this quiz:**")
390
- lines.append(make_sources(records))
391
-
392
- assistant_text = "\n".join(lines).strip()
393
- history = history + [{"role": "assistant", "content": assistant_text}]
394
-
395
- quiz_state = {"active": True, "quiz_data": quiz_data, "language_mode": language_mode}
396
- return "", history, quiz_state
397
-
398
- # Other modes
399
- tutor_prompt = build_tutor_prompt(mode, language_mode, user_text, context)
400
- answer = chat_text(tutor_prompt)
401
-
402
- if show_sources:
403
- answer = (answer or "").strip() + "\n\n**Sources:**\n" + make_sources(records)
404
-
405
- history = history + [{"role": "assistant", "content": answer.strip()}]
406
- return "", history, quiz_state
407
-
408
- except Exception as e:
409
- history = history + [{"role": "assistant", "content": f"Error: {str(e)}"}]
410
- quiz_state = {"active": False, "quiz_data": None, "language_mode": language_mode}
411
- return "", history, quiz_state
412
-
413
-
414
- def clear_all():
415
- return "", [], {"active": False, "quiz_data": None, "language_mode": "Auto"}
416
-
417
-
418
- # ============================================================
419
- # CSS (Instagram-style phone mock)
420
- # ============================================================
421
- CSS = r"""
422
  :root{
423
- --bc-page-bg: #dcdcdc;
424
- --bc-grad-top: #E8C7D4;
425
- --bc-grad-mid: #A55CA2;
426
- --bc-grad-bot: #2B0C46;
427
- --bc-yellow: #FFF34A;
428
- --bc-bot-bubble: #FAF7B4;
429
- --bc-user-bubble: #FFFFFF;
430
- --bc-ink: #141414;
 
431
  }
432
 
433
- body, .gradio-container{
434
- background: var(--bc-page-bg) !important;
435
- font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
436
- }
437
- footer{ display:none !important; }
438
-
439
- /* Banner */
440
- #bc_banner{ max-width: 980px; margin: 18px auto 8px auto; }
441
- .bc-banner{
442
- background: linear-gradient(180deg, var(--bc-grad-top) 0%, var(--bc-grad-mid) 52%, var(--bc-grad-bot) 100%);
443
- border-radius: 26px;
444
- padding: 14px 16px;
445
- box-shadow: 0 10px 26px rgba(0,0,0,.12);
446
  }
447
- .bc-banner-inner{ display:flex; align-items:center; gap: 12px; color: white; }
448
- .bc-banner-title{ font-size: 20px; font-weight: 800; line-height:1.1; }
449
- .bc-banner-subtitle{ font-size: 13px; opacity:.92; margin-top:2px; }
450
- .bc-banner-logo .bc-logo-img{ border-radius: 999px; background: var(--bc-yellow); padding: 6px; display:block; }
451
- .bc-logo-fallback{
452
- width: 64px; height: 64px;
453
- border-radius: 999px;
454
- background: var(--bc-yellow);
455
- display:flex; align-items:center; justify-content:center;
456
- color: #111; font-weight: 900; font-size: 12px; text-align:center;
457
  }
458
 
459
- /* Settings */
460
- #bc_settings{ max-width: 980px; margin: 0 auto 10px auto; }
461
- #bc_settings .label{ font-weight: 700; }
462
-
463
- /* Phone */
464
- #bc_phone{
465
- max-width: 420px;
466
- margin: 0 auto 18px auto;
467
- border-radius: 38px;
468
- background: linear-gradient(180deg, var(--bc-grad-top) 0%, var(--bc-grad-mid) 45%, var(--bc-grad-bot) 100%);
469
- box-shadow: 0 18px 40px rgba(0,0,0,.18);
470
- border: 1px solid rgba(255,255,255,.22);
471
- padding: 14px 14px 12px 14px;
472
- position: relative;
473
  }
474
 
475
- /* Floating logo in phone */
476
- #bc_phone_logo{
477
- position: absolute;
478
- top: 12px;
479
- left: 50%;
480
- transform: translateX(-50%);
481
- z-index: 10;
482
  }
483
- .bc-phone-logo{
484
- width: 92px; height: 92px;
485
- border-radius: 999px;
486
- background: var(--bc-yellow);
487
- display:flex; align-items:center; justify-content:center;
488
- box-shadow: 0 10px 22px rgba(0,0,0,.18);
 
 
 
489
  }
490
- .bc-phone-logo .bc-logo-img{
491
- width: 84px !important; height: 84px !important; object-fit: contain;
 
 
 
 
 
 
 
 
 
 
 
492
  }
493
 
494
- /* Push chat down under logo */
495
- #bc_chatbot{ margin-top: 92px; }
 
 
 
 
 
496
 
497
- /* Chatbot transparent */
498
- #bc_chatbot, #bc_chatbot > div{
499
- background: transparent !important;
500
- border: none !important;
501
- box-shadow: none !important;
502
  }
503
- #bc_chatbot .toolbar{ display:none !important; }
504
-
505
- /* Bubble styling via internal testid markers */
506
- #bc_chatbot button[data-testid="user"],
507
- #bc_chatbot button[data-testid="bot"]{
508
- max-width: 82%;
509
- border-radius: 18px !important;
510
- padding: 12px 14px !important;
511
- color: var(--bc-ink) !important;
512
- box-shadow: 0 8px 18px rgba(0,0,0,.10);
513
- border: 0 !important;
514
- line-height: 1.35;
515
- font-size: 14px;
516
  }
517
 
518
- /* User bubble white */
519
- #bc_chatbot button[data-testid="user"]{
520
- background: var(--bc-user-bubble) !important;
521
  }
522
 
523
- /* Bot bubble pale yellow */
524
- #bc_chatbot button[data-testid="bot"]{
525
- background: var(--bc-bot-bubble) !important;
 
526
  }
527
 
528
- /* Bubble tails */
529
- #bc_chatbot button[data-testid="user"]::after{
530
- content:"";
531
- position:absolute;
532
- right:-7px;
533
- bottom: 12px;
534
- width:0; height:0;
535
- border-left: 10px solid var(--bc-user-bubble);
536
- border-top: 8px solid transparent;
537
- border-bottom: 8px solid transparent;
538
  }
539
- #bc_chatbot button[data-testid="bot"]::before{
540
- content:"";
541
- position:absolute;
542
- left:-7px;
543
- bottom: 12px;
544
- width:0; height:0;
545
- border-right: 10px solid var(--bc-bot-bubble);
546
- border-top: 8px solid transparent;
547
- border-bottom: 8px solid transparent;
548
  }
549
 
550
- /* Input bar */
551
- #bc_input_row{
552
- margin-top: 10px;
553
- background: rgba(255,243,74,.96);
554
- border-radius: 999px;
555
- padding: 10px 10px;
556
- box-shadow: 0 10px 22px rgba(0,0,0,.14);
557
- align-items: center;
558
  }
559
- #bc_plus{
560
- width: 34px; height: 34px;
561
- border-radius: 999px;
562
- display:flex;
563
- align-items:center;
564
- justify-content:center;
565
- font-weight: 900;
566
- color: var(--bc-grad-bot);
567
- background: rgba(255,255,255,.35);
568
- user-select: none;
569
  }
570
- #bc_msg textarea{
571
- background: rgba(255,255,255,.35) !important;
572
- border-radius: 999px !important;
573
- border: none !important;
574
- padding: 10px 12px !important;
575
- color: var(--bc-grad-bot) !important;
576
- box-shadow: none !important;
577
  }
578
- #bc_send{
579
- min-width: 42px !important;
580
- height: 38px !important;
581
- border-radius: 999px !important;
582
- border: none !important;
583
- background: rgba(255,255,255,.35) !important;
584
- color: var(--bc-grad-bot) !important;
585
- font-size: 18px !important;
586
- font-weight: 900 !important;
587
  }
588
- #bc_send:hover{ background: rgba(255,255,255,.55) !important; }
589
 
590
- /* Clear */
591
- #bc_clear{
592
- max-width: 420px;
593
- margin: 10px auto 0 auto;
594
- border-radius: 14px !important;
595
  }
596
 
597
- @media (max-width: 480px){
598
- #bc_phone{ max-width: 95vw; }
599
- #bc_chatbot button[data-testid="user"],
600
- #bc_chatbot button[data-testid="bot"]{
601
- max-width: 88%;
602
- font-size: 14px;
603
- }
604
  }
605
  """
606
 
607
- # ============================================================
608
- # UI
609
- # ============================================================
610
- with gr.Blocks() as demo:
611
- quiz_state = gr.State({"active": False, "quiz_data": None, "language_mode": "Auto"})
612
 
613
- gr.HTML(render_top_banner(), elem_id="bc_banner")
 
 
 
 
614
 
615
- with gr.Accordion("Settings", open=False, elem_id="bc_settings"):
616
- mode = gr.Dropdown(
617
- choices=["Explain", "Detailed", "Short Notes", "Flashcards", "Case-Based", "Quiz Me"],
618
- value="Explain",
619
- label="Tutor Mode",
620
- )
621
- language_mode = gr.Dropdown(
622
- choices=["Auto", "English", "Spanish", "Bilingual"],
623
- value="Auto",
624
- label="Answer Language",
625
- )
626
- quiz_count_mode = gr.Dropdown(
627
- choices=["Auto", "3", "5", "7"],
628
- value="Auto",
629
- label="Quiz Questions",
630
- )
631
- show_sources = gr.Checkbox(value=True, label="Show Sources")
632
-
633
- with gr.Group(elem_id="bc_phone"):
634
- gr.HTML(render_phone_logo(), elem_id="bc_phone_logo")
635
-
636
- chatbot = gr.Chatbot(
637
- value=[],
638
- elem_id="bc_chatbot",
639
- height=560,
640
- layout="bubble",
641
- container=False,
642
- show_label=False,
643
- autoscroll=True,
644
- buttons=[],
645
- placeholder="Ask a question or type a topic…",
646
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
647
 
648
- with gr.Row(elem_id="bc_input_row"):
649
- gr.HTML("<div>+</div>", elem_id="bc_plus")
650
- msg = gr.Textbox(
651
- placeholder="Type a message…",
652
- show_label=False,
653
- container=False,
654
- scale=8,
655
- elem_id="bc_msg",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656
  )
657
- send_btn = gr.Button("➤", elem_id="bc_send", scale=1)
658
 
659
- clear_btn = gr.Button("Clear chat", elem_id="bc_clear")
 
 
 
 
 
660
 
661
- msg.submit(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
662
  respond,
663
- inputs=[msg, chatbot, mode, language_mode, quiz_count_mode, show_sources, quiz_state],
664
- outputs=[msg, chatbot, quiz_state],
665
  )
666
- send_btn.click(
 
667
  respond,
668
- inputs=[msg, chatbot, mode, language_mode, quiz_count_mode, show_sources, quiz_state],
669
- outputs=[msg, chatbot, quiz_state],
670
  )
 
671
  clear_btn.click(
672
- clear_all,
673
- inputs=None,
674
- outputs=[msg, chatbot, quiz_state],
675
- queue=False,
 
 
 
 
 
 
 
 
 
676
  )
677
 
678
  if __name__ == "__main__":
679
- demo.launch(css=CSS)
 
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
 
 
9
  import gradio as gr
10
+
11
+ # Optional readers
12
+ try:
13
+ from pypdf import PdfReader
14
+ except Exception:
15
+ PdfReader = None
16
+
17
+ try:
18
+ import docx
19
+ except Exception:
20
+ docx = None
21
+
22
+
23
+ # =========================================================
24
+ # CONFIG
25
+ # =========================================================
26
+ APP_TITLE = "BrainChat"
27
+ APP_SUBTITLE = "Neurology & neuroanatomy tutor"
28
+ NOT_FOUND_TEXT = "Not found in the course material."
29
+
30
+ SEARCH_DIRS = [
31
+ Path("."),
32
+ Path("./brainchat_build"),
33
+ Path("/home/user/app"),
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",
40
+ "by", "from", "as", "at", "that", "this", "these", "those",
41
+ "it", "its", "into", "about", "what", "which", "who", "whom",
42
+ "why", "how", "when", "where", "do", "does", "did", "can",
43
+ "could", "would", "should", "will", "shall", "i", "you", "we",
44
+ "they", "he", "she", "them", "his", "her", "their", "our", "your",
45
+ "my", "me", "us", "if", "then", "than", "also", "there", "here"
46
+ }
47
 
 
 
 
 
 
 
 
 
48
 
49
+ # =========================================================
50
+ # HELPERS
51
+ # =========================================================
52
+ def normalize_spaces(text: str) -> str:
53
+ return re.sub(r"\s+", " ", text).strip()
54
 
 
 
 
 
 
55
 
56
+ def tokenize(text: str) -> List[str]:
57
+ words = re.findall(r"[a-zA-Z0-9\-]+", text.lower())
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:
64
+ return []
 
 
 
65
 
66
+ chunks = []
67
+ start = 0
68
+ n = len(text)
69
 
70
+ while start < n:
71
+ end = min(start + chunk_size, n)
72
+ chunk = text[start:end]
73
 
74
+ if end < n:
75
+ last_period = chunk.rfind(". ")
76
+ last_newline = chunk.rfind("\n")
77
+ cut = max(last_period, last_newline)
78
+ if cut > 400:
79
+ chunk = chunk[:cut + 1]
80
 
81
+ chunks.append(chunk.strip())
 
82
 
83
+ if end >= n:
84
+ break
85
 
86
+ start = max(0, start + len(chunk) - overlap)
 
 
 
 
87
 
88
+ return [c for c in chunks if c]
89
 
 
 
90
 
91
+ def safe_read_text_file(path: Path) -> str:
92
+ try:
93
+ return path.read_text(encoding="utf-8", errors="ignore")
94
+ except Exception:
95
+ try:
96
+ return path.read_text(encoding="latin-1", errors="ignore")
97
+ except Exception:
98
+ return ""
99
 
 
 
 
100
 
101
+ def read_pdf(path: Path) -> str:
102
+ if PdfReader is None:
103
+ return ""
104
+ try:
105
+ reader = PdfReader(str(path))
106
+ parts = []
107
+ for page in reader.pages:
108
+ try:
109
+ parts.append(page.extract_text() or "")
110
+ except Exception:
111
+ continue
112
+ return "\n".join(parts)
113
+ except Exception:
114
+ return ""
115
+
116
+
117
+ def read_docx(path: Path) -> str:
118
+ if docx is None:
119
+ return ""
120
+ try:
121
+ d = docx.Document(str(path))
122
+ return "\n".join(p.text for p in d.paragraphs if p.text.strip())
123
+ except Exception:
124
+ return ""
125
+
126
+
127
+ def extract_text_from_file(path: Path) -> str:
128
+ suffix = path.suffix.lower()
129
+ if suffix in {".txt", ".md"}:
130
+ return safe_read_text_file(path)
131
+ if suffix == ".pdf":
132
+ return read_pdf(path)
133
+ if suffix == ".docx":
134
+ return read_docx(path)
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:
142
+ if d.exists():
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",
160
+ "brainchat-09.png",
161
+ "BrainChat_logo.png",
162
+ "brainchat_logo.png",
163
+ "logo.png",
164
+ "Logo.png",
165
+ ])
166
+
167
+
168
+ # =========================================================
169
+ # KNOWLEDGE BASE
170
+ # =========================================================
171
+ class LocalKnowledgeBase:
172
+ def __init__(self) -> None:
173
+ self.docs: List[Dict] = []
174
+ self.chunks: List[Dict] = []
175
+
176
+ def clear(self) -> None:
177
+ self.docs = []
178
+ self.chunks = []
179
+
180
+ def add_document(self, source_name: str, text: str) -> None:
181
+ text = normalize_spaces(text)
182
+ if not text:
183
+ return
184
+
185
+ doc_id = len(self.docs)
186
+ self.docs.append({"doc_id": doc_id, "source": source_name, "text": text})
187
+
188
+ for idx, chunk in enumerate(chunk_text(text)):
189
+ tokens = set(tokenize(chunk))
190
+ self.chunks.append({
191
+ "doc_id": doc_id,
192
+ "source": source_name,
193
+ "chunk_id": idx,
194
+ "text": chunk,
195
+ "tokens": tokens
196
+ })
197
+
198
+ def load_from_directories(self) -> None:
199
+ seen = set()
200
+
201
+ for base in SEARCH_DIRS:
202
+ if not base.exists():
203
+ continue
204
+
205
+ for path in base.rglob("*"):
206
+ if not path.is_file():
207
+ continue
208
+ if path.suffix.lower() not in SUPPORTED_EXTENSIONS:
209
+ continue
210
+ if path.name.startswith("."):
211
+ continue
212
 
213
+ key = str(path.resolve())
214
+ if key in seen:
215
+ continue
216
+ seen.add(key)
217
+
218
+ text = extract_text_from_file(path)
219
+ if text.strip():
220
+ self.add_document(path.name, text)
221
+
222
+ def search(self, query: str, top_k: int = 5) -> List[Dict]:
223
+ q_tokens = set(tokenize(query))
224
+ if not q_tokens:
225
+ return []
226
+
227
+ scored = []
228
+
229
+ for item in self.chunks:
230
+ overlap = len(q_tokens.intersection(item["tokens"]))
231
+ if overlap == 0:
232
+ continue
233
+
234
+ score = overlap / max(1, len(q_tokens))
235
+ scored.append((score, item))
236
+
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
249
+ unique.append(result)
250
+
251
+ if len(unique) >= top_k:
252
+ break
253
+
254
+ return unique
255
+
256
+
257
+ KB = LocalKnowledgeBase()
258
+ 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:
300
+ answer_text = (answer_text or "").strip()
 
301
 
302
+ if not answer_text or answer_text.lower() == NOT_FOUND_TEXT.lower():
303
+ return NOT_FOUND_TEXT
304
 
305
+ if show_sources and sources:
306
+ src_lines = "\n".join(f"- {s}" for s in sources if str(s).strip())
307
+ if src_lines.strip():
308
+ return f"{answer_text}\n\n**Sources:**\n{src_lines}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
 
310
+ return answer_text
311
 
 
 
 
 
 
 
 
 
312
 
313
+ def get_answer_and_sources(
314
+ message: str,
315
+ tutor_mode: str,
316
+ answer_language: str,
317
+ quiz_questions: str
318
+ ) -> Tuple[str, List[str]]:
319
+ msg = (message or "").strip()
320
+ if not msg:
321
+ return "Please type a question.", []
322
 
323
+ lower_msg = msg.lower().strip()
 
 
 
 
 
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
+ # =========================================================
352
+ # UI
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 {
419
+ font-size: 30px;
420
+ font-weight: 800;
421
+ color: white;
422
+ line-height: 1.1;
423
+ margin: 0;
424
+ }
425
 
426
+ #brand_subtitle {
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
 
506
+ return f"""
507
+ <div id="topbar">
508
+ <div id="brand_row">
509
+ {logo_html}
510
+ <div>
511
+ <div id="brand_title">{html.escape(APP_TITLE)}</div>
512
+ <div id="brand_subtitle">{html.escape(APP_SUBTITLE)}</div>
513
+ </div>
514
+ </div>
515
+ </div>
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
+
538
+
539
+ def clear_chat():
540
+ return [], ""
541
+
542
+
543
+ 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):
551
+ if not files:
552
+ return "No file uploaded."
553
+
554
+ added = 0
555
+ for f in files:
556
+ try:
557
+ path = Path(f.name)
558
+ text = extract_text_from_file(path)
559
+ if text.strip():
560
+ KB.add_document(path.name, text)
561
+ added += 1
562
+ except Exception:
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:
571
+ with gr.Column(elem_id="main_shell"):
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
+ )
605
 
606
+ with gr.Row():
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(
614
+ placeholder="Ask a question about neurology or neuroanatomy...",
615
+ show_label=False,
616
+ scale=6
617
+ )
618
+
619
+ with gr.Row():
620
+ upload_btn = gr.Button("Upload Files", elem_id="upload_btn")
621
+ reload_btn = gr.Button("Reload Materials", elem_id="reload_btn")
622
+ clear_btn = gr.Button("Clear Chat", elem_id="clear_btn")
623
+ send_btn = gr.Button("Send", elem_id="send_btn")
624
+
625
+ status_box = gr.Markdown("Ready.")
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
+
639
  clear_btn.click(
640
+ clear_chat,
641
+ outputs=[chatbot, msg]
642
+ )
643
+
644
+ reload_btn.click(
645
+ reload_materials,
646
+ outputs=[status_box]
647
+ )
648
+
649
+ upload_btn.click(
650
+ upload_files,
651
+ inputs=[file_input],
652
+ outputs=[status_box]
653
  )
654
 
655
  if __name__ == "__main__":
656
+ demo.launch()