DeepakDev48's picture
Feat: WeMed AI Counsellor
e313c54
Raw
History Blame Contribute Delete
22 kB
"""
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 = """
<script>
(function() {
function syncPanels() {
var intake = document.getElementById('intake-card');
var chat = document.getElementById('chat-card');
var tb = document.getElementById('chat-topbar');
if (!intake || !chat || !tb) return;
var chatActive = tb.getBoundingClientRect().height > 10
&& chat.getBoundingClientRect().height > 60;
if (chatActive) {
intake.style.setProperty('display', 'none', 'important');
}
}
function boot() {
syncPanels();
var obs = new MutationObserver(syncPanels);
obs.observe(document.body, { childList: true, subtree: true, attributes: true });
setInterval(syncPanels, 400);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
setTimeout(boot, 300);
}
})();
</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("""
<div class="bp-orb1"></div><div class="bp-orb2"></div>
<div>
<div class="bp-logo">
<div class="bp-logo-mark">🩺</div>
<div><div class="bp-logo-name">Wemed</div><div class="bp-logo-sub">AI Counsellor</div></div>
</div>
<div class="bp-title">Your medical<br>school abroad,<br>matched by AI 🌍</div>
<div class="bp-desc">Personalised country and college picks based on your budget, scores, and goals.</div>
</div>
<div>
<div class="bp-stats">
<div><div class="bp-stat-n">50+</div><div class="bp-stat-l">NMC colleges</div></div>
<div><div class="bp-stat-n">6</div><div class="bp-stat-l">Countries</div></div>
<div><div class="bp-stat-n">Free</div><div class="bp-stat-l">Consultation</div></div>
</div>
<div class="bp-trust">
<div class="bp-trust-row"><span class="bp-dot"></span>Only NMC-recognized colleges</div>
<div class="bp-trust-row"><span class="bp-dot"></span>FMGE/NEXT pass rates considered</div>
<div class="bp-trust-row"><span class="bp-dot"></span>Real human counsellors after</div>
</div>
</div>
""")
# 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('<div class="intake-h">Tell me about yourself</div><div class="intake-sub">A few quick questions and I\'ll find your best options.</div>')
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("""
<div id="chat-topbar">
<div class="ctb-av">🩺</div>
<div><p class="ctb-name">Wemed AI Counsellor</p>
<p class="ctb-status"><span class="ctb-dot"></span> Online · replies instantly</p></div>
</div>
""")
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('<div id="cta-row">📞 Want a real person? <a href="https://wemed.in" target="_blank">Book a free call with Wemed →</a></div>')
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)