mhfa-buddy / app.py
kendrickfff's picture
Update app.py
dbb06ee verified
"""
MHFA Buddy v2 — Mental Health First Aid Companion
"""
import os
import json
import gradio as gr
from datetime import datetime
from azure.ai.projects import AIProjectClient
from azure.identity import ClientSecretCredential
from azure.ai.agents.models import ListSortOrder
from supabase import create_client, Client
# ── Azure config ──────────────────────────────────────────────────
PROJECT_ENDPOINT = os.environ["PROJECT_ENDPOINT"]
AGENT_ID = os.environ["AGENT_ID"]
AZURE_TENANT_ID = os.environ["AZURE_TENANT_ID"]
AZURE_CLIENT_ID = os.environ["AZURE_CLIENT_ID"]
AZURE_CLIENT_SECRET = os.environ["AZURE_CLIENT_SECRET"]
# ── Supabase config ───────────────────────────────────────────────
SUPABASE_URL = os.environ["SUPABASE_URL"]
SUPABASE_KEY = os.environ["SUPABASE_KEY"]
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
# ── Azure client ──────────────────────────────────────────────────
credential = ClientSecretCredential(
tenant_id=AZURE_TENANT_ID,
client_id=AZURE_CLIENT_ID,
client_secret=AZURE_CLIENT_SECRET,
)
project = AIProjectClient(credential=credential, endpoint=PROJECT_ENDPOINT)
_agent = None
def get_agent():
global _agent
if _agent is None:
_agent = project.agents.get_agent(AGENT_ID)
return _agent
# ── Language Pack ─────────────────────────────────────────────────
LANG = {
"id": {
"subtitle": "Teman Pertolongan Pertama Kesehatan Mental",
"welcome_title": "Halo, aku MHFA Buddy 🤍<br>Teman pertolongan pertama kesehatan mentalmu",
"welcome_p1": "Kamu berada di ruang yang aman. Boleh cerita apa pun — tanpa dihakimi, tanpa harus terlihat kuat.",
"welcome_p2": "Aku akan mendengarkan dan menemani kamu menemukan cara yang terasa paling ringan. Aku bukan terapis, tapi aku akan berusaha jadi <em>safe space</em> buatmu. 🌿",
"tags": ["💬 Dukungan Emosional", "📚 Psikoedukasi", "🧘 Teknik Coping", "🌙 Self-Care", "🤝 Bantu Orang Lain"],
"disclaimer": '⚠️ <b>MHFA Buddy bukan terapis atau dokter.</b> Ini alat bantu edukasi dan dukungan awal.',
"crisis_label": "Darurat",
"typing": "💙 Sedang mengetik...",
"btn_send": "Kirim 💙",
"btn_new": "🔄 Percakapan Baru",
"btn_logout": "🚪 Keluar",
"input_placeholder": "Ceritakan perasaanmu di sini...",
"error_empty": "⚠️ Email dan password wajib diisi.",
"error_short_pw": "⚠️ Password minimal 6 karakter.",
"error_confirm": "📧 Link konfirmasi telah dikirim ke emailmu. Cek inbox (dan spam) lalu klik link untuk mengaktifkan akun.",
"error_exists": "⚠️ Email sudah terdaftar. Silakan masuk.",
"error_not_confirmed": "⚠️ Email belum dikonfirmasi. Cek inbox untuk link aktivasi.",
"error_wrong": "⚠️ Email atau password salah.",
"error_failed": "⚠️ Maaf, terjadi kesalahan. Coba lagi atau mulai percakapan baru.",
"error_no_response": "🤔 Tidak ada respons. Coba lagi ya.",
"error_connection": "❌ Terjadi gangguan koneksi. Silakan coba lagi.\n\nJika kamu dalam kondisi darurat:\n🇮🇩 119 ext 8 (Sejiwa/Kemenkes)\n🌍 Text HOME to 741741 (Crisis Text Line)",
"login_success": "💙 Hai, **{email}**! Percakapanmu tersimpan.",
"signup_success": "💙 Selamat datang, **{email}**! Akunmu sudah aktif.",
"footer": 'Dibuat oleh <strong>Kendrick Filbert</strong> · Powered by Azure AI Foundry<br>Sumber: Mental Health First Aid International · Kemenkes RI',
"examples": [
["Aku merasa cemas dan nggak tahu kenapa"],
["Akhir-akhir ini aku merasa kewalahan di kampus"],
["Apa itu anxiety? Jelaskan dengan sederhana"],
["Bisa ajari aku teknik grounding?"],
["Bagaimana cara menolong teman yang terlihat depresi?"],
["Aku susah tidur akhir-akhir ini"],
["Apa perbedaan antara sedih biasa dan depresi?"],
],
},
"en": {
"subtitle": "Your Mental Health First Aid Companion",
"welcome_title": "Hi, I'm MHFA Buddy 🤍<br>Your Mental Health First Aid Companion",
"welcome_p1": "You're in a safe space. Feel free to share anything — without judgement, without having to be strong.",
"welcome_p2": "I'll listen and help you find what feels lightest. I'm not a therapist, but I'll do my best to be a <em>safe space</em> for you. 🌿",
"tags": ["💬 Emotional Support", "📚 Psychoeducation", "🧘 Coping Skills", "🌙 Self-Care", "🤝 Help Others"],
"disclaimer": '⚠️ <b>MHFA Buddy is not a therapist or doctor.</b> This is an educational and initial support tool.',
"crisis_label": "Emergency",
"typing": "💙 Typing...",
"btn_send": "Send 💙",
"btn_new": "🔄 New Conversation",
"btn_logout": "🚪 Sign Out",
"input_placeholder": "Share what you're feeling...",
"error_empty": "⚠️ Email and password are required.",
"error_short_pw": "⚠️ Password must be at least 6 characters.",
"error_confirm": "📧 Confirmation link sent to your email. Check your inbox (and spam) then click the link to activate.",
"error_exists": "⚠️ Email already registered. Please sign in.",
"error_not_confirmed": "⚠️ Email not confirmed yet. Check your inbox for the activation link.",
"error_wrong": "⚠️ Incorrect email or password.",
"error_failed": "⚠️ Sorry, something went wrong. Please try again or start a new conversation.",
"error_no_response": "🤔 No response received. Please try again.",
"error_connection": "❌ Connection error. Please try again.\n\nIf you're in crisis:\n🇮🇩 119 ext 8 (Sejiwa/Kemenkes)\n🌍 Text HOME to 741741 (Crisis Text Line)",
"login_success": "💙 Hi, **{email}**! Your conversations are saved.",
"signup_success": "💙 Welcome, **{email}**! Your account is active.",
"footer": 'Built by <strong>Kendrick Filbert</strong> · Powered by Azure AI Foundry<br>Source: Mental Health First Aid International · Kemenkes RI',
"examples": [
["I've been feeling anxious and I don't know why"],
["I've been overwhelmed at school lately"],
["What is anxiety? Explain it simply"],
["Can you teach me a grounding technique?"],
["How do I help a friend who seems depressed?"],
["I've been having trouble sleeping"],
["What's the difference between sadness and depression?"],
],
},
}
def get_t(lang: str, key: str) -> str:
return LANG.get(lang, LANG["id"]).get(key, "")
# ── Supabase Auth ─────────────────────────────────────────────────
def do_signup(email: str, password: str, lang: str = "id"):
email = (email or "").strip().lower()
password = (password or "").strip()
if not email or not password:
return None, get_t(lang, "error_empty")
if len(password) < 6:
return None, get_t(lang, "error_short_pw")
try:
res = supabase.auth.sign_up({"email": email, "password": password})
if res.user:
if res.user.email_confirmed_at is None:
return None, get_t(lang, "error_confirm")
return res.user, None
return None, get_t(lang, "error_failed")
except Exception as e:
err = str(e).lower()
if "already registered" in err or "already been registered" in err:
return None, get_t(lang, "error_exists")
return None, f"⚠️ Error: {str(e)}"
def do_login(email: str, password: str, lang: str = "id"):
email = (email or "").strip().lower()
password = (password or "").strip()
if not email or not password:
return None, get_t(lang, "error_empty")
try:
res = supabase.auth.sign_in_with_password({"email": email, "password": password})
if res.user:
return res.user, None
return None, get_t(lang, "error_wrong")
except Exception as e:
err = str(e).lower()
if "email not confirmed" in err:
return None, get_t(lang, "error_not_confirmed")
if "invalid" in err or "credentials" in err:
return None, get_t(lang, "error_wrong")
return None, f"⚠️ Error: {str(e)}"
# ── Memory: Supabase table ────────────────────────────────────────
def get_user_thread(email: str) -> str | None:
try:
res = supabase.table("user_threads").select("thread_id").eq("user_email", email).execute()
if res.data and len(res.data) > 0:
return res.data[0]["thread_id"]
except Exception:
pass
return None
def save_user_thread(email: str, thread_id: str):
try:
supabase.table("user_threads").upsert({
"user_email": email,
"thread_id": thread_id,
"updated_at": datetime.now().isoformat(),
}).execute()
except Exception as e:
print(f"[Memory save error] {e}")
# ── Load chat history from Azure thread ───────────────────────────
def load_history_from_thread(thread_id: str) -> list:
try:
messages = project.agents.messages.list(
thread_id=thread_id, order=ListSortOrder.ASCENDING
)
history = []
for msg in messages:
if msg.text_messages:
content = msg.text_messages[-1].text.value
history.append({"role": msg.role, "content": content})
return history
except Exception:
return []
# ── Chat logic (generator for typing indicator) ──────────────────
def respond(user_message: str, history: list, thread_state: dict | None, auth_state: dict | None, lang: str):
if not user_message.strip():
yield history, thread_state
return
user_email = auth_state.get("email") if auth_state else None
if thread_state is None or "thread_id" not in thread_state:
if user_email:
existing_thread = get_user_thread(user_email)
if existing_thread:
thread_state = {"thread_id": existing_thread}
else:
thread = project.agents.threads.create()
thread_state = {"thread_id": thread.id}
save_user_thread(user_email, thread.id)
else:
thread = project.agents.threads.create()
thread_state = {"thread_id": thread.id}
thread_id = thread_state["thread_id"]
history.append({"role": "user", "content": user_message})
history.append({"role": "assistant", "content": get_t(lang, "typing")})
yield history, thread_state
try:
agent = get_agent()
project.agents.messages.create(
thread_id=thread_id,
role="user",
content=user_message,
)
run = project.agents.runs.create_and_process(
thread_id=thread_id,
agent_id=agent.id,
)
if run.status == "failed":
assistant_reply = get_t(lang, "error_failed")
else:
messages = project.agents.messages.list(
thread_id=thread_id, order=ListSortOrder.DESCENDING
)
assistant_reply = get_t(lang, "error_no_response")
for msg in messages:
if msg.role == "assistant" and msg.text_messages:
assistant_reply = msg.text_messages[-1].text.value
break
except Exception:
assistant_reply = get_t(lang, "error_connection")
history[-1] = {"role": "assistant", "content": assistant_reply}
yield history, thread_state
def new_conversation(auth_state: dict | None):
thread = project.agents.threads.create()
new_state = {"thread_id": thread.id}
if auth_state and auth_state.get("email"):
save_user_thread(auth_state["email"], thread.id)
return [], new_state
# ── Auth UI handlers ──────────────────────────────────────────────
def handle_login_id(email, password, history, thread_state):
return _handle_login(email, password, history, thread_state, "id")
def handle_login_en(email, password, history, thread_state):
return _handle_login(email, password, history, thread_state, "en")
def _handle_login(email, password, history, thread_state, lang):
user, error = do_login(email, password, lang)
if error:
return (
gr.update(visible=True), gr.update(visible=True), # auth sections stay
gr.update(visible=False), # logged_in hidden
gr.update(visible=False), # logout hidden
error, error, # both status fields
None, history, thread_state,
)
auth_state = {"email": user.email, "id": user.id}
existing_thread = get_user_thread(user.email)
if existing_thread:
history = load_history_from_thread(existing_thread)
thread_state = {"thread_id": existing_thread}
msg = get_t(lang, "login_success").format(email=user.email)
return (
gr.update(visible=False), gr.update(visible=False), # hide both auth
gr.update(visible=True, value=msg), # show logged_in
gr.update(visible=True), # show logout
"", "", # clear status
auth_state, history, thread_state,
)
def handle_signup_id(email, password, history, thread_state):
return _handle_signup(email, password, history, thread_state, "id")
def handle_signup_en(email, password, history, thread_state):
return _handle_signup(email, password, history, thread_state, "en")
def _handle_signup(email, password, history, thread_state, lang):
user, error = do_signup(email, password, lang)
if error:
return (
gr.update(visible=True), gr.update(visible=True),
gr.update(visible=False),
gr.update(visible=False),
error, error,
None, history, thread_state,
)
auth_state = {"email": user.email, "id": user.id}
msg = get_t(lang, "signup_success").format(email=user.email)
return (
gr.update(visible=False), gr.update(visible=False),
gr.update(visible=True, value=msg),
gr.update(visible=True),
"", "",
auth_state, history, thread_state,
)
def handle_logout():
try:
supabase.auth.sign_out()
except Exception:
pass
return (
gr.update(visible=True), gr.update(visible=False), # show ID auth, hide EN auth (reset to ID)
gr.update(visible=False), # hide logged_in
gr.update(visible=False), # hide logout
"", "", # clear status
None, [], None,
)
# ── Language toggle handler ───────────────────────────────────────
def build_header_html(lang):
t = LANG[lang]
id_active = "lang-active" if lang == "id" else "lang-inactive"
en_active = "lang-active" if lang == "en" else "lang-inactive"
return f"""
<div class="header-section">
<div class="lang-toggle">
<button class="lang-pill {id_active}" onclick="document.querySelector('#lang-id-btn').click()">🇮🇩 ID</button>
<button class="lang-pill {en_active}" onclick="document.querySelector('#lang-en-btn').click()">🇬🇧 EN</button>
</div>
<h1>💙 MHFA Buddy</h1>
<p class="subtitle">{t["subtitle"]}</p>
</div>
"""
def switch_to_id():
return _switch_language("id")
def switch_to_en():
return _switch_language("en")
def _switch_language(lang):
t = LANG[lang]
tags_html = " ".join([f'<span>{tag}</span>' for tag in t["tags"]])
header = build_header_html(lang)
welcome = f"""
<div class="welcome-card">
<h2>{t["welcome_title"]}</h2>
<p>{t["welcome_p1"]}</p>
<p>{t["welcome_p2"]}</p>
<div class="welcome-tags">{tags_html}</div>
</div>
"""
disclaimer = f"""
<div class="info-strip">
<div class="disclaimer-bar">{t["disclaimer"]}</div>
<div class="crisis-bar">
🆘 <b>{t["crisis_label"]}:</b>&ensp;
🇮🇩 <a href="tel:119">119 ext 8</a> (Sejiwa) ·
LISA: <a href="https://wa.me/628113855472">+62 811-3855-472</a> (ID) /
<a href="https://wa.me/628113815472">+62 811-3815-472</a> (EN) ·
🌍 <a href="sms:741741&body=HOME">741741</a> ·
<a href="tel:112">112</a>
</div>
</div>
"""
footer = f'<div class="footer-info">{t["footer"]}</div>'
show_id = lang == "id"
show_en = lang == "en"
return (
header, # header_html
welcome, # welcome_html
disclaimer, # disclaimer_html
footer, # footer_html
gr.update(placeholder=t["input_placeholder"]), # msg_input
gr.update(value=t["btn_send"]), # send_btn
gr.update(value=t["btn_new"]), # clear_btn
gr.update(value=t["btn_logout"]), # logout_btn
gr.update(visible=show_id), # auth_section_id
gr.update(visible=show_en), # auth_section_en
gr.update(visible=show_id), # examples_id
gr.update(visible=show_en), # examples_en
lang, # lang_state
)
# ── CSS ───────────────────────────────────────────────────────────
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
* {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
}
.gradio-container {
max-width: 780px !important;
margin: 0 auto !important;
background: #FAFBFC !important;
}
footer { display: none !important; }
/* Header */
.header-section {
text-align: center;
padding: 20px 16px 4px 16px;
position: relative;
}
.header-section h1 {
font-size: 1.6rem;
font-weight: 700;
margin: 0 0 2px 0;
color: #5B8FB9;
letter-spacing: -0.02em;
}
.header-section .subtitle {
font-size: 0.82rem;
color: #8DA4B8;
margin: 0;
font-weight: 400;
}
/* Language toggle pill */
.lang-toggle {
position: absolute;
top: 16px;
right: 16px;
display: inline-flex;
background: #EDF2F7;
border-radius: 20px;
padding: 3px;
gap: 2px;
}
.lang-pill {
border: none;
padding: 5px 12px;
border-radius: 18px;
font-size: 0.72rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
line-height: 1;
}
.lang-active {
background: white;
color: #2D3748;
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
}
.lang-inactive {
background: transparent;
color: #A0AEC0;
}
.lang-inactive:hover {
color: #718096;
}
/* Welcome card */
.welcome-card {
background: linear-gradient(145deg, #F0F7FF 0%, #F7FBFF 40%, #FFF9F5 100%);
border: 1px solid #D6E8F5;
border-radius: 18px;
padding: 22px 26px 18px 26px;
margin: 10px 0 10px 0;
text-align: center;
}
.welcome-card h2 {
font-size: 1rem;
font-weight: 600;
color: #3D5A73;
margin: 0 0 8px 0;
line-height: 1.45;
}
.welcome-card p {
font-size: 0.82rem;
color: #5A7A8F;
margin: 0 0 6px 0;
line-height: 1.6;
}
.welcome-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
justify-content: center;
margin-top: 12px;
}
.welcome-tags span {
background: white;
border: 1px solid #E2ECF2;
border-radius: 20px;
padding: 4px 11px;
font-size: 0.7rem;
color: #5A7A8F;
font-weight: 500;
}
/* Info strip */
.info-strip {
display: flex;
flex-direction: column;
gap: 6px;
margin: 6px 0 10px 0;
}
.disclaimer-bar {
text-align: center;
font-size: 0.72rem;
padding: 7px 14px;
background: #FFFCF0;
border: 1px solid #F5E6B8;
border-radius: 10px;
color: #8A7430;
line-height: 1.4;
}
.crisis-bar {
text-align: center;
font-size: 0.72rem;
padding: 7px 14px;
background: #FFF5F5;
border: 1px solid #FECACA;
border-radius: 10px;
color: #9B2C2C;
line-height: 1.5;
}
.crisis-bar a {
color: #C53030;
font-weight: 600;
text-decoration: none;
}
.crisis-bar a:hover {
text-decoration: underline;
}
/* Hidden buttons */
.hidden-btn {
display: none !important;
}
/* Footer */
.footer-info {
text-align: center;
padding: 10px 0 6px 0;
font-size: 0.68rem;
color: #A0B0BC;
line-height: 1.5;
}
"""
# ── UI ────────────────────────────────────────────────────────────
with gr.Blocks(title="MHFA Buddy — Mental Health First Aid Companion") as demo:
# States
auth_state = gr.State(value=None)
thread_state = gr.State(value=None)
lang_state = gr.State(value="id")
# Hidden buttons for language toggle
lang_id_btn = gr.Button("ID", elem_id="lang-id-btn", elem_classes="hidden-btn")
lang_en_btn = gr.Button("EN", elem_id="lang-en-btn", elem_classes="hidden-btn")
# Header (dynamic)
header_html = gr.HTML(build_header_html("id"))
# Welcome card (dynamic)
welcome_html = gr.HTML("""
<div class="welcome-card">
<h2>Halo, aku MHFA Buddy 🤍<br>Teman pertolongan pertama kesehatan mentalmu</h2>
<p>Kamu berada di ruang yang aman. Boleh cerita apa pun — tanpa dihakimi, tanpa harus terlihat kuat.</p>
<p>Aku akan mendengarkan dan menemani kamu menemukan cara yang terasa paling ringan. Aku bukan terapis, tapi aku akan berusaha jadi <em>safe space</em> buatmu. 🌿</p>
<div class="welcome-tags">
<span>💬 Dukungan Emosional</span>
<span>📚 Psikoedukasi</span>
<span>🧘 Teknik Coping</span>
<span>🌙 Self-Care</span>
<span>🤝 Bantu Orang Lain</span>
</div>
</div>
""")
# ══ Auth Section — INDONESIAN ═════════════════════════════════
with gr.Group(visible=True) as auth_section_id:
with gr.Accordion("🔒 Masuk / Daftar — simpan percakapanmu", open=False):
gr.HTML("""
<div style="text-align:center; padding:4px 0 8px 0;">
<span style="font-size:0.8rem; color:#8DA4B8;">
Tanpa akun juga tetap bisa dipakai. Masuk untuk menyimpan riwayat chat.
</span>
</div>
""")
with gr.Tab("Masuk"):
login_email_id = gr.Textbox(placeholder="Alamat email", label="Email", type="email")
login_password_id = gr.Textbox(placeholder="Password", label="Password", type="password")
login_btn_id = gr.Button("Masuk 💙", variant="primary", size="sm")
login_status_id = gr.Markdown("")
with gr.Tab("Daftar Baru"):
signup_email_id = gr.Textbox(placeholder="Alamat email", label="Email", type="email")
signup_password_id = gr.Textbox(placeholder="Buat password (min. 6 karakter)", label="Password", type="password")
signup_btn_id = gr.Button("Daftar ✨", variant="primary", size="sm")
signup_status_id = gr.Markdown("")
# ══ Auth Section — ENGLISH ════════════════════════════════════
with gr.Group(visible=False) as auth_section_en:
with gr.Accordion("🔒 Sign In / Sign Up — save your conversations", open=False):
gr.HTML("""
<div style="text-align:center; padding:4px 0 8px 0;">
<span style="font-size:0.8rem; color:#8DA4B8;">
You can use MHFA Buddy without an account. Sign in to save your chat history.
</span>
</div>
""")
with gr.Tab("Sign In"):
login_email_en = gr.Textbox(placeholder="Email address", label="Email", type="email")
login_password_en = gr.Textbox(placeholder="Password", label="Password", type="password")
login_btn_en = gr.Button("Sign In 💙", variant="primary", size="sm")
login_status_en = gr.Markdown("")
with gr.Tab("Sign Up"):
signup_email_en = gr.Textbox(placeholder="Email address", label="Email", type="email")
signup_password_en = gr.Textbox(placeholder="Create password (min. 6 characters)", label="Password", type="password")
signup_btn_en = gr.Button("Sign Up ✨", variant="primary", size="sm")
signup_status_en = gr.Markdown("")
# Logged-in indicator
logged_in_msg = gr.Markdown("", visible=False)
logout_btn = gr.Button("🚪 Keluar", variant="secondary", size="sm", visible=False)
# Disclaimer + Crisis (dynamic)
disclaimer_html = gr.HTML("""
<div class="info-strip">
<div class="disclaimer-bar">
⚠️ <b>MHFA Buddy bukan terapis atau dokter.</b> Ini alat bantu edukasi dan dukungan awal.
</div>
<div class="crisis-bar">
🆘 <b>Darurat:</b>&ensp;
🇮🇩 <a href="tel:119">119 ext 8</a> (Sejiwa) ·
LISA: <a href="https://wa.me/628113855472">+62 811-3855-472</a> (ID) /
<a href="https://wa.me/628113815472">+62 811-3815-472</a> (EN) ·
🌍 <a href="sms:741741&body=HOME">741741</a> ·
<a href="tel:112">112</a>
</div>
</div>
""")
# Chat
chatbot = gr.Chatbot(label="MHFA Buddy", height=400)
# Input
with gr.Row():
msg_input = gr.Textbox(
placeholder="Ceritakan perasaanmu di sini...",
label="", show_label=False, scale=5, lines=1, max_lines=3, container=False,
)
send_btn = gr.Button("Kirim 💙", variant="primary", scale=1, min_width=100)
with gr.Row():
clear_btn = gr.Button("🔄 Percakapan Baru", variant="secondary", size="sm")
# ══ Examples — INDONESIAN ═════════════════════════════════════
with gr.Accordion("💡 Contoh pertanyaan", open=False, visible=True) as examples_id:
gr.Examples(examples=LANG["id"]["examples"], inputs=msg_input, label="")
# ══ Examples — ENGLISH ════════════════════════════════════════
with gr.Accordion("💡 Example prompts", open=False, visible=False) as examples_en:
gr.Examples(examples=LANG["en"]["examples"], inputs=msg_input, label="")
# Footer (dynamic)
footer_html = gr.HTML("""
<div class="footer-info">
Dibuat oleh <strong>Kendrick Filbert</strong> · Powered by Azure AI Foundry<br>
Sumber: Mental Health First Aid International · Kemenkes RI
</div>
""")
# ── Events ────────────────────────────────────────────────────
# Auth outputs (shared across both ID/EN login/signup)
auth_outputs = [
auth_section_id, auth_section_en,
logged_in_msg, logout_btn,
login_status_id, login_status_en,
auth_state, chatbot, thread_state,
]
# Language toggle
lang_outputs = [
header_html, welcome_html, disclaimer_html, footer_html,
msg_input, send_btn, clear_btn, logout_btn,
auth_section_id, auth_section_en,
examples_id, examples_en,
lang_state,
]
lang_id_btn.click(fn=switch_to_id, outputs=lang_outputs)
lang_en_btn.click(fn=switch_to_en, outputs=lang_outputs)
# Login — ID
login_btn_id.click(
fn=handle_login_id,
inputs=[login_email_id, login_password_id, chatbot, thread_state],
outputs=auth_outputs,
)
# Login — EN
login_btn_en.click(
fn=handle_login_en,
inputs=[login_email_en, login_password_en, chatbot, thread_state],
outputs=auth_outputs,
)
# Signup — ID
signup_btn_id.click(
fn=handle_signup_id,
inputs=[signup_email_id, signup_password_id, chatbot, thread_state],
outputs=auth_outputs,
)
# Signup — EN
signup_btn_en.click(
fn=handle_signup_en,
inputs=[signup_email_en, signup_password_en, chatbot, thread_state],
outputs=auth_outputs,
)
# Logout
logout_btn.click(
fn=handle_logout,
outputs=auth_outputs,
)
# Send message
send_btn.click(
fn=respond,
inputs=[msg_input, chatbot, thread_state, auth_state, lang_state],
outputs=[chatbot, thread_state],
).then(lambda: "", outputs=msg_input)
msg_input.submit(
fn=respond,
inputs=[msg_input, chatbot, thread_state, auth_state, lang_state],
outputs=[chatbot, thread_state],
).then(lambda: "", outputs=msg_input)
# New conversation
clear_btn.click(
fn=new_conversation,
inputs=[auth_state],
outputs=[chatbot, thread_state],
)
if __name__ == "__main__":
demo.launch(
server_name="0.0.0.0",
server_port=7860,
css=CSS,
theme=gr.themes.Soft(),
ssr_mode=False,
)