""" Wemed AI Counsellor β€” Gradio 6 β€” Concept B split-pane (light/mint) """ import os, requests, gradio as gr from dotenv import load_dotenv from prompts import SYSTEM_PROMPT, INTAKE_FIELDS, build_intake_summary load_dotenv() MODAL_ENDPOINT = os.getenv("MODAL_ENDPOINT", "") def stream_modal(messages): if not MODAL_ENDPOINT: import time mock = ( "Namaste! Based on what you've shared, here are my top picks:\n\n" "**1. Russia** πŸ‡·πŸ‡Ί β€” A great fit for your β‚Ή20–30L budget. Kazan Federal University " "is NMC-recognized with a strong Indian student community.\n\n" "**2. Kazakhstan** πŸ‡°πŸ‡Ώ β€” More affordable (β‚Ή18–25L) and closer to home. " "Al-Farabi KNU is an excellent option.\n\n" "**3. Kyrgyzstan** πŸ‡°πŸ‡¬ β€” Most budget-friendly at β‚Ή18–22L. " "Osh State University has solid FMGE pass rates.\n\n" "Want me to compare FMGE pass rates, or talk through the admission process? 😊" ) for word in mock.split(" "): yield word + " " time.sleep(0.02) return try: import codecs decoder = codecs.getincrementaldecoder("utf-8")() with requests.post(MODAL_ENDPOINT, json={"messages": messages, "max_tokens": 1024, "temperature": 0.7}, headers={"Accept-Encoding": "identity"}, stream=True, timeout=120) as r: r.raise_for_status() for chunk in r.iter_content(chunk_size=None): if chunk: yield decoder.decode(chunk) yield decoder.decode(b"", final=True) except requests.exceptions.Timeout: yield "\n\n⏳ Model warming up β€” please retry!" except Exception as e: yield f"\n\n❌ {e}" def swap_to_chat(): # non-generator: reliably toggles panel visibility return gr.update(visible=False), gr.update(visible=True) def start_chat(budget, neet_score, country_pref, concern, name): intake = {"budget": budget, "neet_score": neet_score, "country_pref": country_pref, "concern": concern, "name": name.strip() if name else ""} first = build_intake_summary(intake) msgs = [{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": first}] history = [gr.ChatMessage(role="user", content=first), gr.ChatMessage(role="assistant", content="")] acc = "" for tok in stream_modal(msgs): acc += tok history[-1] = gr.ChatMessage(role="assistant", content=acc) yield history def _to_str(content): """Safely convert any Gradio message content to a plain string.""" if content is None: return "" if isinstance(content, str): return content if isinstance(content, list): parts = [] for item in content: if isinstance(item, str): parts.append(item) elif isinstance(item, dict): parts.append(item.get("text", item.get("content", str(item)))) elif hasattr(item, "text"): parts.append(str(item.text)) else: parts.append(str(item)) return "".join(parts) if isinstance(content, dict): return content.get("text", content.get("content", str(content))) return str(content) def respond(user_msg, history): if not user_msg.strip(): yield history, ""; return msgs = [{"role": "system", "content": SYSTEM_PROMPT}] for m in history: r = m.role if hasattr(m, "role") else m.get("role", "user") c = m.content if hasattr(m, "content") else m.get("content", "") msgs.append({"role": str(r), "content": _to_str(c)}) msgs.append({"role": "user", "content": str(user_msg)}) # Debug: print the message list being sent print("[DEBUG respond] Sending messages to Modal:") for i, msg in enumerate(msgs): print(f" [{i}] role={msg['role']!r}, content_type={type(msg['content']).__name__}, len={len(msg['content'])}") if i > 0: # skip system prompt (too long) print(f" preview: {msg['content'][:100]!r}") print(f" Total messages: {len(msgs)}") history = list(history) + [ gr.ChatMessage(role="user", content=user_msg), gr.ChatMessage(role="assistant", content=""), ] acc = "" for tok in stream_modal(msgs): acc += tok history[-1] = gr.ChatMessage(role="assistant", content=acc) yield history, "" def reset(): return gr.update(visible=True), gr.update(visible=False), [] CSS = """ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap'); /* Force light surfaces β€” override Gradio's dark-mode CSS vars directly */ :root, .dark { --background-fill-primary: #ffffff !important; --background-fill-secondary: #ffffff !important; --block-background-fill: #ffffff !important; --body-background-fill: #eef4f0 !important; --panel-background-fill: #ffffff !important; --border-color-primary: transparent !important; --block-border-color: transparent !important; --block-label-background-fill: transparent !important; --block-info-text-color: #5d7a6b !important; --body-text-color: #1d3b2c !important; --body-text-color-subdued: #5d7a6b !important; --block-title-text-color: #1d3b2c !important; --block-label-text-color: #5d7a6b !important; --input-text-color: #1d3b2c !important; color-scheme: light !important; } html, body, .gradio-container, gradio-app, .dark { background: #eef4f0 !important; color-scheme: light !important; } html, body { margin: 0 !important; padding: 0 !important; font-family: 'Space Grotesk', sans-serif !important; } .gradio-container { max-width: 1060px !important; margin: 0 auto !important; padding: 36px 20px !important; } footer { display: none !important; } /* neutralize default grey blocks/wrappers so cards look clean */ .block, .form, .gr-box, .gr-group { background: transparent !important; border: none !important; box-shadow: none !important; } /* kill dark wrapper boxes around dropdowns/inputs in dark mode */ #intake-card .block, #intake-card .form, #intake-card .wrap, #intake-card [class*="container"] { background: transparent !important; border: none !important; box-shadow: none !important; } /* dropdown label text β€” keep readable */ #intake-card .label-wrap span, #intake-card label span, #intake-card span[data-testid="block-info"] { color: #5d7a6b !important; opacity: 1 !important; } /* ── The split card wraps both panes ── */ #split-card { background: #ffffff !important; border-radius: 32px !important; border: 0.5px solid #d4e6dc !important; overflow: hidden !important; box-shadow: 0 2px 4px rgba(29,59,44,0.04), 0 24px 60px rgba(29,59,44,0.09) !important; padding: 0 !important; } #split-row { gap: 0 !important; flex-wrap: nowrap !important; } /* ── LEFT BRAND PANE ── */ #brand-pane { background: #2f9e6e !important; min-height: 600px !important; padding: 44px 40px !important; position: relative !important; overflow: hidden !important; display: flex !important; flex-direction: column !important; justify-content: space-between !important; } .bp-orb1 { position: absolute; top: -70px; right: -70px; width: 280px; height: 280px; border-radius: 50%; background: rgba(255,255,255,0.08); pointer-events: none; } .bp-orb2 { position: absolute; bottom: -50px; left: -50px; width: 200px; height: 200px; border-radius: 50%; background: rgba(255,255,255,0.06); pointer-events: none; } .bp-logo { display: flex; align-items: center; gap: 11px; margin-bottom: 48px; position: relative; } .bp-logo-mark { width: 42px; height: 42px; border-radius: 13px; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 21px; } .bp-logo-name { color: #fff; font-size: 16px; font-weight: 600; } .bp-logo-sub { color: rgba(255,255,255,0.7); font-size: 11px; margin-top: 1px; } .bp-title { font-size: 38px; font-weight: 600; color: #fff; line-height: 1.12; letter-spacing: -1px; margin-bottom: 14px; position: relative; } .bp-desc { font-size: 14px; color: rgba(255,255,255,0.8); line-height: 1.65; max-width: 280px; position: relative; } .bp-stats { display: flex; gap: 28px; margin-bottom: 26px; position: relative; } .bp-stat-n { font-size: 24px; font-weight: 700; color: #fff; line-height: 1; margin-bottom: 4px; } .bp-stat-l { font-size: 11px; color: rgba(255,255,255,0.65); } .bp-trust { display: flex; flex-direction: column; gap: 10px; position: relative; } .bp-trust-row { display: flex; align-items: center; gap: 9px; font-size: 12px; color: rgba(255,255,255,0.8); } .bp-dot { width: 6px; height: 6px; border-radius: 50%; background: #b9f5d8; flex-shrink: 0; } /* ── RIGHT PANE ── */ #right-pane { background: #fff !important; padding: 0 !important; display: flex !important; flex-direction: column !important; justify-content: flex-start !important; align-items: stretch !important; } #right-pane > div { width: 100% !important; } /* INTAKE inside right pane β€” do NOT force display, let Gradio toggle visibility */ #intake-card { background: #fff !important; padding: 40px 40px 36px !important; } #intake-card.hide, #intake-card[style*="display: none"] { display: none !important; } .intake-h { font-size: 23px; font-weight: 600; color: #1d3b2c; margin-bottom: 5px; } .intake-sub { font-size: 14px; color: #8aa899; margin-bottom: 26px; } #intake-card label > span { font-size: 12px !important; font-weight: 500 !important; color: #5d7a6b !important; font-family: 'Space Grotesk', sans-serif !important; margin-bottom: 7px !important; letter-spacing: 0 !important; text-transform: none !important; } #intake-card select, #intake-card input, #intake-card .wrap input, #intake-card [class*="dropdown"] input, #intake-card [class*="dropdown"] .single-select { background: #f5faf7 !important; border: 1.5px solid #e0ede7 !important; border-radius: 14px !important; color: #1d3b2c !important; font-family: 'Space Grotesk', sans-serif !important; font-size: 14px !important; padding: 12px 15px !important; transition: all 0.18s !important; } /* dropdown displayed value + listbox options must be dark text */ #intake-card [class*="dropdown"] *, #intake-card .token, #intake-card .single-select span { color: #1d3b2c !important; } #intake-card ul[role="listbox"], #intake-card .options { background: #fff !important; } #intake-card ul[role="listbox"] li, #intake-card .options .item { color: #1d3b2c !important; background: #fff !important; } #intake-card ul[role="listbox"] li:hover, #intake-card .options .item:hover { background: #eef7f1 !important; } #intake-card select:hover, #intake-card input:hover { border-color: #b8d8c8 !important; } #intake-card select:focus, #intake-card input:focus { border-color: #2f9e6e !important; box-shadow: 0 0 0 4px rgba(47,158,110,0.1) !important; outline: none !important; background: #fff !important; } #intake-card select option { background: #fff; color: #1d3b2c; } #start-btn button { background: #2f9e6e !important; color: #fff !important; border: none !important; border-radius: 16px !important; font-family: 'Space Grotesk', sans-serif !important; font-size: 15px !important; font-weight: 600 !important; width: 100% !important; padding: 15px !important; margin-top: 18px !important; cursor: pointer !important; transition: all 0.2s !important; } #start-btn button:hover { background: #268a5e !important; transform: translateY(-2px) !important; box-shadow: 0 8px 24px rgba(47,158,110,0.3) !important; } /* CHAT inside right pane */ #chat-card { background: #fff !important; display: flex !important; flex-direction: column !important; justify-content: flex-start !important; padding: 0 !important; } #chat-card .block.padded { padding: 0 !important; } #chat-card .html-container { padding: 0 !important; } #chat-topbar { background: #1d3b2c; padding: 18px 24px; display: flex; align-items: center; gap: 12px; } .ctb-av { width: 40px; height: 40px; border-radius: 13px; background: rgba(255,255,255,0.18); display: flex; align-items: center; justify-content: center; font-size: 20px; } .ctb-name { color: #fff; font-size: 15px; font-weight: 600; margin: 0; } .ctb-status { font-size: 12px; color: rgba(255,255,255,0.7); display: flex; align-items: center; gap: 5px; margin: 2px 0 0; } .ctb-dot { width: 7px; height: 7px; border-radius: 50%; background: #6fe3a8; display: inline-block; } #chat-card .chatbot, #chat-card [data-testid="chatbot"] { height: 520px !important; min-height: 520px !important; max-height: 520px !important; background: #f5faf7 !important; border: none !important; border-radius: 0 !important; overflow-y: auto !important; } /* kill dark message-row wrappers so only the bubble shows */ #chat-card .message-row, #chat-card .message-wrap, #chat-card .message, #chat-card [class*="message-row"], #chat-card [class*="bubble"] { background: transparent !important; border: none !important; box-shadow: none !important; } #chat-card .message.user { justify-content: flex-end !important; padding-left: 60px !important; background: transparent !important; } #chat-card .message.user > div, #chat-card .user .prose { background: #2f9e6e !important; color: #fff !important; border-radius: 20px 20px 6px 20px !important; padding: 11px 16px !important; font-size: 14px !important; line-height: 1.55 !important; border: none !important; } #chat-card .user .prose * { color: #fff !important; } #chat-card .message.bot > div, #chat-card .message.assistant > div, #chat-card .bot .prose, #chat-card .assistant .prose { background: #1d3b2c !important; color: #fff !important; border-radius: 20px 20px 20px 6px !important; padding: 13px 17px !important; font-size: 14px !important; line-height: 1.65 !important; border: none !important; } #chat-card .bot .prose *, #chat-card .assistant .prose * { color: #fff !important; } #chat-card .bot .prose a, #chat-card .assistant .prose a { color: #6fe3a8 !important; text-decoration: underline !important; } #chat-card .bot .prose strong, #chat-card .assistant .prose strong { color: #b9f5d8 !important; font-weight: 600 !important; } #input-area { border-top: 1px solid #edf4f0; padding: 16px 18px; background: #fff; display: flex; gap: 10px; align-items: flex-end; } #input-area textarea { background: #f5faf7 !important; border: 1.5px solid #e0ede7 !important; border-radius: 16px !important; color: #1d3b2c !important; font-family: 'Space Grotesk', sans-serif !important; font-size: 14px !important; resize: none !important; padding: 12px 16px !important; } #input-area textarea:focus { border-color: #2f9e6e !important; box-shadow: 0 0 0 4px rgba(47,158,110,0.1) !important; } #input-area textarea::placeholder { color: #a8c2b5 !important; } #send-btn button { background: #2f9e6e !important; color: #fff !important; border: none !important; border-radius: 14px !important; font-weight: 600 !important; font-size: 14px !important; height: 46px !important; min-width: 78px !important; cursor: pointer !important; } #send-btn button:hover { background: #268a5e !important; } #cta-row { padding: 13px 24px; background: #f5faf7; border-top: 1px solid #edf4f0; font-size: 13px; color: #5d7a6b; } #cta-row a { color: #2f9e6e; font-weight: 600; text-decoration: none; } """ FORCE_LIGHT_JS = """ function() { const url = new URL(window.location.href); if (url.searchParams.get('__theme') !== 'light') { url.searchParams.set('__theme', 'light'); window.location.replace(url.toString()); } } """ HEAD_SCRIPT = """ """ with gr.Blocks(title="Wemed AI β€” MBBS Abroad Counsellor", fill_width=True) as demo: with gr.Group(elem_id="split-card"): with gr.Row(equal_height=True, elem_id="split-row"): # LEFT β€” branding (always visible) with gr.Column(scale=1, min_width=300, elem_id="brand-pane"): gr.HTML("""
Your medical
school abroad,
matched by AI 🌍
Personalised country and college picks based on your budget, scores, and goals.
50+
NMC colleges
6
Countries
Free
Consultation
Only NMC-recognized colleges
FMGE/NEXT pass rates considered
Real human counsellors after
""") # RIGHT β€” form then chat with gr.Column(scale=2, min_width=380, elem_id="right-pane"): with gr.Group(visible=True, elem_id="intake-card") as intake_panel: gr.HTML('
Tell me about yourself
A few quick questions and I\'ll find your best options.
') with gr.Row(): budget_dd = gr.Dropdown(label="What's your budget?", choices=[f["options"] for f in INTAKE_FIELDS if f["id"] == "budget"][0], value="β‚Ή20–30 lakhs", interactive=True) neet_dd = gr.Dropdown(label="NEET score range?", choices=[f["options"] for f in INTAKE_FIELDS if f["id"] == "neet_score"][0], value="200–350", interactive=True) with gr.Row(): country_dd = gr.Dropdown(label="Any country in mind?", choices=[f["options"] for f in INTAKE_FIELDS if f["id"] == "country_pref"][0], value="No preference", interactive=True) concern_dd = gr.Dropdown(label="Biggest worry?", choices=[f["options"] for f in INTAKE_FIELDS if f["id"] == "concern"][0], value="NMC recognition & FMGE/NEXT", interactive=True) name_box = gr.Textbox(label="What's your name? (optional)", placeholder="e.g. Priya", max_lines=1) with gr.Row(elem_id="start-btn"): start_btn = gr.Button("Find my matches", variant="primary", size="lg") with gr.Group(visible=False, elem_id="chat-card") as chat_panel: gr.HTML("""
🩺

Wemed AI Counsellor

Online Β· replies instantly

""") chatbot = gr.Chatbot(label="", show_label=False, avatar_images=(None, None)) with gr.Row(elem_id="input-area"): user_input = gr.Textbox(show_label=False, container=False, placeholder="Ask me anything about MBBS abroad…", scale=8, max_lines=3) with gr.Column(scale=1, min_width=78, elem_id="send-btn"): send_btn = gr.Button("Send", variant="primary") gr.HTML('
πŸ“ž Want a real person? Book a free call with Wemed β†’
') start_btn.click(fn=swap_to_chat, inputs=None, outputs=[intake_panel, chat_panel]).then( fn=start_chat, inputs=[budget_dd, neet_dd, country_dd, concern_dd, name_box], outputs=[chatbot]) send_btn.click(fn=respond, inputs=[user_input, chatbot], outputs=[chatbot, user_input]) user_input.submit(fn=respond, inputs=[user_input, chatbot], outputs=[chatbot, user_input]) if __name__ == "__main__": demo.launch(share=False, theme=gr.themes.Base(), css=CSS, js=FORCE_LIGHT_JS, head=HEAD_SCRIPT)