Spaces:
Sleeping
Sleeping
| """ | |
| 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>  | |
| 🇮🇩 <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>  | |
| 🇮🇩 <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, | |
| ) |