""" MedVision AI — Medical Imaging Assistant Gradio app for Build Small Hackathon (Hugging Face Spaces) """ import warnings from typing import Dict, List, Optional, Tuple import gradio as gr from PIL import Image from utils import ( LangChainMedicalChatbot, MedGemmaRunnable, ) warnings.filterwarnings("ignore") # --------------------------------------------------------------------------- # Module-level client state # --------------------------------------------------------------------------- medgemma_llm = None chatbot: Optional[LangChainMedicalChatbot] = None model_load_error: Optional[str] = None DISCLAIMER_HTML = """
""" CUSTOM_CSS = """ :root { --bg-primary: #0f172a; --bg-card: #1e293b; --accent: #14b8a6; --accent-hover: #0d9488; --text-primary: #f1f5f9; --text-muted: #94a3b8; --user-bubble: #1d4ed8; } .gradio-container { background: var(--bg-primary) !important; color: var(--text-primary) !important; max-width: 1400px !important; } .app-header { background: linear-gradient(135deg, #0f172a 0%, #1e3a5f 50%, #0f766e 100%); border: 1px solid var(--accent); border-radius: 12px; padding: 1.5rem 2rem; margin-bottom: 1rem; text-align: center; } .app-header h1 { color: #f8fafc; font-size: 1.75rem; margin: 0 0 0.25rem 0; font-weight: 700; } .app-header p { color: var(--text-muted); margin: 0; font-size: 0.95rem; } .disclaimer-banner { background: linear-gradient(90deg, #78350f, #92400e); border: 1px solid #f59e0b; border-radius: 8px; padding: 0.75rem 1rem; color: #fef3c7; font-size: 0.875rem; margin-bottom: 1rem; line-height: 1.5; } .block, .panel { background: var(--bg-card) !important; border: 1px solid #334155 !important; border-radius: 10px !important; } label, .label-wrap span { color: var(--text-primary) !important; } button.primary { background: var(--accent) !important; border-color: var(--accent) !important; color: #0f172a !important; font-weight: 600 !important; } button.primary:hover { background: var(--accent-hover) !important; } button.secondary { border-color: #475569 !important; color: var(--text-muted) !important; } .model-status { padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.85rem; margin-bottom: 0.75rem; } .model-status.loading { background: #1e3a5f; border: 1px solid #3b82f6; color: #93c5fd; } .model-status.ready { background: #064e3b; border: 1px solid var(--accent); color: #5eead4; } .model-status.error { background: #450a0a; border: 1px solid #ef4444; color: #fca5a5; } /* Chatbot bubbles */ .message.user { background: var(--user-bubble) !important; border-color: #2563eb !important; } .message.bot, .message.assistant { background: var(--bg-card) !important; border: 1px solid #334155 !important; } .chatbot { border: 1px solid var(--accent) !important; border-radius: 10px !important; } /* Findings clinical report cards */ .findings-card { display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0.5rem 0; } .finding-tag { background: #0f766e; border: 1px solid var(--accent); border-radius: 6px; padding: 0.35rem 0.75rem; font-size: 0.8rem; color: #ccfbf1; font-family: 'Courier New', monospace; } .memory-display textarea { font-family: 'Courier New', Consolas, monospace !important; font-size: 0.8rem !important; background: #0f172a !important; color: #a5f3fc !important; border: 1px solid #334155 !important; } footer { display: none !important; } """ def load_model() -> str: """Initialize MedGemma Modal endpoint and LangChain chatbot. Returns status HTML.""" global medgemma_llm, chatbot, model_load_error if chatbot is not None: return _status_html("ready", "✅ MedGemma client ready.") print("📡 Connecting to MedGemma Modal endpoint...") try: medgemma_llm = MedGemmaRunnable() chatbot = LangChainMedicalChatbot(medgemma_llm) print("✅ MedGemma client initialized") except Exception as exc: model_load_error = str(exc) return _status_html("error", f"❌ Failed to initialize: {exc}") return _status_html("ready", "✅ Connected to MedGemma API · Modal endpoint") def _status_html(state: str, message: str) -> str: return f'No findings detected yet.
' tags = "".join( f'{label} ({score:.0%})' for label, score in findings_dict.items() ) return f'Session reset. Upload an image and ask a clinical question to begin.
', [], ) # --------------------------------------------------------------------------- # Build Gradio UI # --------------------------------------------------------------------------- model_status_html = load_model() with gr.Blocks( title="MedVision AI", theme=gr.themes.Base( primary_hue="teal", secondary_hue="blue", neutral_hue="slate", ).set( body_background_fill="#0f172a", block_background_fill="#1e293b", block_border_color="#334155", body_text_color="#f1f5f9", ), css=CUSTOM_CSS, ) as demo: gr.HTML( """Powered by MedGemma Modal API · LangChain
Findings will appear after analysis.
' ) with gr.Accordion("🧠 Conversation Memory", open=False): memory_output = gr.Textbox( label="LangChain Memory (raw)", lines=8, interactive=False, elem_classes=["memory-display"], ) analyze_btn.click( fn=run_analysis, inputs=[image_input, image_type, chain_mode, clinical_question, chat_state], outputs=[chatbot_ui, findings_label, stats_output, memory_output, findings_html, chat_state], ) reset_btn.click( fn=reset_session, outputs=[chatbot_ui, findings_label, stats_output, memory_output, findings_html, chat_state], ) gr.Markdown( """ --- **MedVision AI** · Powered by MedGemma 1.5 via Modal · Endpoint configured in `utils.py`. """ ) if __name__ == "__main__": demo.launch(share=False, server_name="0.0.0.0")