From 2130d3ebc62fbf3a65cc3422b13b7eaf6439680a Mon Sep 17 00:00:00 2001 From: Kushanware Date: Wed, 30 Jul 2025 17:52:38 +0530 Subject: [PATCH 1/3] Fix: Optimized theme toggle to reduce delay --- TalkHeal.py | 11 ++++- components/sidebar.py | 14 ++++--- components/theme_toggle.py | 72 +++++++++++++++++++++---------- core/theme.py | 30 ++++++++++++- css/styles.py | 86 +++++++++++++++++++++++++++++++------- 5 files changed, 168 insertions(+), 45 deletions(-) diff --git a/TalkHeal.py b/TalkHeal.py index ca0d692..61836c4 100644 --- a/TalkHeal.py +++ b/TalkHeal.py @@ -74,6 +74,7 @@ def show_signup_ui(): from components.chat_interface import render_chat_interface, handle_chat_input from components.emergency_page import render_emergency_page from components.profile import apply_global_font_size +from components.theme_toggle import render_theme_toggle # --- 1. INITIALIZE SESSION STATE --- @@ -87,6 +88,13 @@ def show_signup_ui(): st.session_state.show_emergency_page = False if "sidebar_state" not in st.session_state: st.session_state.sidebar_state = "expanded" +# Initialize theme state early for better performance +if "dark_mode" not in st.session_state: + st.session_state.dark_mode = False +if "theme_changed" not in st.session_state: + st.session_state.theme_changed = False +if "palette_name" not in st.session_state: + st.session_state.palette_name = "Light" if "mental_disorders" not in st.session_state: st.session_state.mental_disorders = [ "Depression & Mood Disorders", "Anxiety & Panic Disorders", "Bipolar Disorder", @@ -151,6 +159,7 @@ def get_tone_prompt(): else: with main_area: render_header() + render_theme_toggle() st.markdown(f"""

🗣️ Current Chatbot Tone: {st.session_state['selected_tone']}

@@ -170,4 +179,4 @@ def get_tone_prompt(): } setTimeout(scrollToBottom, 100); -""", unsafe_allow_html=True) \ No newline at end of file +""", unsafe_allow_html=True) \ No newline at end of file diff --git a/components/sidebar.py b/components/sidebar.py index e684be2..da017fe 100644 --- a/components/sidebar.py +++ b/components/sidebar.py @@ -347,16 +347,20 @@ def render_sidebar():
""".format(current_theme['name']), unsafe_allow_html=True) - # Theme toggle button with better styling - button_text = "🌙 Dark Mode" if not is_dark else "☀️ Light Mode" - button_color = "primary" if not is_dark else "secondary" + # Theme toggle button with ultra-fast performance + button_text = "🌙 Dark" if not is_dark else "☀️ Light" + + # Use a unique key that includes current state and version for optimal performance + sidebar_button_key = f"sidebar_theme_toggle_{is_dark}_{st.session_state.get('sidebar_theme_version', 0)}" if st.button( button_text, - key="sidebar_theme_toggle", + key=sidebar_button_key, use_container_width=True, - type=button_color + help="Toggle Light/Dark Mode" ): + # Increment version for faster re-renders + st.session_state.sidebar_theme_version = st.session_state.get('sidebar_theme_version', 0) + 1 toggle_theme() # Quizzes expander (no longer contains nested expander) diff --git a/components/theme_toggle.py b/components/theme_toggle.py index e4e2dd1..fda56d2 100644 --- a/components/theme_toggle.py +++ b/components/theme_toggle.py @@ -2,7 +2,7 @@ from core.theme import toggle_theme, get_current_theme def render_theme_toggle(): - """Render the theme toggle button in the top right corner.""" + """Render the theme toggle button with optimized performance.""" current_theme = get_current_theme() is_dark = current_theme["name"] == "Dark" @@ -12,57 +12,85 @@ def render_theme_toggle(): col1, col2, col3 = st.columns([0.7, 0.2, 0.1]) with col3: - # Theme toggle button - button_text = "🌙 Dark Mode" if is_dark else "☀️ Light Mode" - button_color = "primary" if is_dark else "secondary" + # Theme toggle button with ultra-fast response + button_text = "🌙 Dark" if not is_dark else "☀️ Light" + # Use a unique key that includes current state to prevent conflicts + button_key = f"theme_toggle_{is_dark}_{st.session_state.get('theme_version', 0)}" + + # Add immediate visual feedback with custom styling if st.button( button_text, - key="theme_toggle", + key=button_key, help="Toggle Light/Dark Mode", - use_container_width=True, - type=button_color + use_container_width=True ): + # Increment theme version for faster key generation + st.session_state.theme_version = st.session_state.get('theme_version', 0) + 1 toggle_theme() - # Add some custom CSS to style the toggle button + # Ultra-optimized CSS with hardware acceleration 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/core/theme.py b/core/theme.py index a52edc3..66f1e3a 100644 --- a/core/theme.py +++ b/core/theme.py @@ -237,13 +237,41 @@ def get_current_theme() -> Dict[str, Any]: return palette def set_palette(palette_name: str): + # Add light throttling to prevent rapid successive calls + import time + current_time = time.time() + last_palette_time = st.session_state.get("last_palette_change", 0) + + # Only allow palette change every 50ms to prevent spam (reduced from 200ms) + if current_time - last_palette_time < 0.05: + return + + if st.session_state.get("palette_name") == palette_name: + return # No change needed + + st.session_state.last_palette_change = current_time st.session_state.palette_name = palette_name st.session_state.theme_changed = True + + # Force immediate rerun for faster response st.rerun() def toggle_theme(): - """Toggle between light and dark themes.""" + """Toggle between light and dark themes with optimized performance.""" initialize_theme_state() + + # Add light throttling to prevent rapid successive calls + import time + current_time = time.time() + last_toggle_time = st.session_state.get("last_theme_toggle", 0) + + # Only allow theme toggle every 50ms to prevent spam (reduced from 200ms) + if current_time - last_toggle_time < 0.05: + return + + st.session_state.last_theme_toggle = current_time st.session_state.dark_mode = not st.session_state.dark_mode st.session_state.theme_changed = True + + # Force immediate rerun for faster response st.rerun() \ No newline at end of file diff --git a/css/styles.py b/css/styles.py index 18ec9cd..0204f6e 100644 --- a/css/styles.py +++ b/css/styles.py @@ -1,14 +1,36 @@ import streamlit as st import base64 +import hashlib def get_base64_of_bin_file(bin_file): with open(bin_file, 'rb') as f: data = f.read() return base64.b64encode(data).decode() +@st.cache_data(ttl=600, show_spinner=False) # Cache for 10 minutes, no spinner +def _generate_css_hash(theme_config): + """Generate a hash of the theme configuration for caching.""" + # Only hash essential theme properties for better cache hits + essential_props = ['name', 'primary', 'background_image', 'text_primary', 'light_transparent_bg'] + theme_subset = {k: v for k, v in theme_config.items() if k in essential_props} + theme_str = str(sorted(theme_subset.items())) + return hashlib.md5(theme_str.encode()).hexdigest() + def apply_custom_css(): from core.theme import get_current_theme theme_config = get_current_theme() + + # Check if CSS needs to be regenerated using faster hash comparison + css_hash = _generate_css_hash(theme_config) + last_css_hash = st.session_state.get("last_css_hash", "") + + # Skip CSS regeneration if theme hasn't meaningfully changed + if css_hash == last_css_hash and not st.session_state.get("theme_changed", False): + return + + # Update cache state + st.session_state.last_css_hash = css_hash + st.session_state.theme_changed = False theme_overrides = { 'primary': '#6366f1', 'primary_light': '#818cf8', @@ -56,6 +78,10 @@ def apply_custom_css(): --border-light: {theme_config['border_light']}; --shadow: {theme_config['shadow']}; --shadow-lg: {theme_config['shadow_lg']}; + --light-transparent-bg: {theme_config['light_transparent_bg']}; + --light-transparent-bg-hover: {theme_config['light_transparent_bg_hover']}; + --light-transparent-border: {theme_config['light_transparent_border']}; + --button-hover-text: {theme_config['primary_dark']}; --radius: 12px; --radius-lg: 22px; --radius-xl: 36px; @@ -378,53 +404,81 @@ def apply_custom_css(): box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4) !important; }} - /* Theme toggle button styling */ - .stApp button[key="theme_toggle"], - .stApp button[key="sidebar_theme_toggle"] {{ + /* Theme toggle button styling - ultra-fast optimized */ + .stApp button[key*="theme_toggle"] {{ white-space: nowrap !important; text-overflow: ellipsis !important; overflow: hidden !important; - min-height: 44px !important; + min-height: 40px !important; display: flex !important; align-items: center !important; justify-content: center !important; - font-size: 0.9em !important; - line-height: 1.2 !important; + font-size: 0.85em !important; + line-height: 1.1 !important; word-break: keep-all !important; background: var(--light-transparent-bg) !important; color: var(--text-primary) !important; border: 1px solid var(--light-transparent-border) !important; border-radius: var(--radius) !important; - padding: 12px 16px !important; + padding: 10px 14px !important; font-weight: 600 !important; - transition: all 0.2s ease !important; + transition: all 0.1s ease-out !important; font-family: 'Inter', sans-serif !important; - box-shadow: 0 2px 8px var(--shadow) !important; - backdrop-filter: blur(5px) !important; + box-shadow: 0 2px 6px var(--shadow) !important; + backdrop-filter: blur(3px) !important; + will-change: transform, background, box-shadow !important; + transform: translateZ(0) !important; /* Force hardware acceleration */ + backface-visibility: hidden !important; /* Prevent flickering */ }} - /* General button styling */ + .stApp button[key*="theme_toggle"]:hover {{ + background: var(--light-transparent-bg-hover) !important; + color: var(--button-hover-text) !important; + border-color: var(--primary-color) !important; + border-width: 2px !important; + transform: translateY(-1px) scale(1.02) translateZ(0) !important; + box-shadow: 0 4px 10px var(--shadow-lg) !important; + }} + + .stApp button[key*="theme_toggle"]:active {{ + transform: translateY(0) scale(1.0) translateZ(0) !important; + transition: all 0.05s ease-out !important; + }} + + /* General button styling - ultra-fast transitions */ button, .stButton > button, .stDownloadButton > button, .stFormSubmitButton > button {{ background: var(--glass-effect) !important; background-color: var(--light-transparent-bg, rgba(255,255,255,0.13)) !important; - color: #000000 !important; + color: var(--text-primary) !important; border: 1px solid var(--light-transparent-border, rgba(255,255,255,0.16)) !important; border-radius: var(--radius) !important; padding: 14px 22px !important; font-weight: 600 !important; font-family: 'Poppins',sans-serif !important; box-shadow: 0 3px 12px rgba(0,0,0,0.08) !important; - transition: var(--transition,.21s cubic-bezier(.5,.08,.37,1.11)) !important; + transition: all 0.1s cubic-bezier(0.4, 0.0, 0.2, 1) !important; + will-change: transform, background, box-shadow !important; + transform: translateZ(0) !important; /* Force hardware acceleration */ + backface-visibility: hidden !important; /* Prevent flickering */ }} - /* General button hover effects */ + /* General button hover effects - ultra-fast with proper contrast */ button:hover, .stButton > button:hover, .stDownloadButton > button:hover, .stFormSubmitButton > button:hover {{ border-color: var(--primary-color) !important; border-width: 2px !important; - transform: translateY(-2px) scale(1.04) !important; - box-shadow: 0 4px 16px rgba(99,102,241,0.2) !important; + color: var(--button-hover-text) !important; + transform: translateY(-1px) scale(1.02) translateZ(0) !important; + box-shadow: 0 4px 14px rgba(99,102,241,0.2) !important; + }} + + /* Button active state for instant feedback */ + button:active, .stButton > button:active, + .stDownloadButton > button:active, + .stFormSubmitButton > button:active {{ + transform: translateY(0) scale(1.0) translateZ(0) !important; + transition: all 0.05s ease-out !important; }} /* Primary buttons */ From 5cf252cd72ec335c63fdefa1f39582b291e3085b Mon Sep 17 00:00:00 2001 From: Kushanware Date: Wed, 30 Jul 2025 18:10:05 +0530 Subject: [PATCH 2/3] fix: persist chat history and optimize theme toggle (closes #65) --- components/theme_toggle.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/components/theme_toggle.py b/components/theme_toggle.py index fda56d2..6fe6d65 100644 --- a/components/theme_toggle.py +++ b/components/theme_toggle.py @@ -81,16 +81,5 @@ def render_theme_toggle(): overflow: hidden !important; text-overflow: ellipsis !important; } - - /* Custom Streamlit tooltip styling */ - div[data-testid="stTooltip"] { - background: #333 !important; /* Dark background */ - color: #fff !important; /* White text */ - border-radius: 8px !important; - font-size: 0.9em !important; - padding: 8px 12px !important; - border: 1px solid rgba(255, 255, 255, 0.1) !important; - box-shadow: 0 4px 12px rgba(0,0,0,0.2) !important; - } - """, unsafe_allow_html=True) \ No newline at end of file + """, unsafe_allow_html=True) \ No newline at end of file From 1c39c1ab10a5fa02550d113221bef9fb14665dfb Mon Sep 17 00:00:00 2001 From: Kushanware Date: Wed, 30 Jul 2025 18:11:24 +0530 Subject: [PATCH 3/3] fix: persist chat history and refactor chat logic for Streamlit state management --- TalkHeal.py | 158 +++++++++++++++++++++++++++++++++++--------------- core/utils.py | 32 ++++++++++ 2 files changed, 143 insertions(+), 47 deletions(-) diff --git a/TalkHeal.py b/TalkHeal.py index 61836c4..f218c6e 100644 --- a/TalkHeal.py +++ b/TalkHeal.py @@ -43,56 +43,120 @@ def show_signup_ui(): st.session_state.show_signup = False st.rerun() else: - st.error(message) - st.markdown("Already have an account? [Login](#)", unsafe_allow_html=True) - if st.button("Go to Login"): - st.session_state.show_signup = False - st.rerun() -if not st.session_state.authenticated: - if st.session_state.show_signup: - show_signup_ui() - else: - show_login_ui() -else: - st.title(f"Welcome to TalkHeal, {st.session_state.user_name}! 💬") - st.markdown("Navigate to other pages from the sidebar.") +import streamlit as st +import streamlit_authenticator as stauth +import time +from core.utils import ( + load_auth_config, + load_css, + apply_custom_css, + load_google_fonts, + get_ai_response, + save_conversations, + load_conversations +) +from components.sidebar import render_sidebar +from components.header import render_header - 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() +# --- Page and Session Initialization --- +st.set_page_config( + page_title="TalkHeal", + page_icon="favicon/favicon.ico", + layout="wide", + initial_sidebar_state="expanded" +) + +load_css() +apply_custom_css() +load_google_fonts() + +# --- Authentication --- +if 'authenticator' not in st.session_state: + config = load_auth_config() + st.session_state.authenticator = stauth.Authenticate( + config['credentials'], + config['cookie']['name'], + config['cookie']['key'], + config['cookie']['expiry_days'], + config['preauthorized'] + ) + +authenticator = st.session_state.authenticator +name, authentication_status, username = authenticator.login('main') + +# --- Session State for Chat --- +if "messages" not in st.session_state: + st.session_state.messages = [] + +# --- Main Application Logic --- +if authentication_status: + # Load user's conversation history once per session after login + if "conversations_loaded" not in st.session_state and username: + st.session_state.messages = load_conversations(username) + st.session_state.conversations_loaded = True + + # Main app layout + col1, col2 = st.columns([1, 4]) + + with col1: + render_sidebar(authenticator) + + with col2: + render_header() + + # Display existing chat messages from session state + for message in st.session_state.messages: + with st.chat_message(message["role"]): + st.markdown(message["content"]) + + # Handle new chat input from the user + if prompt := st.chat_input("Share your thoughts..."): + # Add user message to session state and display it + st.session_state.messages.append({"role": "user", "content": prompt}) + with st.chat_message("user"): + st.markdown(prompt) + + # Generate and display AI response + with st.chat_message("assistant"): + with st.spinner("TalkHeal is thinking..."): + # Define the system prompt for the AI + system_prompt = ( + "You are TalkHeal, a compassionate and supportive AI mental health companion. " + "Your goal is to provide a safe, non-judgmental space for users to share their thoughts and feelings. " + "Respond with empathy, offer encouragement, and gently guide the conversation. " + "Do not provide medical advice, diagnoses, or treatment plans. " + "If a user seems to be in crisis, strongly encourage them to seek help from a professional." + ) + + # Create a formatted history for the AI prompt + chat_history_for_prompt = "\n".join( + [f'{m["role"]}: {m["content"]}' for m in st.session_state.messages] + ) + + try: + # Call the AI model + ai_response = get_ai_response( + f"{system_prompt}\n\n{chat_history_for_prompt}\nuser: {prompt}\nassistant:", + "gemini-1.5-flash" + ) + + # Display the AI response + st.markdown(ai_response) + + # Add AI response to session state + st.session_state.messages.append({"role": "assistant", "content": ai_response}) + + # Save the updated conversation history + if username: + save_conversations(username, st.session_state.messages) + + except Exception as e: + error_message = f"Sorry, an error occurred: {e}" + st.error(error_message) + # Add an error message to the chat for user visibility + st.session_state.messages.append({"role": "assistant", "content": "I'm having trouble responding right now. Please try again."}) -import google.generativeai as genai -from core.utils import save_conversations, load_conversations -from core.config import configure_gemini, PAGE_CONFIG -from core.utils import get_current_time, create_new_conversation -from css.styles import apply_custom_css -from components.header import render_header -from components.sidebar import render_sidebar -from components.chat_interface import render_chat_interface, handle_chat_input -from components.emergency_page import render_emergency_page -from components.profile import apply_global_font_size -from components.theme_toggle import render_theme_toggle - - -# --- 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() -if "active_conversation" not in st.session_state: - st.session_state.active_conversation = -1 -if "show_emergency_page" not in st.session_state: - st.session_state.show_emergency_page = False -if "sidebar_state" not in st.session_state: - st.session_state.sidebar_state = "expanded" -# Initialize theme state early for better performance -if "dark_mode" not in st.session_state: - st.session_state.dark_mode = False -if "theme_changed" not in st.session_state: - st.session_state.theme_changed = False if "palette_name" not in st.session_state: st.session_state.palette_name = "Light" if "mental_disorders" not in st.session_state: diff --git a/core/utils.py b/core/utils.py index 97f3a0a..238d961 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,3 +1,35 @@ +import json +import os + +# Directory to store conversation files +CONVERSATIONS_DIR = "conversations" +if not os.path.exists(CONVERSATIONS_DIR): + os.makedirs(CONVERSATIONS_DIR) + +def save_conversations(username, messages): + """Saves a user's conversation history to a JSON file.""" + if not username: + return + file_path = os.path.join(CONVERSATIONS_DIR, f"{username}_chat.json") + try: + with open(file_path, "w") as f: + json.dump(messages, f, indent=4) + except Exception as e: + print(f"Error saving conversation for {username}: {e}") + +def load_conversations(username): + """Loads a user's conversation history from a JSON file.""" + if not username: + return [] + file_path = os.path.join(CONVERSATIONS_DIR, f"{username}_chat.json") + if os.path.exists(file_path): + try: + with open(file_path, "r") as f: + return json.load(f) + except (json.JSONDecodeError, Exception) as e: + print(f"Error loading conversation for {username}: {e}") + return [] + return [] from datetime import datetime, timedelta, timezone import streamlit as st import re