diff --git a/TalkHeal.py b/TalkHeal.py index cc28d6e..1d196bd 100644 --- a/TalkHeal.py +++ b/TalkHeal.py @@ -4,16 +4,42 @@ st.set_page_config(page_title="TalkHeal", page_icon="💬", layout="wide") + +# --- AUTHENTICATION AND SESSION INIT --- +if "conversations" not in st.session_state: + st.session_state.conversations = [] +if "active_conversation" not in st.session_state: + st.session_state.active_conversation = None +if "db_initialized" not in st.session_state: + init_db() + st.session_state["db_initialized"] = True + # --- DB Initialization --- if "db_initialized" not in st.session_state: init_db() st.session_state["db_initialized"] = True # --- Auth State Initialization --- + if "authenticated" not in st.session_state: st.session_state.authenticated = False if "show_signup" not in st.session_state: st.session_state.show_signup = False +if "anonymous_mode" not in st.session_state: + st.session_state.anonymous_mode = False + + +# --- LOGIN/SIGNUP UI FUNCTIONS --- +def show_login_ui(): + st.subheader("🔐 Login") + email = st.text_input("Email", key="login_email") + password = st.text_input("Password", type="password", key="login_password") + if st.button("Login"): + success, user = authenticate_user(email, password) + if success: + st.session_state.authenticated = True + st.session_state.user_email = user["email"] + st.session_state.user_name = user["name"] # --- LOGIN PAGE --- if not st.session_state.authenticated: @@ -30,6 +56,7 @@ if st.button("🌙" if is_dark else "☀️", key="top_theme_toggle", help="Toggle Light/Dark Mode", use_container_width=True): st.session_state.dark_mode = not is_dark st.session_state.theme_changed = True + st.rerun() with col_logout: if st.button("Logout", key="logout_btn", use_container_width=True): @@ -44,6 +71,14 @@ st.title(f"Welcome to TalkHeal, {st.session_state.user_name}! 💬") st.markdown("Navigate to other pages from the sidebar.") + if st.button("Logout"): + for key in ["authenticated", "user_email", "user_name", "show_signup"]: + if key in st.session_state: + del st.session_state[key] + st.rerun() + +# --- IMPORTS --- + import google.generativeai as genai from core.utils import save_conversations, load_conversations from core.config import configure_gemini, PAGE_CONFIG @@ -55,11 +90,15 @@ from components.emergency_page import render_emergency_page from components.profile import apply_global_font_size + +# --- SESSION INITIALIZATION --- + # --- 1. INITIALIZE SESSION STATE --- + if "chat_history" not in st.session_state: st.session_state.chat_history = [] if "conversations" not in st.session_state: - st.session_state.conversations = load_conversations() + st.session_state.conversations = [] if st.session_state.anonymous_mode else load_conversations() if "active_conversation" not in st.session_state: st.session_state.active_conversation = -1 if "show_emergency_page" not in st.session_state: @@ -76,14 +115,16 @@ if "selected_tone" not in st.session_state: st.session_state.selected_tone = "Compassionate Listener" -# --- 2. SET PAGE CONFIG --- +# --- CONFIG --- apply_global_font_size() + # --- 3. APPLY STYLES & CONFIGURATIONS --- + apply_custom_css() model = configure_gemini() -# --- 4. TONE SELECTION DROPDOWN IN SIDEBAR --- +# --- SIDEBAR TONE & PRIVACY TOGGLE --- TONE_OPTIONS = { "Compassionate Listener": "You are a compassionate listener — soft, empathetic, patient — like a therapist who listens without judgment.", "Motivating Coach": "You are a motivating coach — energetic, encouraging, and action-focused — helping the user push through rough days.", @@ -101,28 +142,44 @@ ) st.session_state.selected_tone = selected_tone -# --- 5. DEFINE FUNCTION TO GET TONE PROMPT --- + st.markdown("---") + st.markdown("### 🔒 Privacy Mode") + anonymous_mode_toggle = st.toggle("Enable Anonymous Mode", value=st.session_state.anonymous_mode) + if st.session_state.anonymous_mode != anonymous_mode_toggle: + st.session_state.anonymous_mode = anonymous_mode_toggle + st.rerun() + +# --- TONE PROMPT FUNCTION --- def get_tone_prompt(): - return TONE_OPTIONS.get(st.session_state.get("selected_tone", "Compassionate Listener"), TONE_OPTIONS["Compassionate Listener"]) + return TONE_OPTIONS.get(st.session_state.get("selected_tone", "Compassionate Listener")) + +# --- RENDER SIDEBAR --- +def render_sidebar(): + st.sidebar.title("🧠 TalkHeal") + st.sidebar.markdown(f"**🕒 Current Time:** {get_current_time()}") + st.sidebar.markdown("---") + + if st.sidebar.button("➕ Start New Conversation"): + create_new_conversation() + + st.sidebar.markdown("### 💬 Past Conversations") + if not st.session_state.conversations: + st.sidebar.info("No conversations yet.") + else: + for idx, convo in enumerate(st.session_state.conversations): + title = convo.get("title", f"Conversation {idx+1}") + if st.sidebar.button(f"🗂️ {title}", key=f"convo_{idx}"): + st.session_state.active_conversation = idx -# --- 6. RENDER SIDEBAR --- render_sidebar() -# --- 7. PAGE ROUTING --- +# --- ROUTING --- main_area = st.container() - if not st.session_state.conversations: - saved_conversations = load_conversations() - if saved_conversations: - st.session_state.conversations = saved_conversations - if st.session_state.active_conversation == -1: - st.session_state.active_conversation = 0 - else: - create_new_conversation() - st.session_state.active_conversation = 0 + create_new_conversation() st.rerun() -# --- 8. RENDER PAGE --- +# --- MAIN CONTENT --- if st.session_state.get("show_emergency_page"): with main_area: render_emergency_page() @@ -210,7 +267,11 @@ def mood_slider(): render_chat_interface() handle_chat_input(model, system_prompt=get_tone_prompt()) -# --- 9. SCROLL SCRIPT --- + # ✅ Save only if not in anonymous mode + if not st.session_state.anonymous_mode: + save_conversations(st.session_state.conversations) + +# --- AUTO-SCROLL SCRIPT --- st.markdown(""" -""", unsafe_allow_html=True) \ No newline at end of file +""", unsafe_allow_html=True) \ No newline at end of file diff --git a/components/chat_interface.py b/components/chat_interface.py index 874a329..8a61e55 100644 --- a/components/chat_interface.py +++ b/components/chat_interface.py @@ -96,7 +96,8 @@ def handle_chat_input(model, system_prompt): title = user_input[:30] + "..." if len(user_input) > 30 else user_input active_convo["title"] = title - save_conversations(st.session_state.conversations) + if not st.session_state.anonymous_mode: + save_conversations(st.session_state.conversations) # Format memory def format_memory(convo_history, max_turns=10): @@ -118,27 +119,29 @@ def format_memory(convo_history, max_turns=10): "time": get_current_time() }) - except ValueError as e: + except ValueError: st.error("I'm having trouble understanding your message. Could you please rephrase it?") active_convo["messages"].append({ "sender": "bot", "message": "I'm having trouble understanding your message. Could you please rephrase it?", "time": get_current_time() }) - except requests.RequestException as e: + except requests.RequestException: st.error("Network connection issue. Please check your internet connection.") active_convo["messages"].append({ "sender": "bot", "message": "I'm having trouble connecting to my services. Please check your internet connection and try again.", "time": get_current_time() }) - except Exception as e: - st.error(f"An unexpected error occurred. Please try again.") + except Exception: + st.error("An unexpected error occurred. Please try again.") active_convo["messages"].append({ "sender": "bot", "message": "I'm having trouble responding right now. Please try again in a moment.", "time": get_current_time() }) - save_conversations(st.session_state.conversations) + if not st.session_state.anonymous_mode: + save_conversations(st.session_state.conversations) + st.rerun() diff --git a/components/sidebar.py b/components/sidebar.py index 462617e..22f7ed2 100644 --- a/components/sidebar.py +++ b/components/sidebar.py @@ -224,7 +224,9 @@ def render_sidebar(): del st.session_state.conversations[st.session_state.delete_candidate] from core.utils import save_conversations - save_conversations(st.session_state.conversations) + if not st.session_state.get("anonymous_mode", False): + save_conversations(st.session_state.conversations) + del st.session_state.delete_candidate st.session_state.active_conversation = -1 diff --git a/core/utils.py b/core/utils.py index 97f3a0a..87dd4a9 100644 --- a/core/utils.py +++ b/core/utils.py @@ -6,41 +6,30 @@ import requests import google.generativeai +# Time utility def get_current_time(): - """Returns the user's local time formatted as HH:MM AM/PM.""" tz_offset = st.context.timezone_offset - if tz_offset is None: - # Default to UTC if timezone is not available (e.g., on Streamlit Cloud) now = datetime.now() else: now_utc = datetime.now(timezone.utc) now = now_utc + timedelta(minutes=-tz_offset) - return now.strftime("%I:%M %p").lstrip("0") - - +# Conversation management def create_new_conversation(initial_message=None): - """ - Creates a new conversation in the session state. - Optionally adds an initial message to the conversation. - Returns the index of the newly created conversation. - """ new_convo = { "id": len(st.session_state.conversations), "title": initial_message[:30] + "..." if initial_message and len(initial_message) > 30 else "New Conversation", "date": datetime.now().strftime("%B %d, %Y"), "messages": [] } - if initial_message: new_convo["messages"].append({ "sender": "user", "message": initial_message, "time": get_current_time() }) - st.session_state.conversations.insert(0, new_convo) st.session_state.active_conversation = 0 return 0 @@ -54,13 +43,12 @@ def clean_ai_response(response_text): response_text = response_text.replace('<', '<') response_text = response_text.replace('>', '>') response_text = response_text.replace('&', '&') - return response_text def get_ai_response(user_message, model): if model is None: return "I'm sorry, I can't connect right now. Please check the API configuration." - + mental_health_prompt = f""" You are a compassionate mental health support chatbot named TalkHeal. Your role is to: 1. Provide empathetic, supportive responses @@ -69,87 +57,93 @@ def get_ai_response(user_message, model): 4. Be warm, understanding, and non-judgmental 5. Ask follow-up questions to better understand the user's situation 6. Provide coping strategies and resources when appropriate - 7. Not assume that the user is always in overwhelming states. Sometimes he/she might also be in joyful or curious moods and ask questions not related to mental health - - IMPORTANT: Respond with PLAIN TEXT ONLY. Do not include any HTML tags, markdown formatting, or special characters. Just provide a natural, conversational response. - + 7. Not assume the user is always overwhelmed – they might also just be curious or joyful. + + IMPORTANT: Respond with PLAIN TEXT ONLY. Do not include HTML or markdown. + User message: {user_message} - - Respond in a caring, supportive manner (keep response under 150 words): + + Respond in a caring, supportive manner (max 150 words): """ try: response = model.generate_content(mental_health_prompt) - # Clean the response to remove any HTML or unwanted formatting - cleaned_response = clean_ai_response(response.text) - return cleaned_response - except ValueError as e: - # Handle invalid input or model configuration issues - return "I'm having trouble understanding your message. Could you please rephrase it?" - except google.generativeai.types.BlockedPromptException as e: - # Handle content policy violations + return clean_ai_response(response.text) + except google.generativeai.types.BlockedPromptException: return "I understand you're going through something difficult. Let's focus on how you're feeling and what might help you feel better." - except google.generativeai.types.GenerationException as e: - # Handle generation errors + except google.generativeai.types.GenerationException: return "I'm having trouble generating a response right now. Please try again in a moment." - except requests.RequestException as e: - # Handle network/API connection issues + except requests.RequestException: return "I'm having trouble connecting to my services. Please check your internet connection and try again." - except Exception as e: - # Log unexpected errors for debugging (you can add logging here) - # import logging - # logging.error(f"Unexpected error in get_ai_response: {e}") - return "I'm here to listen and support you. Sometimes I have trouble connecting, but I want you to know that your feelings are valid and you're not alone. Would you like to share more about what you're experiencing?" + except Exception: + return "I'm here to listen and support you. Sometimes I have trouble connecting, but I want you to know that your feelings are valid and you're not alone. Would you like to share more?" +# IP caching def cached_user_ip(): - # Check if IP is already cached in session state if hasattr(st.session_state, 'cached_ip') and hasattr(st.session_state, 'ip_cache_time'): - # Check if cache is still valid (cache for 1 hour) - cache_age = datetime.now() - st.session_state.ip_cache_time - if cache_age < timedelta(hours=1): + if datetime.now() - st.session_state.ip_cache_time < timedelta(hours=1): return st.session_state.cached_ip - - # Cache is missing or expired, fetch new IP try: - response = requests.get("https://api.ipify.org", timeout=5) - ip = response.text.strip() - # Cache the IP and timestamp + ip = requests.get("https://api.ipify.org", timeout=5).text.strip() st.session_state.cached_ip = ip st.session_state.ip_cache_time = datetime.now() - return ip - except (requests.RequestException, requests.Timeout, Exception): - # Fallback: use session ID or generate a unique identifier + except: fallback_id = f"session_{hash(str(st.session_state)) % 100000}" - - # Cache the fallback ID so we use the same one consistently - if not hasattr(st.session_state, 'cached_ip'): - st.session_state.cached_ip = fallback_id - st.session_state.ip_cache_time = datetime.now() - - return st.session_state.cached_ip - -#Implementing IP Based Isolation + st.session_state.cached_ip = fallback_id + st.session_state.ip_cache_time = datetime.now() + return fallback_id + def get_user_ip(): try: return requests.get("https://api.ipify.org").text except: return "unknown_ip" -#Saving and loading to/from JSON File +# JSON memory file def get_memory_file(): ip = cached_user_ip() os.makedirs("data", exist_ok=True) return f"data/conversations_{ip}.json" def save_conversations(conversations): - memory_file = get_memory_file() - with open(memory_file, 'w', encoding="utf-8") as f: + with open(get_memory_file(), 'w', encoding="utf-8") as f: json.dump(conversations, f, indent=4) def load_conversations(): - memory_file = get_memory_file() - if not os.path.exists(memory_file): + if not os.path.exists(get_memory_file()): return [] - with open(memory_file, 'r', encoding="utf-8") as f: + with open(get_memory_file(), 'r', encoding="utf-8") as f: return json.load(f) + +# 🧠 SIDEBAR Handling +def render_sidebar(): + st.sidebar.title("🧠 TalkHeal") + + if "conversations" not in st.session_state: + st.session_state.conversations = load_conversations() + + if "active_conversation" not in st.session_state: + st.session_state.active_conversation = 0 + + # New conversation button + if st.sidebar.button("➕ New Chat"): + create_new_conversation() + + # Display conversation list + for idx, convo in enumerate(st.session_state.conversations): + title = convo["title"] + date = convo.get("date", "") + label = f"🗨️ {title} ({date})" + if st.sidebar.button(label, key=f"convo_{idx}"): + st.session_state.active_conversation = idx + + # Clear conversations + if st.sidebar.button("🗑️ Clear All Chats"): + st.session_state.conversations = [] + st.session_state.active_conversation = 0 + save_conversations([]) + + st.sidebar.markdown("---") + st.sidebar.caption("Your conversations are stored locally and are private.") + diff --git a/pages/Journaling.py b/pages/Journaling.py index 715e19c..13b8016 100644 --- a/pages/Journaling.py +++ b/pages/Journaling.py @@ -143,11 +143,13 @@ def journaling_app(): with st.form("journal_form"): journal_text = st.text_area("How are you feeling today?", height=200) submitted = st.form_submit_button("Submit Entry") - if submitted and journal_text.strip(): sentiment = analyze_sentiment(journal_text) - save_entry(email, journal_text, sentiment) - st.success(f"Entry saved! Sentiment: **{sentiment}**") + if not st.session_state.get("anonymous_mode", False): + save_entry(email, journal_text, sentiment) + st.success(f"Entry saved! Sentiment: **{sentiment}**") + else: + st.success(f"(Anonymous Mode) Entry not saved. Sentiment: **{sentiment}**") st.markdown("---") st.subheader("📖 Your Journal Entries") @@ -160,7 +162,11 @@ def journaling_app(): with col2: end_date = st.date_input("End Date", value=datetime.date.today()) - entries = fetch_entries(email, sentiment_filter=filter_sentiment, start_date=start_date, end_date=end_date) + if not st.session_state.get("anonymous_mode", False): + entries = fetch_entries(email, sentiment_filter=filter_sentiment, start_date=start_date, end_date=end_date) + else: + entries = [] + st.info("Anonymous Mode is ON. No entries are loaded from storage.") if not entries: st.info("No entries found for selected filters.")