>", "").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}]
revision = _persist(email, can_save, revision, convo=convo,
chat_display=chat_display, followups=n_followups + 1)
_log_turns(email, can_save, [
{"role": "user", "content": user_msg, "turn": n_followups + 1},
{"role": "assistant", "content": display_reply, "turn": n_followups + 1},
])
return (chat_display, chat_display, convo, n_followups + 1, revision, "") + dismiss
def show_reset_warning():
"""Start Over is destructive, so it asks first."""
gr.Warning("Starting over will erase your current chat history and begin a "
"new session.")
return (
gr.update(visible=True), # reset_confirm
gr.update(visible=False), # restart_btn — replaced by the choice
)
def cancel_reset():
"""Back out of Start Over, leaving the conversation untouched."""
return (
gr.update(visible=False), # reset_confirm
gr.update(visible=True), # restart_btn
)
def reset_all(email, can_save, revision):
"""Start Over, confirmed: clear this page AND the stored session, so a
participant who restarts and leaves is not dropped back into the
conversation they abandoned. The p3_messages transcript log is deliberately
left intact, so nothing is lost for the study."""
revision = _persist(email, can_save, revision, answers=[None] * len(QUESTIONS),
convo=[], chat_display=[], followups=0)
radio_resets = [gr.update(value=None) for _ in QUESTIONS]
return (
[],
[],
[],
0,
revision,
gr.update(visible=True), # intake_panel
gr.update(visible=False), # chat_panel
gr.update(value=""), # msg — drop any unsent draft
gr.update(visible=False), # reset_confirm
gr.update(visible=True), # restart_btn
*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;
}
#status-line {
font-size: 0.9rem !important;
color: #444444 !important;
margin: 2px 0 0 0 !important;
}
#status-line p {color: #444444 !important; margin: 0 !important;}
#switch-btn {
max-width: 200px !important;
margin: 6px 0 10px 0 !important;
}
.email-card {
border: 1px solid #e2e2e2 !important;
border-radius: 8px !important;
padding: 18px 20px !important;
background: #ffffff !important;
box-shadow: 0 1px 2px rgba(0,0,0,0.04) !important;
}
#app-header {
align-items: flex-start !important;
}
#user-id-badge {
text-align: right !important;
font-size: 0.85rem !important;
color: #5a5a5a !important;
line-height: 1.3 !important;
}
#user-id-badge p {
color: #5a5a5a !important;
margin: 0 !important;
text-align: right !important;
}
#user-id-badge strong {
color: #7a0c2e !important;
font-size: 1rem !important;
letter-spacing: 0.02em;
}
button.stop, .gradio-container button.stop {
background: #b3261e !important;
color: #ffffff !important;
border: none !important;
border-radius: 6px !important;
font-weight: 600 !important;
}
button.stop:hover, .gradio-container button.stop:hover {
background: #8c1d16 !important;
}
.reset-warning {
border: 1px solid #7a0c2e !important;
border-left: 4px solid #7a0c2e !important;
border-radius: 8px !important;
padding: 14px 16px !important;
margin-top: 8px !important;
background: #fdf6f8 !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.Row(elem_id="app-header"):
gr.HTML("MyeAI (Myeloma AI)
")
# The sign-in error for a wrong User ID points participants here, so it
# has to be visible on every screen once they are signed in.
user_badge = gr.Markdown("", elem_id="user-id-badge")
api_key = gr.State("")
convo_state = gr.State([])
display_state = gr.State([])
followups = gr.State(0)
email_state = gr.State("")
# False when we could not read this participant's stored row, so we must
# not write over it. True otherwise.
can_save = gr.State(True)
# Optimistic lock guarding against a stale tab / second device.
revision = gr.State(0)
# Throttles the "your answers are not being saved" warning to once.
answers_warned = gr.State(False)
user_id_state = gr.State("")
# Consecutive failed User ID attempts, so the second one can point the
# participant at where their User ID is shown.
uid_attempts = gr.State(0)
status_md = gr.Markdown("", elem_id="status-line")
switch_btn = gr.Button("Use a different email", size="sm",
visible=False, elem_id="switch-btn")
with gr.Group(visible=True, elem_classes="email-card") as email_panel:
gr.Markdown(
"**Enter your User ID and email address to begin.**\n\n"
"Your answers and your conversation are saved against these, so you "
"can close this page and come back later to pick up exactly where you left off."
)
user_id_box = gr.Textbox(label="User ID", placeholder="e.g. P3-014",
max_lines=1, autofocus=True)
email_box = gr.Textbox(label="Email address", placeholder="name@example.com",
max_lines=1)
email_btn = gr.Button("Continue", variant="primary")
with gr.Group(visible=False) 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(
""
"Sample question — copy and paste it into the box below to begin:"
f"{FIRST_QUESTION}"
"
"
)
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")
with gr.Group(visible=False, elem_classes="reset-warning") as reset_confirm:
gr.Markdown(
"**Are you sure you want to start over?**\n\n"
"This erases your current chat history and begins a new session. "
"You will answer the 16 questions again."
)
with gr.Row():
confirm_reset_btn = gr.Button("Yes, erase and start over",
variant="stop")
cancel_reset_btn = gr.Button("Cancel")
# Sign-in: one shared output list for every outcome (new / returning / error).
gate_outputs = [
user_id_state, email_state, chatbot, display_state, convo_state,
followups, can_save, revision, uid_attempts,
user_badge, email_panel, intake_panel, chat_panel, status_md,
switch_btn, msg, reset_confirm, restart_btn,
] + radios
gate_inputs = [user_id_box, email_box, uid_attempts]
email_btn.click(sign_in, inputs=gate_inputs, outputs=gate_outputs)
user_id_box.submit(sign_in, inputs=gate_inputs, outputs=gate_outputs)
email_box.submit(sign_in, inputs=gate_inputs, outputs=gate_outputs)
switch_btn.click(switch_email, inputs=None,
outputs=gate_outputs + [user_id_box, email_box])
# Save the questionnaire as it is filled in, so a half-finished intake
# survives a closed tab.
# `.input` rather than `.change`: fires only on a real click, so restoring
# or clearing the form does not stampede 16 writes back at Supabase.
for r in radios:
r.input(
save_partial_answers,
inputs=[email_state, can_save, answers_warned] + radios,
outputs=[answers_warned],
)
start_btn.click(
start_chat,
inputs=[api_key, email_state, can_save, revision,
display_state, convo_state, followups] + radios,
outputs=[chatbot, display_state, convo_state, followups, revision,
intake_panel, chat_panel],
)
send_btn.click(
respond,
inputs=[msg, display_state, convo_state, followups, api_key, email_state,
can_save, revision],
outputs=[chatbot, display_state, convo_state, followups, revision, msg,
reset_confirm, restart_btn],
)
msg.submit(
respond,
inputs=[msg, display_state, convo_state, followups, api_key, email_state,
can_save, revision],
outputs=[chatbot, display_state, convo_state, followups, revision, msg,
reset_confirm, restart_btn],
)
# Start Over warns first; only the confirmation actually erases anything.
restart_btn.click(
show_reset_warning,
inputs=None,
outputs=[reset_confirm, restart_btn],
)
cancel_reset_btn.click(
cancel_reset,
inputs=None,
outputs=[reset_confirm, restart_btn],
)
confirm_reset_btn.click(
reset_all,
inputs=[email_state, can_save, revision],
outputs=[chatbot, display_state, convo_state, followups, revision,
intake_panel, chat_panel, msg, reset_confirm, restart_btn] + 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 =
# 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.