File size: 25,376 Bytes
dd4d624
d97ee23
d19f25f
dd4d624
 
d19f25f
 
 
 
 
 
 
 
 
dd4d624
d19f25f
 
 
dd4d624
 
 
 
7f8678c
 
d19f25f
 
 
7f8678c
 
 
 
 
 
 
 
 
 
dd4d624
 
 
 
d19f25f
 
dd4d624
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d0bd74c
 
 
d19f25f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dd4d624
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d19f25f
 
 
 
 
 
 
 
 
 
 
 
9586c06
 
 
 
 
d19f25f
 
 
 
 
dd4d624
 
d19f25f
 
 
dd4d624
f8a1e2a
 
d19f25f
f8a1e2a
 
d19f25f
 
 
 
 
 
 
 
 
 
 
 
 
f8a1e2a
 
d0bd74c
dd4d624
 
 
 
 
 
 
 
 
 
 
 
d19f25f
dd4d624
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d19f25f
 
 
 
 
 
 
dd4d624
d19f25f
 
 
 
 
 
 
 
 
 
 
 
 
dd4d624
d19f25f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dd4d624
d19f25f
 
dd4d624
d19f25f
 
3124adf
 
 
 
 
 
 
dd4d624
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d19f25f
 
 
 
 
 
dd4d624
 
 
 
f8a1e2a
dd4d624
 
 
 
f8a1e2a
 
dd4d624
 
f8a1e2a
dd4d624
 
 
 
 
 
 
f8a1e2a
dd4d624
d19f25f
 
 
 
 
 
dd4d624
 
 
 
 
 
 
 
 
 
 
 
 
d19f25f
 
dd4d624
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d19f25f
 
 
 
 
 
dd4d624
 
 
 
 
d0bd74c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f8a1e2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d0bd74c
dd4d624
 
d97ee23
d0bd74c
 
 
 
 
 
 
d97ee23
d0bd74c
dd4d624
f8a1e2a
dd4d624
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f8a1e2a
 
 
 
 
 
dd4d624
f8a1e2a
dd4d624
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d19f25f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
"""
MyeAI (Myeloma AI)
===========================================================
Takes a participant's answers to the 16 Shared Decision Making questions
and runs an adaptive follow-up conversation using Google's Gemini API.

GROUNDING:
General informational questions from the user are answered STRICTLY from the
documents in the uploaded ZIP corpus (Archive_2.zip) using retrieval-augmented
generation. At startup the app extracts text from every PDF/PNG in the corpus,
embeds it with Gemini embeddings, and builds a searchable index. Each user
question retrieves the most relevant passages, and Gemini is instructed to
answer only from those passages (or say the answer isn't in the documents).

Deploy on Hugging Face Spaces (SDK: Gradio).
- Upload Archive_2.zip to the Space repo root (or set KB_ZIP / KB_DIR).
- Set your Gemini key as a Space Secret named GEMINI_API_KEY.
- Recommended packages.txt: poppler-utils, tesseract-ocr  (see notes at bottom).
"""

import os
import json

# ----------------------------------------------------------------------
# ZeroGPU compatibility shim (harmless on CPU basic; skipped if not present).
# Satisfies "No @spaces.GPU function detected during startup" if the Space is
# on ZeroGPU. This app is API-only and does not use a GPU — CPU basic is fine.
# ----------------------------------------------------------------------
try:
    import spaces

    @spaces.GPU
    def _zerogpu_warmup():
        return None
except Exception:
    pass

from google import genai
from google.genai import types
import gradio as gr

from knowledge_base import KnowledgeBase

# ----------------------------------------------------------------------
# The 16 profile questions (verbatim from the intake form)
# ----------------------------------------------------------------------
QUESTIONS = [
    "I prefer my healthcare team (hematologist, nurse practitioner, or physician assistant), and I collaborate in deciding which treatment for relapsed or refractory myeloma is best for me",
    "It is important for me to understand my treatment options for relapsed or refractory myeloma",
    "I trust my healthcare team very much, that's why I leave it up to them to recommend therapy that is right for me",
    "I live alone with no caregiver",
    "I have strong social support and a network that can help with my treatment appointments",
    "I will do whatever it takes to so I can be cured, cancer-free, kill all the myeloma, or at least be in remission and live a long life",
    "I would like to take an aggressive approach to treat my myeloma",
    "I am willing to endure as many side effects as possible to control my myeloma",
    "I prefer to receive treatment in an outpatient setting",
    "I prefer to take medications at home",
    "I prefer to take the least possible amount of pills to control my cancer",
    "Quality of life is more important to me than quantity of life",
    "Clinical drug trial participation is of interest to me",
    "My out-of-pocket cost of treatment is important to me",
    "I prefer to continue an active lifestyle during my myeloma treatment",
    "I worry about how my treatment will affect future treatment options",
]

# Short tags used for the summary view
QUESTION_TAGS = [
    "Shared decision-making",
    "Wants to understand options",
    "Defers to care team",
    "Lives alone / no caregiver",
    "Strong social support",
    "Goal: cure / long life",
    "Wants aggressive approach",
    "Tolerant of side effects",
    "Prefers outpatient",
    "Prefers meds at home",
    "Prefers fewer pills",
    "Quality over quantity",
    "Interested in clinical trials",
    "Out-of-pocket cost matters",
    "Wants active lifestyle",
    "Worries about future options",
]

MODEL = "gemini-2.5-flash"
MAX_FOLLOWUPS = 8  # cap the adaptive conversation

# The conversation always opens with this fixed question from the patient.
FIRST_QUESTION = "Using my profile, what are the best treatment options for my myeloma?"

# ----------------------------------------------------------------------
# Knowledge base — built once at startup.
# ----------------------------------------------------------------------
KB = KnowledgeBase()
_KB_BUILD_ATTEMPTED = False


def ensure_kb(client):
    """Build the KB on first successful client. Returns (ready, message)."""
    global _KB_BUILD_ATTEMPTED
    if KB.ready:
        return True, ""
    if _KB_BUILD_ATTEMPTED and KB.error:
        return False, KB.error
    _KB_BUILD_ATTEMPTED = True
    KB.build(client)
    if KB.ready:
        return True, ""
    return False, (KB.error or "Knowledge base could not be initialized.")


# ----------------------------------------------------------------------
# Prompt building
# ----------------------------------------------------------------------
def build_profile_text(answers):
    """answers: list of 'Yes'/'No' aligned to QUESTIONS."""
    lines = []
    for q, tag, a in zip(QUESTIONS, QUESTION_TAGS, answers):
        lines.append(f"- [{a.upper()}] ({tag}) {q}")
    return "\n".join(lines)


def detect_tensions(answers):
    """Flag notable patterns/contradictions worth probing. Pure logic, no API."""
    a = {i: (answers[i].strip().lower() == "yes") for i in range(len(answers))}
    flags = []

    if (a.get(5) or a.get(6) or a.get(7)) and a.get(11):
        flags.append("Wants an aggressive/curative approach but also values quality of life over quantity. Worth clarifying how they weigh these when they conflict.")
    if a.get(7) and (a.get(14) or a.get(11)):
        flags.append("Willing to endure many side effects, yet wants to stay active / prioritizes quality of life. Probe acceptable side-effect threshold.")
    if a.get(3) and not a.get(4):
        flags.append("Lives alone with no caregiver and limited social support. Treatment logistics and safety monitoring need exploration.")
    if a.get(3) and a.get(9):
        flags.append("Lives alone but prefers taking medications at home. Explore support for safe self-administration and side-effect monitoring.")
    if a.get(2) and (a.get(0) or a.get(1)):
        flags.append("Says they leave decisions to the care team, yet also wants to collaborate / understand options. Clarify how involved they actually want to be.")
    if a.get(13) and a.get(12):
        flags.append("Cost matters and they're open to clinical trials — trials may reduce drug cost; worth surfacing.")
    if a.get(10) and (a.get(6) or a.get(7)):
        flags.append("Prefers the fewest pills possible but wants an aggressive approach. Explore tolerance for treatment intensity vs convenience.")
    if a.get(8) and a.get(3):
        flags.append("Prefers outpatient treatment but lives alone — explore transport and post-visit support.")
    return flags


def system_instruction():
    return (
        "You are a warm, plain-spoken health navigator helping a multiple myeloma patient "
        "(relapsed or refractory) prepare for a shared decision-making conversation with their "
        "care team. You are NOT a doctor and you never give medical advice, diagnoses, dosing, or "
        "treatment recommendations. Your job is to (a) ask thoughtful follow-up questions that help "
        "the patient clarify their own values, priorities, constraints, and concerns, and (b) answer "
        "the patient's general informational questions using ONLY the reference documents provided to "
        "you in the RETRIEVED CONTEXT for that turn.\n\n"
        "STRICT GROUNDING RULES (most important):\n"
        "A. For any general/informational question the patient asks (e.g. 'what is CAR T-cell "
        "therapy?', 'what are the side effects of stem cell transplant?', 'what does relapsed mean?'), "
        "you MUST base your answer solely on the text inside the RETRIEVED CONTEXT block for that turn. "
        "Do NOT use outside knowledge, and do NOT add facts that are not present in that context.\n"
        "B. If the RETRIEVED CONTEXT does not contain enough information to answer, say so plainly: "
        "\"I couldn't find that in the reference documents provided for this tool. Please ask your care "
        "team.\" Do not guess or fill gaps from general knowledge.\n"
        "C. When you use information from the context, attribute it naturally to the document "
        "TITLE(S) shown in each passage's [Document: ...] label — use that title, not a number, and "
        "never say \"Source 1/2/3\" (e.g. \"According to the NCCN Guideline ...\" or \"The CAR T cell "
        "Therapy — International Myeloma Foundation page notes ...\"). Use the title exactly as shown; "
        "do not invent or embellish document names.\n"
        "D. Never invent drug names, doses, statistics, or study results that are not in the context.\n\n"
        "CONVERSATION RULES:\n"
        "1. When you ask a follow-up question, ask exactly ONE question per turn, 1-3 sentences, "
        "conversational and jargon-free.\n"
        "2. Base follow-up questions on the patient's profile and previous answers. Prioritize the "
        "FLAGGED TENSIONS provided — gently explore apparent contradictions without judgment.\n"
        "3. Do not repeat questions already asked. Build on what they say.\n"
        "4. Never recommend or rank treatments. If the patient asks for medical advice or 'what should "
        "I do', kindly redirect them to their care team, then (if appropriate) continue with a follow-up "
        "question.\n"
        "5. Tone: empathetic, respectful, never alarming.\n"
        "6. TREATMENT-OPTIONS QUESTION: When the patient asks what the best treatment options are for "
        "their myeloma (e.g. 'Using my profile, what are the best treatment options for my myeloma?'), "
        "combine TWO sources: their 16 yes/no profile answers AND the RETRIEVED CONTEXT documents. Do "
        "this:\n"
        "   - Open with one warm sentence.\n"
        "   - Walk through how THEIR specific stated priorities map to the kinds of treatment approaches "
        "described in the RETRIEVED CONTEXT for relapsed/refractory myeloma. Tie each point explicitly "
        "back to their answers (for example: a preference for medications at home and fewest pills points "
        "toward asking about convenient outpatient or oral regimens; openness to clinical trials means "
        "trials are worth raising; willingness to endure side effects and wanting an aggressive approach "
        "points toward asking about more intensive options; prioritizing quality of life points toward "
        "regimens that protect daily functioning; living alone with limited support points toward "
        "logistics and monitoring).\n"
        "   - Only describe treatment approaches that actually appear in the RETRIEVED CONTEXT. Do NOT "
        "invent clinical details, drug names, dosing, or anything not supported by the context or implied "
        "by their answers. Do NOT rank, prescribe, or state which option is medically best. Frame "
        "everything as 'based on what you told us and these reference materials, here are options and "
        "questions to raise with your care team.'\n"
        "   - End by reminding them their care team must confirm what is medically appropriate, then ask "
        "your first single follow-up question drawn from their profile and the flagged tensions.\n"
        "7. When you judge that you have enough to summarize their priorities (or after several "
        "exchanges), instead of asking another question, output a final summary. Begin that final "
        "message with the exact token <<SUMMARY>> on its own line, then 4-7 short bullet points "
        "capturing the patient's key priorities, constraints, and questions to raise with their care team."
    )


# ----------------------------------------------------------------------
# Gemini client helpers
# ----------------------------------------------------------------------
def get_client(user_key):
    key = (user_key or "").strip() or os.environ.get("GEMINI_API_KEY", "").strip()
    if not key:
        return None, "No API key found. Set GEMINI_API_KEY as a Space secret."
    try:
        client = genai.Client(api_key=key)
        return client, None
    except Exception as e:
        return None, f"Could not initialize Gemini client: {e}"


def to_gemini_contents(history):
    """history: list of {'role': 'user'|'assistant', 'content': str}
    Returns Gemini Content list (assistant -> 'model')."""
    contents = []
    for m in history:
        role = "model" if m["role"] == "assistant" else "user"
        contents.append(types.Content(role=role, parts=[types.Part(text=m["content"])]))
    return contents


def _latest_user_query(convo):
    for m in reversed(convo):
        if m["role"] == "user":
            return m["content"]
    return ""


def gemini_reply(client, convo, n_followups):
    """convo: internal history list. Retrieves KB context for the latest user
    turn and injects it, then asks Gemini to answer grounded in that context."""
    # Retrieve relevant passages for the most recent user message.
    context_block = ""
    query = _latest_user_query(convo)
    if KB.ready and query.strip():
        try:
            retrieved = KB.retrieve(client, query)
            if retrieved:
                context_block = KB.context_block(retrieved)
        except Exception:
            context_block = ""

    contents = to_gemini_contents(convo)

    # Build the per-turn system instruction with the retrieved context appended.
    sys = system_instruction()
    if context_block:
        sys += (
            "\n\n==================== RETRIEVED CONTEXT (reference documents) ====================\n"
            "The following passages were retrieved from the uploaded reference documents for THIS "
            "turn. Answer general/informational questions using ONLY these passages. If they do not "
            "contain the answer, say you couldn't find it in the reference documents.\n\n"
            f"{context_block}\n"
            "================================================================================\n"
        )
    else:
        sys += (
            "\n\n[No reference passages were retrieved for this turn. If the patient asked a general "
            "informational question, tell them you couldn't find it in the reference documents and "
            "suggest they ask their care team. You may still ask a values-clarifying follow-up "
            "question or work with their profile.]\n"
        )

    if n_followups >= MAX_FOLLOWUPS - 1:
        sys += "\n\nYou have asked enough questions. Provide the final <<SUMMARY>> now."

    cfg = types.GenerateContentConfig(
        system_instruction=sys,
        temperature=0.3,          # lower temp -> stays closer to the sources
        # gemini-2.5-flash has "thinking" on by default, and those thinking
        # tokens are drawn from max_output_tokens. That could exhaust the budget
        # and truncate the visible answer mid-sentence (e.g. the long treatment-
        # options response). Disable thinking (budget=0) so the whole budget goes
        # to the answer, and raise the ceiling for extra headroom.
        max_output_tokens=8192,
        thinking_config=types.ThinkingConfig(thinking_budget=0),
    )
    resp = client.models.generate_content(model=MODEL, contents=contents, config=cfg)
    return (resp.text or "").strip()


# ----------------------------------------------------------------------
# Gradio app
# ----------------------------------------------------------------------
def start_chat(api_key, *radio_values):
    answers = [v if v in ("Yes", "No") else None for v in radio_values]
    if any(v is None for v in answers):
        missing = [i + 1 for i, v in enumerate(answers) if v is None]
        gr.Warning(f"Please answer all 16 questions. Missing: {missing}")
        return (gr.update(), [], [], 0, gr.update(visible=True), gr.update(visible=False))

    client, err = get_client(api_key)
    if err:
        gr.Warning(err)
        return (gr.update(), [], [], 0, gr.update(visible=True), gr.update(visible=False))

    # Build the knowledge base on first use (may take a bit on cold start).
    ready, msg = ensure_kb(client)
    if not ready:
        gr.Warning(f"Reference documents unavailable: {msg}")
        # We still allow the conversation, but answers will note missing docs.

    profile = build_profile_text(answers)
    flags = detect_tensions(answers)
    flag_text = "\n".join(f"- {f}" for f in flags) if flags else "- (No obvious contradictions; explore their highest-stakes priorities.)"

    seed = (
        "Here is the patient's completed profile (16 yes/no answers):\n\n"
        f"{profile}\n\n"
        "FLAGGED TENSIONS / PRIORITIES TO EXPLORE:\n"
        f"{flag_text}\n\n"
        "Acknowledge in one short sentence that you have their profile. Do NOT ask a question yet "
        "and do NOT list treatment options yet. Simply invite them to ask their question."
    )

    convo = [{"role": "user", "content": seed}]
    try:
        reply = gemini_reply(client, convo, 0)
    except Exception as e:
        gr.Warning(f"Gemini error: {e}")
        return (gr.update(), [], [], 0, gr.update(visible=True), gr.update(visible=False))

    convo.append({"role": "assistant", "content": reply})
    chat_display = [{"role": "assistant", "content": reply}]
    return (
        chat_display,
        chat_display,
        convo,
        0,
        gr.update(visible=False),
        gr.update(visible=True),
    )


def respond(user_msg, chat_display, convo, n_followups, api_key):
    user_msg = (user_msg or "").strip()
    if not user_msg:
        return chat_display, chat_display, convo, n_followups, ""

    client, err = get_client(api_key)
    if err:
        gr.Warning(err)
        return chat_display, chat_display, convo, n_followups, user_msg

    ensure_kb(client)  # no-op if already built

    convo = convo + [{"role": "user", "content": user_msg}]
    chat_display = chat_display + [{"role": "user", "content": user_msg}]

    try:
        reply = gemini_reply(client, convo, n_followups)
    except Exception as e:
        gr.Warning(f"Gemini error: {e}")
        return chat_display, chat_display, convo, n_followups, ""

    convo = convo + [{"role": "assistant", "content": reply}]

    display_reply = reply
    if "<<SUMMARY>>" in reply:
        display_reply = reply.replace("<<SUMMARY>>", "").strip()
        display_reply = "**Your Priorities Summary**\n\n" + display_reply + (
            "\n\n_This summary reflects what you shared. Please bring it to your care team. "
            "It is not medical advice._"
        )

    chat_display = chat_display + [{"role": "assistant", "content": display_reply}]
    return chat_display, chat_display, convo, n_followups + 1, ""


def reset_all():
    radio_resets = [gr.update(value=None) for _ in QUESTIONS]
    return (
        [],
        [],
        [],
        0,
        gr.update(visible=True),
        gr.update(visible=False),
        *radio_resets,
    )


CSS = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
.gradio-container, .gradio-container * {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif !important;
}
.gradio-container {
    background: #ffffff !important;
    color: #111111 !important;
    max-width: 880px !important;
    margin: 0 auto !important;
}
body, .gradio-container .prose, .gradio-container p,
.gradio-container span, .gradio-container label {
    color: #111111 !important;
}
#app-header {
    border-bottom: 1px solid #e6e6e6;
    padding: 4px 0 18px 0;
    margin-bottom: 6px;
}
#app-title {
    font-size: 1.7rem;
    font-weight: 700;
    color: #111111;
    letter-spacing: -0.01em;
    margin: 0;
}
.q-card {
    border: 1px solid #e2e2e2 !important;
    border-radius: 8px !important;
    padding: 16px 18px !important;
    margin-bottom: 12px !important;
    background: #ffffff !important;
    box-shadow: 0 1px 2px rgba(0,0,0,0.04) !important;
}
.q-card label, .q-card span {
    color: #111111 !important;
    font-weight: 500 !important;
}
.gradio-container input[type="radio"] + span,
.gradio-container .wrap label {
    color: #111111 !important;
}
button.primary, .gradio-container button.primary {
    background: #7a0c2e !important;
    color: #ffffff !important;
    border: none !important;
    border-radius: 6px !important;
    font-weight: 600 !important;
}
button.primary:hover, .gradio-container button.primary:hover {
    background: #5f0a24 !important;
}
button.secondary, .gradio-container button.secondary {
    background: #ffffff !important;
    color: #111111 !important;
    border: 1px solid #cfcfcf !important;
    border-radius: 6px !important;
    font-weight: 600 !important;
}
.gradio-container .chatbot, .gradio-container [class*="chatbot"] {
    background: #ffffff !important;
    border: 1px solid #e2e2e2 !important;
    border-radius: 8px !important;
}
.gradio-container .message.bot, .gradio-container .message.user {
    color: #111111 !important;
}
.gradio-container textarea, .gradio-container input[type="text"],
.gradio-container input[type="password"] {
    background: #ffffff !important;
    color: #111111 !important;
    border: 1px solid #cfcfcf !important;
    border-radius: 6px !important;
}
.gradio-container .label-wrap, .gradio-container details summary {
    color: #111111 !important;
}
.sample-q {
    background: #f7f7f7 !important;
    border: 1px solid #e2e2e2 !important;
    border-left: 3px solid #7a0c2e !important;
    border-radius: 6px !important;
    padding: 10px 14px !important;
    margin: 12px 0 4px 0 !important;
}
.sample-q-label {
    display: block;
    font-size: 0.85rem;
    font-weight: 600;
    color: #5a5a5a !important;
    margin-bottom: 4px;
}
.sample-q-text {
    display: block;
    font-size: 0.95rem;
    color: #111111 !important;
    background: #ffffff !important;
    border: 1px solid #e2e2e2 !important;
    border-radius: 4px !important;
    padding: 6px 10px !important;
    font-family: 'Inter', sans-serif !important;
}
footer {visibility: hidden;}
"""

with gr.Blocks(title="MyeAI (Myeloma AI)", css=CSS,
               theme=gr.themes.Base(
                   primary_hue=gr.themes.colors.red,
                   neutral_hue=gr.themes.colors.gray,
                   font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
               )) as demo:
    with gr.Column(elem_id="app-header"):
        gr.HTML(
            "<h1 id='app-title'>MyeAI (Myeloma AI)</h1>"
        )

    api_key = gr.State("")
    convo_state = gr.State([])
    display_state = gr.State([])
    followups = gr.State(0)

    with gr.Group(visible=True) as intake_panel:
        gr.Markdown("**Answer all 16 questions, then click Start Follow-up.**")
        radios = []
        for i, q in enumerate(QUESTIONS):
            with gr.Row(elem_classes="q-card"):
                r = gr.Radio(["Yes", "No"], label=f"{i+1}. {q}")
                radios.append(r)
        start_btn = gr.Button("Start Follow-up", variant="primary")

    with gr.Group(visible=False) as chat_panel:
        chatbot = gr.Chatbot(label="Follow-up Conversation", type="messages", height=460)
        gr.HTML(
            "<div class='sample-q'>"
            "<span class='sample-q-label'>Sample question &mdash; copy and paste it into the box below to begin:</span>"
            f"<code class='sample-q-text'>{FIRST_QUESTION}</code>"
            "</div>"
        )
        with gr.Row():
            msg = gr.Textbox(placeholder="Type or paste your question here...", show_label=False, scale=8)
            send_btn = gr.Button("Send", variant="primary", scale=1)
        restart_btn = gr.Button("Start Over")

    start_btn.click(
        start_chat,
        inputs=[api_key] + radios,
        outputs=[chatbot, display_state, convo_state, followups, intake_panel, chat_panel],
    )
    send_btn.click(
        respond,
        inputs=[msg, display_state, convo_state, followups, api_key],
        outputs=[chatbot, display_state, convo_state, followups, msg],
    )
    msg.submit(
        respond,
        inputs=[msg, display_state, convo_state, followups, api_key],
        outputs=[chatbot, display_state, convo_state, followups, msg],
    )
    restart_btn.click(
        reset_all,
        inputs=None,
        outputs=[chatbot, display_state, convo_state, followups, intake_panel, chat_panel] + radios,
    )

if __name__ == "__main__":
    demo.launch()

# ----------------------------------------------------------------------
# HUGGING FACE SPACE SETUP NOTES
# ----------------------------------------------------------------------
# 1) Files in the Space repo:
#      app.py
#      knowledge_base.py
#      Archive_2.zip          <- the uploaded corpus (repo root)
#      requirements.txt       <- google-genai, gradio, numpy, pypdf, pdf2image, pytesseract, pillow
#      packages.txt           <- poppler-utils, tesseract-ocr   (system deps for PDF text + OCR)
# 2) Space Secret:
#      GEMINI_API_KEY = <your key>
# 3) Hardware: CPU basic is sufficient (no GPU needed).
# 4) On first launch the app extracts + embeds the corpus once and caches the
#    index to _kb_index.npz, so subsequent restarts are fast.