""" 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, }