"""
ui_components.py — Elder-friendly Gradio UI layout for PharmaGuide.
Design principles (from CLAUDE.md):
- Large fonts (minimum 18px body, 28px headings)
- High contrast (dark text on light background)
- Simple language in all labels and placeholders
- One clear action per tab
- Warm, friendly tone
- Prominent disclaimer
This module exports:
CUSTOM_CSS : CSS string injected into the Gradio Blocks theme
build_ui() : Builds and returns the gr.Blocks app object
app.py calls build_ui() and attaches the inference callbacks to the
returned component references.
"""
import gradio as gr
# ── Custom CSS ───────────────────────────────────────────────────────────────
CUSTOM_CSS = """
/* ── Base font size — elder-friendly ── */
.gradio-container {
font-size: 18px !important;
font-family: 'Segoe UI', Arial, sans-serif !important;
max-width: 860px !important;
margin: 0 auto !important;
}
/* ── Headings ── */
h1 { font-size: 32px !important; color: #1a3a5c !important; }
h2 { font-size: 24px !important; color: #1a3a5c !important; }
h3 { font-size: 20px !important; color: #2c5282 !important; }
/* ── Primary button — large and clear ── */
.primary-btn button {
font-size: 20px !important;
padding: 14px 28px !important;
border-radius: 8px !important;
font-weight: 600 !important;
}
/* ── Input fields — larger text ── */
textarea, input[type="text"] {
font-size: 18px !important;
line-height: 1.5 !important;
}
/* ── Output markdown — readable line height ── */
.output-markdown {
font-size: 18px !important;
line-height: 1.7 !important;
}
/* ── Age slider label ── */
.age-slider label {
font-size: 18px !important;
font-weight: 600 !important;
}
/* ── Status / loading messages ── */
.status-box {
font-size: 16px !important;
color: #4a5568 !important;
font-style: italic;
}
/* ── Disclaimer box ── */
.disclaimer {
background: #fff3cd !important;
border-left: 5px solid #f6ad55 !important;
padding: 12px 16px !important;
border-radius: 6px !important;
font-size: 16px !important;
}
/* ── Tab labels ── */
.tab-nav button {
font-size: 17px !important;
padding: 10px 20px !important;
}
/* ── Hide Gradio footer ── */
footer { display: none !important; }
"""
# ── Header markdown ──────────────────────────────────────────────────────────
_HEADER_MD = """
# 💊 PharmaGuide
### Your Personal Medication Helper
**I can explain your medicines in plain, simple language.**
Enter your medications below or take a photo of a pill bottle — I'll do the rest.
"""
_DISCLAIMER_MD = """
⚠️ Important: PharmaGuide provides general information only.
Always talk to your doctor or pharmacist before making any changes to your medications.
In an emergency, call 911.
"""
_LIFESTYLE_EMOJI = {
"alcohol": "🍷",
"grapefruit": "🍊",
"food": "🍽️",
"dairy": "🥛",
"sun": "🌞",
"driving": "🚗",
"exercise": "🏃",
}
_LIFESTYLE_LABEL = {
"alcohol": "Alcohol",
"grapefruit": "Grapefruit juice",
"food": "Food timing",
"dairy": "Dairy / antacids",
"sun": "Sunlight",
"driving": "Driving / machinery",
"exercise": "Exercise / heat",
}
# ── Lifestyle card formatter ─────────────────────────────────────────────────
def format_lifestyle_card(lifestyle_data: dict) -> str:
"""
Convert the dict returned by get_lifestyle_warnings() into a
Markdown-formatted card for display in the UI output box.
Args:
lifestyle_data: {drug_name: {category: [sentence, ...]}}
Returns:
Markdown string, or "" if no lifestyle warnings found.
"""
if not lifestyle_data:
return ""
lines = ["---", "### ⚠️ Things to be careful about while taking your medicines", ""]
for drug_name, categories in lifestyle_data.items():
if not categories:
continue
lines.append(f"**{drug_name}**")
for category, sentences in categories.items():
emoji = _LIFESTYLE_EMOJI.get(category, "•")
label = _LIFESTYLE_LABEL.get(category, category.title())
lines.append(f"{emoji} **{label}**")
for sentence in sentences[:2]:
lines.append(f" {sentence.strip()}")
lines.append("")
lines.append(
"_These are general guidelines. "
"Your pharmacist can give you advice specific to your situation._"
)
return "\n".join(lines)
# ── UI builder ───────────────────────────────────────────────────────────────
def build_ui(callbacks=None):
"""
Build the Gradio Blocks app and return it along with the key component
references that app.py needs to attach callbacks to.
Args:
callbacks: dict with keys on_text_submit, on_photo_submit, on_voice_submit.
Wired inside the Blocks context (required for Gradio 6+).
Returns:
demo : gr.Blocks instance
components : dict of component references
"""
with gr.Blocks(title="PharmaGuide — Your Medication Helper") as demo:
gr.Markdown(_HEADER_MD)
with gr.Tabs():
# ── Tab 1: Type medications ──────────────────────────────────
with gr.Tab("📝 Type Your Medications"):
gr.Markdown(
"**List your medications below.** Separate them with commas. "
"You can include 1 to 10 medicines."
)
drug_input = gr.Textbox(
label="Your medications",
placeholder="Example: metformin, lisinopril, aspirin, atorvastatin",
lines=3,
max_lines=5,
)
age_input = gr.Slider(
label="Your age",
minimum=50,
maximum=100,
value=70,
step=1,
elem_classes=["age-slider"],
)
submit_btn = gr.Button(
"🔍 Check My Medications",
variant="primary",
elem_classes=["primary-btn"],
)
# ── Tab 2: Photo of pill bottle ──────────────────────────────
with gr.Tab("📸 Take a Photo of Your Pill Bottle"):
gr.Markdown(
"**Take a clear photo of your pill bottle label** and upload it here. "
"I'll read the medicine name and look it up for you."
)
image_input = gr.Image(
label="Photo of your pill bottle",
type="pil",
sources=["upload", "webcam"],
)
age_input_photo = gr.Slider(
label="Your age",
minimum=50,
maximum=100,
value=70,
step=1,
elem_classes=["age-slider"],
)
photo_btn = gr.Button(
"📷 Look Up This Medicine",
variant="primary",
elem_classes=["primary-btn"],
)
# ── Tab 3: Speak your medications ────────────────────────────
with gr.Tab("🎤 Speak Your Medications"):
gr.Markdown(
"**Press the microphone button and say your medication names.**\n\n"
"For example, you can say:\n"
"*\"I take metformin, lisinopril, and aspirin\"*\n\n"
"You can also describe a concern, like:\n"
"*\"I just started warfarin — what should I watch out for?\"*"
)
audio_input = gr.Audio(
label="Press the microphone to start recording",
sources=["microphone"],
type="filepath",
)
age_input_voice = gr.Slider(
label="Your age",
minimum=50,
maximum=100,
value=70,
step=1,
elem_classes=["age-slider"],
)
voice_btn = gr.Button(
"🎤 Check What I Said",
variant="primary",
elem_classes=["primary-btn"],
)
# Show the transcription so the user can confirm what was heard
transcript_box = gr.Markdown(
value="",
label="What I heard",
elem_classes=["status-box"],
)
# ── Status / loading indicator ───────────────────────────────────
status_box = gr.Markdown(
value="",
elem_classes=["status-box"],
)
# ── Main response area ───────────────────────────────────────────
output_box = gr.Markdown(
value="",
label="What PharmaGuide Found",
elem_classes=["output-markdown"],
)
# ── Disclaimer ───────────────────────────────────────────────────
gr.HTML(_DISCLAIMER_MD)
# ── Example queries (makes judging easier) ───────────────────────
gr.Examples(
examples=[
["metformin, lisinopril, aspirin", 70],
["atorvastatin, omeprazole, warfarin, metoprolol", 75],
["furosemide, potassium chloride, digoxin", 80],
],
inputs=[drug_input, age_input],
label="Quick examples — click to try",
)
# ── Wire callbacks inside context (required for Gradio 6+) ──────
if callbacks:
submit_btn.click(
fn = callbacks["on_text_submit"],
inputs = [drug_input, age_input],
outputs = [status_box, output_box],
)
photo_btn.click(
fn = callbacks["on_photo_submit"],
inputs = [image_input, age_input_photo],
outputs = [status_box, output_box],
)
voice_btn.click(
fn = callbacks["on_voice_submit"],
inputs = [audio_input, age_input_voice],
outputs = [status_box, transcript_box, output_box],
)
return demo, {
"drug_input": drug_input,
"age_input": age_input,
"age_input_photo": age_input_photo,
"submit_btn": submit_btn,
"image_input": image_input,
"photo_btn": photo_btn,
"audio_input": audio_input,
"age_input_voice": age_input_voice,
"voice_btn": voice_btn,
"transcript_box": transcript_box,
"output_box": output_box,
"status_box": status_box,
}