Spaces:
Sleeping
Sleeping
| """ | |
| 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 = """ | |
| <div class="disclaimer"> | |
| β οΈ <strong>Important:</strong> PharmaGuide provides general information only. | |
| Always talk to your <strong>doctor or pharmacist</strong> before making any changes to your medications. | |
| In an emergency, call <strong>911</strong>. | |
| </div> | |
| """ | |
| _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, | |
| } | |