"""Saathi — a multilingual mental-health companion for India. This is the Streamlit entry point. It wires together: - Language selector (7 Indian languages, Claude is natively multilingual) - One-time consent acknowledgment gate (ethical alignment) - Persistent "not a professional" banner across every tab - 5 feature tabs (Chat, Legal Aid, Thought Diary, My Patterns, Soothe Corner) - Session-only sidebar conversations, similar to ChatGPT's left rail - "What happens to my data?" privacy explainer in sidebar - Footer with privacy disclaimer Zero persistence: everything lives in st.session_state. Close the browser, it's gone. """ from __future__ import annotations import copy from datetime import datetime import streamlit as st from backend.claude_client import get_active_provider_label from backend.i18n import DEFAULT_LANGUAGE, LANGUAGES, native_label, t from modules import ( cognitive_journal, legal_aid, my_patterns, saathi_chat, soothe_poetry, ) st.set_page_config( page_title="Saathi — Mental Health Companion", page_icon="🫂", layout="wide", initial_sidebar_state="expanded", ) LANGUAGE_KEY = "saathi_language" SESSIONS_KEY = "saathi_sessions" ACTIVE_SESSION_KEY = "saathi_active_session" SESSION_COUNTER_KEY = "saathi_session_counter" CONSENT_KEY = "saathi_consent_given" SESSION_STATE_KEYS = [ # Saathi Chat "saathi_chat_history", "saathi_chat_city", # Legal Aid "legal_aid_situation", "legal_aid_category", "legal_aid_sections", "legal_aid_response", "legal_aid_letter", # Student Mode (now in Chat) "saathi_chat_student_mode", "saathi_chat_student_event", # My Patterns "my_patterns_songs", "my_patterns_activities", # Thought Diary "cognitive_journal_entries", "cognitive_journal_last", "cognitive_journal_section", "cognitive_journal_widget_version", "cognitive_journal_notice", "cognitive_journal_phq9_score", "cognitive_journal_phq9_answers", "cognitive_journal_phq9_taken_at", "cognitive_journal_phq9_history", "cognitive_journal_phq9_item9_positive", "cognitive_journal_phq9_widget_version", "cognitive_journal_gad7_score", "cognitive_journal_gad7_answers", "cognitive_journal_gad7_taken_at", "cognitive_journal_gad7_history", "cognitive_journal_gad7_widget_version", "cognitive_journal_checkins", "cognitive_journal_legacy_cleanup_done", # Soothe Corner "soothe_feeling", "soothe_poem", "soothe_songs", ] SESSION_WIDGET_KEYS_TO_CLEAR = [ "saathi_chat_city_input", "legal_aid_situation_input", "saathi_chat_student_event_radio", "cognitive_journal_input", "cognitive_journal_checkin_mood", "cognitive_journal_checkin_sleep", "cognitive_journal_checkin_stress", "soothe_feeling_input", ] SESSION_WIDGET_KEY_PREFIXES_TO_CLEAR = [ "cognitive_journal_input_v", "cognitive_journal_phq9_v", "cognitive_journal_gad7_v", ] _CUSTOM_CSS = """ """ def _init_language() -> None: if LANGUAGE_KEY not in st.session_state: st.session_state[LANGUAGE_KEY] = DEFAULT_LANGUAGE def _new_session_title(index: int) -> str: return f"Session {index}" def _init_sessions() -> None: if SESSION_COUNTER_KEY not in st.session_state: st.session_state[SESSION_COUNTER_KEY] = 1 if SESSIONS_KEY not in st.session_state: st.session_state[SESSIONS_KEY] = { "session_1": { "title": _new_session_title(1), "created_at": datetime.now().isoformat(timespec="minutes"), "state": {}, } } if ACTIVE_SESSION_KEY not in st.session_state: st.session_state[ACTIVE_SESSION_KEY] = "session_1" def _current_session_id() -> str: _init_sessions() active_id = st.session_state[ACTIVE_SESSION_KEY] sessions = st.session_state[SESSIONS_KEY] if active_id not in sessions: active_id = next(iter(sessions)) st.session_state[ACTIVE_SESSION_KEY] = active_id return active_id def _save_active_session() -> None: active_id = _current_session_id() state = {} for key in SESSION_STATE_KEYS: if key in st.session_state: state[key] = copy.deepcopy(st.session_state[key]) st.session_state[SESSIONS_KEY][active_id]["state"] = state def _restore_session(session_id: str) -> None: sessions = st.session_state[SESSIONS_KEY] if session_id not in sessions: return restored = sessions[session_id].get("state", {}) for key in SESSION_STATE_KEYS: if key in restored: st.session_state[key] = copy.deepcopy(restored[key]) elif key in st.session_state: del st.session_state[key] for key in SESSION_WIDGET_KEYS_TO_CLEAR: if key in st.session_state: del st.session_state[key] for key in list(st.session_state.keys()): if any(key.startswith(prefix) for prefix in SESSION_WIDGET_KEY_PREFIXES_TO_CLEAR): del st.session_state[key] st.session_state[ACTIVE_SESSION_KEY] = session_id def _create_session() -> None: _save_active_session() st.session_state[SESSION_COUNTER_KEY] += 1 idx = st.session_state[SESSION_COUNTER_KEY] session_id = f"session_{idx}" st.session_state[SESSIONS_KEY][session_id] = { "title": _new_session_title(idx), "created_at": datetime.now().isoformat(timespec="minutes"), "state": {}, } _restore_session(session_id) def _switch_session(session_id: str) -> None: if session_id == st.session_state[ACTIVE_SESSION_KEY]: return _save_active_session() _restore_session(session_id) def _render_session_sidebar(lang: str) -> None: _init_sessions() _save_active_session() with st.sidebar: st.markdown(f"### {t('sessions_heading', lang)}") st.caption(t("sessions_sub", lang)) if st.button(t("sessions_new_button", lang), key="saathi_new_session_button", use_container_width=True): _create_session() st.rerun() st.divider() active_id = st.session_state[ACTIVE_SESSION_KEY] for session_id, meta in st.session_state[SESSIONS_KEY].items(): title = meta.get("title", session_id) label = f"{'● ' if session_id == active_id else ''}{title}" if st.button( label, key=f"saathi_session_button_{session_id}", disabled=session_id == active_id, use_container_width=True, ): _switch_session(session_id) st.rerun() st.caption(t("sessions_zero_persistence", lang)) # --- "What happens to my data?" privacy explainer --- st.divider() with st.expander(t("data_privacy_heading", lang)): st.markdown(t("data_privacy_body", lang)) def _language_selector() -> str: codes = list(LANGUAGES.keys()) labels = [native_label(c) for c in codes] current = st.session_state[LANGUAGE_KEY] idx = codes.index(current) if current in codes else 0 label = st.selectbox( t("language_label", current), options=labels, index=idx, key="saathi_language_select", ) new_code = codes[labels.index(label)] if new_code != st.session_state[LANGUAGE_KEY]: st.session_state[LANGUAGE_KEY] = new_code st.rerun() return st.session_state[LANGUAGE_KEY] def _header(lang: str) -> None: """Render the gradient wordmark band + language selector.""" cols = st.columns([5, 1]) with cols[0]: title_en = t("app_title", "en") title_local = t("app_title", lang) tagline = t("tagline", lang) # Always show the Devanagari wordmark alongside the English one so the # brand reads cross-lingually — even when the UI is set to Tamil or Telugu, # "Saathi / साथी" is instantly recognisable. wordmark_suffix = "" if title_en != title_local: wordmark_suffix = f"· {title_local}" elif lang == "en": wordmark_suffix = "· साथी" st.markdown( f"""

🫂 {title_en} {wordmark_suffix}

{tagline}

""", unsafe_allow_html=True, ) with cols[1]: _language_selector() def _banners(lang: str) -> None: st.info(t("not_therapy_banner", lang)) def _consent_gate(lang: str) -> bool: """Display a one-time consent acknowledgment before the main UI. Returns True if the user has already consented in this session. """ if st.session_state.get(CONSENT_KEY): return True st.markdown(_CUSTOM_CSS, unsafe_allow_html=True) # Show a minimal header even on the consent screen _header(lang) st.markdown( f""" """, unsafe_allow_html=True, ) col1, col2, col3 = st.columns([1, 2, 1]) with col2: if st.button( t("consent_button", lang), key="saathi_consent_button", use_container_width=True, type="primary", ): st.session_state[CONSENT_KEY] = True st.rerun() return False def _footer(lang: str) -> None: st.divider() cols = st.columns([3, 1]) with cols[0]: st.caption(t("footer_disclaimer", lang)) with cols[1]: provider_label = get_active_provider_label() st.caption( f"_{provider_label} · Built for Anthropic × IIT Delhi 2026 · " "[source](https://github.com/samarth2018/Hackathons) · MIT licensed_" ) def main() -> None: _init_language() lang = st.session_state[LANGUAGE_KEY] # --- Consent gate (one-time per session) --- if not _consent_gate(lang): return # Consent not yet given; stop here # --- Main UI (only after consent) --- st.markdown(_CUSTOM_CSS, unsafe_allow_html=True) _render_session_sidebar(lang) _header(lang) _banners(lang) tab_chat, tab_legal, tab_journal, tab_patterns, tab_soothe = st.tabs( [ t("tab_chat", lang), t("tab_legal", lang), t("tab_journal", lang), t("tab_patterns", lang), t("tab_soothe", lang), ] ) with tab_chat: saathi_chat.render(lang) with tab_legal: legal_aid.render(lang) with tab_journal: cognitive_journal.render(lang) with tab_patterns: my_patterns.render(lang) with tab_soothe: soothe_poetry.render(lang) _footer(lang) if __name__ == "__main__": main()