Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -11,7 +11,7 @@ from sentence_transformers import SentenceTransformer
|
|
| 11 |
from openai import OpenAI
|
| 12 |
|
| 13 |
# =====================================================
|
| 14 |
-
#
|
| 15 |
# =====================================================
|
| 16 |
BUILD_DIR = "brainchat_build"
|
| 17 |
CHUNKS_PATH = os.path.join(BUILD_DIR, "chunks.pkl")
|
|
@@ -19,19 +19,9 @@ 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 |
-
|
| 23 |
-
"Brain chat-09.png",
|
| 24 |
-
"brainchat_logo.png.png",
|
| 25 |
-
"Brain Chat Imagen.svg",
|
| 26 |
-
"logo.png",
|
| 27 |
-
"logo.svg",
|
| 28 |
-
]
|
| 29 |
-
|
| 30 |
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
|
| 31 |
|
| 32 |
-
# =====================================================
|
| 33 |
-
# GLOBALS
|
| 34 |
-
# =====================================================
|
| 35 |
BM25 = None
|
| 36 |
CHUNKS = None
|
| 37 |
EMBEDDINGS = None
|
|
@@ -120,7 +110,12 @@ def make_sources(records):
|
|
| 120 |
seen = set()
|
| 121 |
lines = []
|
| 122 |
for r in records:
|
| 123 |
-
key = (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
if key in seen:
|
| 125 |
continue
|
| 126 |
seen.add(key)
|
|
@@ -140,7 +135,7 @@ def language_instruction(language_mode: str) -> str:
|
|
| 140 |
return "Answer only in Spanish."
|
| 141 |
if language_mode == "Bilingual":
|
| 142 |
return "Answer first in English, then provide a Spanish version under the heading 'Español:'."
|
| 143 |
-
return "If the user's message is in Spanish, answer in Spanish
|
| 144 |
|
| 145 |
|
| 146 |
def choose_quiz_count(user_text: str, selector: str) -> int:
|
|
@@ -156,12 +151,12 @@ def choose_quiz_count(user_text: str, selector: str) -> int:
|
|
| 156 |
|
| 157 |
|
| 158 |
def build_tutor_prompt(mode: str, language_mode: str, question: str, context: str) -> str:
|
| 159 |
-
|
| 160 |
-
"Explain": "Explain clearly like a friendly tutor using simple language
|
| 161 |
-
"Detailed": "Give a
|
| 162 |
"Short Notes": "Write concise revision notes using short bullet points.",
|
| 163 |
"Flashcards": "Create 6 flashcards in Q/A format using only the context.",
|
| 164 |
-
"Case-Based": "Create a short clinical scenario and then explain the concept using the context."
|
| 165 |
}
|
| 166 |
|
| 167 |
return f"""
|
|
@@ -175,7 +170,7 @@ Rules:
|
|
| 175 |
- {language_instruction(language_mode)}
|
| 176 |
|
| 177 |
Teaching style:
|
| 178 |
-
{
|
| 179 |
|
| 180 |
Context:
|
| 181 |
{context}
|
|
@@ -192,9 +187,9 @@ You are BrainChat, an interactive tutor.
|
|
| 192 |
Rules:
|
| 193 |
- Use ONLY the provided context.
|
| 194 |
- Create exactly {n_questions} quiz questions.
|
| 195 |
-
- Keep
|
| 196 |
-
- Include a short answer key.
|
| 197 |
-
- Return
|
| 198 |
- {language_instruction(language_mode)}
|
| 199 |
|
| 200 |
Return JSON in this format:
|
|
@@ -222,7 +217,9 @@ You are BrainChat, an interactive tutor.
|
|
| 222 |
Evaluate the student's answers fairly using the answer keys.
|
| 223 |
Accept semantically correct answers even if wording differs.
|
| 224 |
|
| 225 |
-
Return
|
|
|
|
|
|
|
| 226 |
{{
|
| 227 |
"score_obtained": 0,
|
| 228 |
"score_total": 0,
|
|
@@ -251,7 +248,7 @@ Language:
|
|
| 251 |
|
| 252 |
|
| 253 |
# =====================================================
|
| 254 |
-
# OPENAI
|
| 255 |
# =====================================================
|
| 256 |
def oai_text(prompt: str) -> str:
|
| 257 |
ensure_loaded()
|
|
@@ -283,18 +280,10 @@ def oai_json(prompt: str) -> dict:
|
|
| 283 |
# =====================================================
|
| 284 |
# LOGO
|
| 285 |
# =====================================================
|
| 286 |
-
def find_logo_filename():
|
| 287 |
-
for name in LOGO_CANDIDATES:
|
| 288 |
-
if os.path.exists(name):
|
| 289 |
-
return name
|
| 290 |
-
return None
|
| 291 |
-
|
| 292 |
-
|
| 293 |
def logo_url():
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
return f"/gradio_api/file={quote(f)}"
|
| 298 |
|
| 299 |
|
| 300 |
def render_logo():
|
|
@@ -305,9 +294,9 @@ def render_logo():
|
|
| 305 |
|
| 306 |
|
| 307 |
# =====================================================
|
| 308 |
-
# CHAT HTML
|
| 309 |
# =====================================================
|
| 310 |
-
def
|
| 311 |
safe = (
|
| 312 |
text.replace("&", "&")
|
| 313 |
.replace("<", "<")
|
|
@@ -323,8 +312,7 @@ def render_chat(history):
|
|
| 323 |
return """
|
| 324 |
<div class="bc-empty">
|
| 325 |
<div class="bc-empty-text">
|
| 326 |
-
|
| 327 |
-
For quiz mode, type a topic such as <strong>cranial nerves</strong>.
|
| 328 |
</div>
|
| 329 |
</div>
|
| 330 |
"""
|
|
@@ -332,7 +320,7 @@ def render_chat(history):
|
|
| 332 |
rows = []
|
| 333 |
for item in history:
|
| 334 |
role = item["role"]
|
| 335 |
-
content =
|
| 336 |
|
| 337 |
if role == "user":
|
| 338 |
rows.append(
|
|
@@ -348,9 +336,9 @@ def render_chat(history):
|
|
| 348 |
{''.join(rows)}
|
| 349 |
</div>
|
| 350 |
<script>
|
| 351 |
-
const
|
| 352 |
-
if (
|
| 353 |
-
|
| 354 |
}}
|
| 355 |
</script>
|
| 356 |
"""
|
|
@@ -370,7 +358,6 @@ def respond(user_msg, history, mode, language_mode, quiz_count_mode, show_source
|
|
| 370 |
try:
|
| 371 |
history = history + [{"role": "user", "content": text}]
|
| 372 |
|
| 373 |
-
# quiz evaluation
|
| 374 |
if quiz_state.get("active", False):
|
| 375 |
evaluation = oai_json(
|
| 376 |
build_quiz_eval_prompt(
|
|
@@ -402,11 +389,9 @@ def respond(user_msg, history, mode, language_mode, quiz_count_mode, show_source
|
|
| 402 |
quiz_state = {"active": False, "quiz_data": None, "language_mode": language_mode}
|
| 403 |
return "", history, render_chat(history), quiz_state
|
| 404 |
|
| 405 |
-
# normal retrieval
|
| 406 |
records = search_hybrid(text, shortlist_k=20, final_k=4)
|
| 407 |
context = build_context(records)
|
| 408 |
|
| 409 |
-
# quiz generation
|
| 410 |
if mode == "Quiz Me":
|
| 411 |
n_questions = choose_quiz_count(text, quiz_count_mode)
|
| 412 |
quiz_data = oai_json(build_quiz_generation_prompt(language_mode, text, context, n_questions))
|
|
@@ -414,9 +399,8 @@ def respond(user_msg, history, mode, language_mode, quiz_count_mode, show_source
|
|
| 414 |
lines = []
|
| 415 |
lines.append(f"**{quiz_data.get('title', 'Quiz')}**")
|
| 416 |
lines.append(f"\n**Total questions:** {len(quiz_data.get('questions', []))}\n")
|
| 417 |
-
lines.append("Reply in
|
| 418 |
-
lines.append("1. ...")
|
| 419 |
-
lines.append("2. ...\n")
|
| 420 |
|
| 421 |
for i, q in enumerate(quiz_data.get("questions", []), start=1):
|
| 422 |
lines.append(f"**Q{i}.** {q.get('q','')}")
|
|
@@ -429,7 +413,6 @@ def respond(user_msg, history, mode, language_mode, quiz_count_mode, show_source
|
|
| 429 |
quiz_state = {"active": True, "quiz_data": quiz_data, "language_mode": language_mode}
|
| 430 |
return "", history, render_chat(history), quiz_state
|
| 431 |
|
| 432 |
-
# tutor modes
|
| 433 |
answer = oai_text(build_tutor_prompt(mode, language_mode, text, context))
|
| 434 |
if show_sources:
|
| 435 |
answer = answer.strip() + "\n\n**Sources:**\n" + make_sources(records)
|
|
@@ -451,24 +434,22 @@ def clear_all():
|
|
| 451 |
|
| 452 |
# =====================================================
|
| 453 |
# CSS
|
| 454 |
-
# dark but light-friendly, no black background
|
| 455 |
# =====================================================
|
| 456 |
CSS = """
|
| 457 |
:root{
|
| 458 |
-
--page-bg: #
|
| 459 |
-
--panel-bg: #
|
| 460 |
-
--chat-bg: #
|
| 461 |
--grad-top: #e8c7d4;
|
| 462 |
--grad-mid: #a55ca2;
|
| 463 |
-
--grad-bot: #
|
| 464 |
--accent: #f4eb4b;
|
| 465 |
-
--accent-soft: #
|
| 466 |
-
--user-bubble: #
|
| 467 |
-
--bot-bubble: #
|
| 468 |
-
--text-dark: #
|
| 469 |
--text-light: #ffffff;
|
| 470 |
-
--
|
| 471 |
-
--shadow: rgba(25, 20, 40, 0.18);
|
| 472 |
}
|
| 473 |
|
| 474 |
html, body, .gradio-container{
|
|
@@ -477,25 +458,36 @@ html, body, .gradio-container{
|
|
| 477 |
}
|
| 478 |
footer{display:none !important;}
|
| 479 |
|
| 480 |
-
#
|
| 481 |
-
max-width:
|
| 482 |
margin: 18px auto;
|
| 483 |
}
|
| 484 |
|
| 485 |
-
.bc-
|
| 486 |
-
background:
|
| 487 |
-
border-radius:
|
| 488 |
-
padding:
|
| 489 |
box-shadow: 0 10px 24px var(--shadow);
|
| 490 |
margin-bottom: 14px;
|
| 491 |
}
|
| 492 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
.bc-phone{
|
| 494 |
position: relative;
|
| 495 |
-
background: linear-gradient(180deg, var(--grad-top) 0%, var(--grad-mid)
|
| 496 |
-
border-radius:
|
| 497 |
-
padding:
|
| 498 |
box-shadow: 0 16px 34px var(--shadow);
|
|
|
|
| 499 |
}
|
| 500 |
|
| 501 |
.bc-logo-holder{
|
|
@@ -536,27 +528,27 @@ footer{display:none !important;}
|
|
| 536 |
}
|
| 537 |
|
| 538 |
.bc-chat-shell{
|
| 539 |
-
background:
|
| 540 |
-
border-radius:
|
| 541 |
padding: 16px;
|
| 542 |
-
min-height:
|
| 543 |
-
box-shadow: inset 0 1px 0 rgba(255,255,255,0.
|
| 544 |
}
|
| 545 |
|
| 546 |
.bc-chat-wrap{
|
| 547 |
display: flex;
|
| 548 |
flex-direction: column;
|
| 549 |
gap: 14px;
|
| 550 |
-
max-height:
|
| 551 |
overflow-y: auto;
|
| 552 |
padding-right: 4px;
|
| 553 |
}
|
| 554 |
|
| 555 |
.bc-chat-wrap::-webkit-scrollbar{
|
| 556 |
-
width:
|
| 557 |
}
|
| 558 |
.bc-chat-wrap::-webkit-scrollbar-thumb{
|
| 559 |
-
background: rgba(255,255,255,0.
|
| 560 |
border-radius: 999px;
|
| 561 |
}
|
| 562 |
|
|
@@ -579,17 +571,18 @@ footer{display:none !important;}
|
|
| 579 |
line-height: 1.5;
|
| 580 |
font-size: 15px;
|
| 581 |
box-shadow: 0 10px 18px rgba(0,0,0,0.10);
|
| 582 |
-
color: var(--text-dark);
|
| 583 |
word-wrap: break-word;
|
| 584 |
}
|
| 585 |
|
| 586 |
.bc-user-bubble{
|
| 587 |
background: var(--user-bubble);
|
|
|
|
| 588 |
border-bottom-left-radius: 8px;
|
| 589 |
}
|
| 590 |
|
| 591 |
.bc-bot-bubble{
|
| 592 |
background: var(--bot-bubble);
|
|
|
|
| 593 |
border-bottom-right-radius: 8px;
|
| 594 |
}
|
| 595 |
|
|
@@ -597,10 +590,10 @@ footer{display:none !important;}
|
|
| 597 |
display:flex;
|
| 598 |
justify-content:center;
|
| 599 |
align-items:center;
|
| 600 |
-
min-height:
|
| 601 |
}
|
| 602 |
.bc-empty-text{
|
| 603 |
-
color:
|
| 604 |
text-align:center;
|
| 605 |
opacity: 0.96;
|
| 606 |
font-size: 16px;
|
|
@@ -621,7 +614,7 @@ footer{display:none !important;}
|
|
| 621 |
width: 38px;
|
| 622 |
height: 38px;
|
| 623 |
border-radius: 999px;
|
| 624 |
-
background: rgba(255,255,255,0.
|
| 625 |
display:flex;
|
| 626 |
align-items:center;
|
| 627 |
justify-content:center;
|
|
@@ -632,7 +625,7 @@ footer{display:none !important;}
|
|
| 632 |
}
|
| 633 |
|
| 634 |
#bc_msg textarea{
|
| 635 |
-
background: rgba(255,255,255,0.
|
| 636 |
border: none !important;
|
| 637 |
box-shadow: none !important;
|
| 638 |
border-radius: 999px !important;
|
|
@@ -641,7 +634,7 @@ footer{display:none !important;}
|
|
| 641 |
min-height: 42px !important;
|
| 642 |
}
|
| 643 |
#bc_msg textarea::placeholder{
|
| 644 |
-
color: rgba(
|
| 645 |
}
|
| 646 |
|
| 647 |
#bc_send button{
|
|
@@ -649,14 +642,14 @@ footer{display:none !important;}
|
|
| 649 |
height: 42px !important;
|
| 650 |
border-radius: 999px !important;
|
| 651 |
border: none !important;
|
| 652 |
-
background: rgba(255,255,255,0.
|
| 653 |
color: var(--text-dark) !important;
|
| 654 |
font-size: 20px !important;
|
| 655 |
font-weight: 900 !important;
|
| 656 |
box-shadow: none !important;
|
| 657 |
}
|
| 658 |
#bc_send button:hover{
|
| 659 |
-
background: rgba(255,255,255,0.
|
| 660 |
}
|
| 661 |
|
| 662 |
#bc_clear button{
|
|
@@ -664,7 +657,7 @@ footer{display:none !important;}
|
|
| 664 |
}
|
| 665 |
|
| 666 |
@media (max-width: 768px){
|
| 667 |
-
#
|
| 668 |
max-width: 96vw;
|
| 669 |
}
|
| 670 |
.bc-bubble{
|
|
@@ -681,8 +674,8 @@ with gr.Blocks() as demo:
|
|
| 681 |
history_state = gr.State([])
|
| 682 |
quiz_state = gr.State({"active": False, "quiz_data": None, "language_mode": "Auto"})
|
| 683 |
|
| 684 |
-
with gr.Column(elem_id="
|
| 685 |
-
with gr.Group(elem_classes="bc-
|
| 686 |
with gr.Row():
|
| 687 |
mode = gr.Dropdown(
|
| 688 |
choices=["Explain", "Detailed", "Short Notes", "Flashcards", "Case-Based", "Quiz Me"],
|
|
@@ -703,6 +696,16 @@ with gr.Blocks() as demo:
|
|
| 703 |
)
|
| 704 |
show_sources = gr.Checkbox(value=True, label="Show Sources")
|
| 705 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
with gr.Group(elem_classes="bc-phone"):
|
| 707 |
gr.HTML(f'<div class="bc-logo-holder">{render_logo()}</div>')
|
| 708 |
|
|
|
|
| 11 |
from openai import OpenAI
|
| 12 |
|
| 13 |
# =====================================================
|
| 14 |
+
# CONFIG
|
| 15 |
# =====================================================
|
| 16 |
BUILD_DIR = "brainchat_build"
|
| 17 |
CHUNKS_PATH = os.path.join(BUILD_DIR, "chunks.pkl")
|
|
|
|
| 19 |
EMBED_PATH = os.path.join(BUILD_DIR, "embeddings.npy")
|
| 20 |
CONFIG_PATH = os.path.join(BUILD_DIR, "config.json")
|
| 21 |
|
| 22 |
+
LOGO_FILE = "logo.png"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
|
| 24 |
|
|
|
|
|
|
|
|
|
|
| 25 |
BM25 = None
|
| 26 |
CHUNKS = None
|
| 27 |
EMBEDDINGS = None
|
|
|
|
| 110 |
seen = set()
|
| 111 |
lines = []
|
| 112 |
for r in records:
|
| 113 |
+
key = (
|
| 114 |
+
r.get("book"),
|
| 115 |
+
r.get("section_title"),
|
| 116 |
+
r.get("page_start"),
|
| 117 |
+
r.get("page_end"),
|
| 118 |
+
)
|
| 119 |
if key in seen:
|
| 120 |
continue
|
| 121 |
seen.add(key)
|
|
|
|
| 135 |
return "Answer only in Spanish."
|
| 136 |
if language_mode == "Bilingual":
|
| 137 |
return "Answer first in English, then provide a Spanish version under the heading 'Español:'."
|
| 138 |
+
return "If the user's message is in Spanish, answer in Spanish; otherwise answer in English."
|
| 139 |
|
| 140 |
|
| 141 |
def choose_quiz_count(user_text: str, selector: str) -> int:
|
|
|
|
| 151 |
|
| 152 |
|
| 153 |
def build_tutor_prompt(mode: str, language_mode: str, question: str, context: str) -> str:
|
| 154 |
+
styles = {
|
| 155 |
+
"Explain": "Explain clearly like a friendly tutor using simple language.",
|
| 156 |
+
"Detailed": "Give a more detailed explanation with key points and clinical relevance only when supported by context.",
|
| 157 |
"Short Notes": "Write concise revision notes using short bullet points.",
|
| 158 |
"Flashcards": "Create 6 flashcards in Q/A format using only the context.",
|
| 159 |
+
"Case-Based": "Create a short clinical case scenario and then explain the concept using the context.",
|
| 160 |
}
|
| 161 |
|
| 162 |
return f"""
|
|
|
|
| 170 |
- {language_instruction(language_mode)}
|
| 171 |
|
| 172 |
Teaching style:
|
| 173 |
+
{styles.get(mode, "Explain clearly like a friendly tutor.")}
|
| 174 |
|
| 175 |
Context:
|
| 176 |
{context}
|
|
|
|
| 187 |
Rules:
|
| 188 |
- Use ONLY the provided context.
|
| 189 |
- Create exactly {n_questions} quiz questions.
|
| 190 |
+
- Keep questions short and clear.
|
| 191 |
+
- Include a short answer key for each.
|
| 192 |
+
- Return VALID JSON only.
|
| 193 |
- {language_instruction(language_mode)}
|
| 194 |
|
| 195 |
Return JSON in this format:
|
|
|
|
| 217 |
Evaluate the student's answers fairly using the answer keys.
|
| 218 |
Accept semantically correct answers even if wording differs.
|
| 219 |
|
| 220 |
+
Return VALID JSON only.
|
| 221 |
+
|
| 222 |
+
Return JSON in this format:
|
| 223 |
{{
|
| 224 |
"score_obtained": 0,
|
| 225 |
"score_total": 0,
|
|
|
|
| 248 |
|
| 249 |
|
| 250 |
# =====================================================
|
| 251 |
+
# OPENAI
|
| 252 |
# =====================================================
|
| 253 |
def oai_text(prompt: str) -> str:
|
| 254 |
ensure_loaded()
|
|
|
|
| 280 |
# =====================================================
|
| 281 |
# LOGO
|
| 282 |
# =====================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
def logo_url():
|
| 284 |
+
if os.path.exists(LOGO_FILE):
|
| 285 |
+
return f"/gradio_api/file={quote(LOGO_FILE)}"
|
| 286 |
+
return None
|
|
|
|
| 287 |
|
| 288 |
|
| 289 |
def render_logo():
|
|
|
|
| 294 |
|
| 295 |
|
| 296 |
# =====================================================
|
| 297 |
+
# CHAT HTML
|
| 298 |
# =====================================================
|
| 299 |
+
def format_text(text: str) -> str:
|
| 300 |
safe = (
|
| 301 |
text.replace("&", "&")
|
| 302 |
.replace("<", "<")
|
|
|
|
| 312 |
return """
|
| 313 |
<div class="bc-empty">
|
| 314 |
<div class="bc-empty-text">
|
| 315 |
+
Ask a question and choose a tutor mode above.
|
|
|
|
| 316 |
</div>
|
| 317 |
</div>
|
| 318 |
"""
|
|
|
|
| 320 |
rows = []
|
| 321 |
for item in history:
|
| 322 |
role = item["role"]
|
| 323 |
+
content = format_text(item["content"])
|
| 324 |
|
| 325 |
if role == "user":
|
| 326 |
rows.append(
|
|
|
|
| 336 |
{''.join(rows)}
|
| 337 |
</div>
|
| 338 |
<script>
|
| 339 |
+
const chatWrap = document.getElementById("bc-chat-wrap");
|
| 340 |
+
if (chatWrap) {{
|
| 341 |
+
chatWrap.scrollTop = chatWrap.scrollHeight;
|
| 342 |
}}
|
| 343 |
</script>
|
| 344 |
"""
|
|
|
|
| 358 |
try:
|
| 359 |
history = history + [{"role": "user", "content": text}]
|
| 360 |
|
|
|
|
| 361 |
if quiz_state.get("active", False):
|
| 362 |
evaluation = oai_json(
|
| 363 |
build_quiz_eval_prompt(
|
|
|
|
| 389 |
quiz_state = {"active": False, "quiz_data": None, "language_mode": language_mode}
|
| 390 |
return "", history, render_chat(history), quiz_state
|
| 391 |
|
|
|
|
| 392 |
records = search_hybrid(text, shortlist_k=20, final_k=4)
|
| 393 |
context = build_context(records)
|
| 394 |
|
|
|
|
| 395 |
if mode == "Quiz Me":
|
| 396 |
n_questions = choose_quiz_count(text, quiz_count_mode)
|
| 397 |
quiz_data = oai_json(build_quiz_generation_prompt(language_mode, text, context, n_questions))
|
|
|
|
| 399 |
lines = []
|
| 400 |
lines.append(f"**{quiz_data.get('title', 'Quiz')}**")
|
| 401 |
lines.append(f"\n**Total questions:** {len(quiz_data.get('questions', []))}\n")
|
| 402 |
+
lines.append("Reply in one message using numbered answers.")
|
| 403 |
+
lines.append("Example: 1. ... 2. ...\n")
|
|
|
|
| 404 |
|
| 405 |
for i, q in enumerate(quiz_data.get("questions", []), start=1):
|
| 406 |
lines.append(f"**Q{i}.** {q.get('q','')}")
|
|
|
|
| 413 |
quiz_state = {"active": True, "quiz_data": quiz_data, "language_mode": language_mode}
|
| 414 |
return "", history, render_chat(history), quiz_state
|
| 415 |
|
|
|
|
| 416 |
answer = oai_text(build_tutor_prompt(mode, language_mode, text, context))
|
| 417 |
if show_sources:
|
| 418 |
answer = answer.strip() + "\n\n**Sources:**\n" + make_sources(records)
|
|
|
|
| 434 |
|
| 435 |
# =====================================================
|
| 436 |
# CSS
|
|
|
|
| 437 |
# =====================================================
|
| 438 |
CSS = """
|
| 439 |
:root{
|
| 440 |
+
--page-bg: #d9d9dd;
|
| 441 |
+
--panel-bg: #555765;
|
| 442 |
+
--chat-bg: #4a4c59;
|
| 443 |
--grad-top: #e8c7d4;
|
| 444 |
--grad-mid: #a55ca2;
|
| 445 |
+
--grad-bot: #5a2d77;
|
| 446 |
--accent: #f4eb4b;
|
| 447 |
+
--accent-soft: #f5ef9a;
|
| 448 |
+
--user-bubble: #ffffff;
|
| 449 |
+
--bot-bubble: #f5efad;
|
| 450 |
+
--text-dark: #221735;
|
| 451 |
--text-light: #ffffff;
|
| 452 |
+
--shadow: rgba(30,20,50,0.18);
|
|
|
|
| 453 |
}
|
| 454 |
|
| 455 |
html, body, .gradio-container{
|
|
|
|
| 458 |
}
|
| 459 |
footer{display:none !important;}
|
| 460 |
|
| 461 |
+
#bc_app{
|
| 462 |
+
max-width: 1000px;
|
| 463 |
margin: 18px auto;
|
| 464 |
}
|
| 465 |
|
| 466 |
+
.bc-settings{
|
| 467 |
+
background: linear-gradient(180deg, #484b59 0%, #3f424f 100%);
|
| 468 |
+
border-radius: 20px;
|
| 469 |
+
padding: 14px;
|
| 470 |
box-shadow: 0 10px 24px var(--shadow);
|
| 471 |
margin-bottom: 14px;
|
| 472 |
}
|
| 473 |
|
| 474 |
+
.bc-howto{
|
| 475 |
+
margin-top: 8px;
|
| 476 |
+
padding: 12px 14px;
|
| 477 |
+
border-radius: 14px;
|
| 478 |
+
background: rgba(255,255,255,0.10);
|
| 479 |
+
color: white;
|
| 480 |
+
font-size: 14px;
|
| 481 |
+
line-height: 1.5;
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
.bc-phone{
|
| 485 |
position: relative;
|
| 486 |
+
background: linear-gradient(180deg, var(--grad-top) 0%, var(--grad-mid) 48%, var(--grad-bot) 100%);
|
| 487 |
+
border-radius: 30px;
|
| 488 |
+
padding: 92px 14px 14px 14px;
|
| 489 |
box-shadow: 0 16px 34px var(--shadow);
|
| 490 |
+
min-height: 620px;
|
| 491 |
}
|
| 492 |
|
| 493 |
.bc-logo-holder{
|
|
|
|
| 528 |
}
|
| 529 |
|
| 530 |
.bc-chat-shell{
|
| 531 |
+
background: rgba(74,76,89,0.92);
|
| 532 |
+
border-radius: 20px;
|
| 533 |
padding: 16px;
|
| 534 |
+
min-height: 460px;
|
| 535 |
+
box-shadow: inset 0 1px 0 rgba(255,255,255,0.06);
|
| 536 |
}
|
| 537 |
|
| 538 |
.bc-chat-wrap{
|
| 539 |
display: flex;
|
| 540 |
flex-direction: column;
|
| 541 |
gap: 14px;
|
| 542 |
+
max-height: 460px;
|
| 543 |
overflow-y: auto;
|
| 544 |
padding-right: 4px;
|
| 545 |
}
|
| 546 |
|
| 547 |
.bc-chat-wrap::-webkit-scrollbar{
|
| 548 |
+
width: 8px;
|
| 549 |
}
|
| 550 |
.bc-chat-wrap::-webkit-scrollbar-thumb{
|
| 551 |
+
background: rgba(255,255,255,0.28);
|
| 552 |
border-radius: 999px;
|
| 553 |
}
|
| 554 |
|
|
|
|
| 571 |
line-height: 1.5;
|
| 572 |
font-size: 15px;
|
| 573 |
box-shadow: 0 10px 18px rgba(0,0,0,0.10);
|
|
|
|
| 574 |
word-wrap: break-word;
|
| 575 |
}
|
| 576 |
|
| 577 |
.bc-user-bubble{
|
| 578 |
background: var(--user-bubble);
|
| 579 |
+
color: var(--text-dark);
|
| 580 |
border-bottom-left-radius: 8px;
|
| 581 |
}
|
| 582 |
|
| 583 |
.bc-bot-bubble{
|
| 584 |
background: var(--bot-bubble);
|
| 585 |
+
color: var(--text-dark);
|
| 586 |
border-bottom-right-radius: 8px;
|
| 587 |
}
|
| 588 |
|
|
|
|
| 590 |
display:flex;
|
| 591 |
justify-content:center;
|
| 592 |
align-items:center;
|
| 593 |
+
min-height: 400px;
|
| 594 |
}
|
| 595 |
.bc-empty-text{
|
| 596 |
+
color: white;
|
| 597 |
text-align:center;
|
| 598 |
opacity: 0.96;
|
| 599 |
font-size: 16px;
|
|
|
|
| 614 |
width: 38px;
|
| 615 |
height: 38px;
|
| 616 |
border-radius: 999px;
|
| 617 |
+
background: rgba(255,255,255,0.34);
|
| 618 |
display:flex;
|
| 619 |
align-items:center;
|
| 620 |
justify-content:center;
|
|
|
|
| 625 |
}
|
| 626 |
|
| 627 |
#bc_msg textarea{
|
| 628 |
+
background: rgba(255,255,255,0.42) !important;
|
| 629 |
border: none !important;
|
| 630 |
box-shadow: none !important;
|
| 631 |
border-radius: 999px !important;
|
|
|
|
| 634 |
min-height: 42px !important;
|
| 635 |
}
|
| 636 |
#bc_msg textarea::placeholder{
|
| 637 |
+
color: rgba(34,23,53,0.72) !important;
|
| 638 |
}
|
| 639 |
|
| 640 |
#bc_send button{
|
|
|
|
| 642 |
height: 42px !important;
|
| 643 |
border-radius: 999px !important;
|
| 644 |
border: none !important;
|
| 645 |
+
background: rgba(255,255,255,0.34) !important;
|
| 646 |
color: var(--text-dark) !important;
|
| 647 |
font-size: 20px !important;
|
| 648 |
font-weight: 900 !important;
|
| 649 |
box-shadow: none !important;
|
| 650 |
}
|
| 651 |
#bc_send button:hover{
|
| 652 |
+
background: rgba(255,255,255,0.52) !important;
|
| 653 |
}
|
| 654 |
|
| 655 |
#bc_clear button{
|
|
|
|
| 657 |
}
|
| 658 |
|
| 659 |
@media (max-width: 768px){
|
| 660 |
+
#bc_app{
|
| 661 |
max-width: 96vw;
|
| 662 |
}
|
| 663 |
.bc-bubble{
|
|
|
|
| 674 |
history_state = gr.State([])
|
| 675 |
quiz_state = gr.State({"active": False, "quiz_data": None, "language_mode": "Auto"})
|
| 676 |
|
| 677 |
+
with gr.Column(elem_id="bc_app"):
|
| 678 |
+
with gr.Group(elem_classes="bc-settings"):
|
| 679 |
with gr.Row():
|
| 680 |
mode = gr.Dropdown(
|
| 681 |
choices=["Explain", "Detailed", "Short Notes", "Flashcards", "Case-Based", "Quiz Me"],
|
|
|
|
| 696 |
)
|
| 697 |
show_sources = gr.Checkbox(value=True, label="Show Sources")
|
| 698 |
|
| 699 |
+
gr.HTML("""
|
| 700 |
+
<div class="bc-howto">
|
| 701 |
+
<strong>How to use</strong><br>
|
| 702 |
+
1. Choose a tutor mode such as Explain, Detailed, Flashcards, or Quiz Me.<br>
|
| 703 |
+
2. Type your topic or question in the message box below.<br>
|
| 704 |
+
3. For Quiz Me, the next message you send will be evaluated automatically.<br>
|
| 705 |
+
4. Turn on Show Sources if you want references from the books.
|
| 706 |
+
</div>
|
| 707 |
+
""")
|
| 708 |
+
|
| 709 |
with gr.Group(elem_classes="bc-phone"):
|
| 710 |
gr.HTML(f'<div class="bc-logo-holder">{render_logo()}</div>')
|
| 711 |
|