diff --git a/TalkHeal.py b/TalkHeal.py index 785d7e1..a4620b0 100644 --- a/TalkHeal.py +++ b/TalkHeal.py @@ -1,72 +1,81 @@ + import streamlit as st -from auth.auth_utils import init_db -from components.login_page import show_login_page +import streamlit_authenticator as stauth +import time +from auth.auth_utils import init_db, register_user, authenticate_user st.set_page_config(page_title="TalkHeal", page_icon="💬", layout="wide") -# --- 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 -# --- LOGIN PAGE --- -if not st.session_state.authenticated: - show_login_page() - st.stop() - -# --- TOP RIGHT BUTTONS: THEME TOGGLE & LOGOUT --- -if st.session_state.get("authenticated", False): - col_spacer, col_theme, col_logout = st.columns([5, 0.5, 0.7]) - with col_spacer: - pass # empty spacer to push buttons right - with col_theme: - is_dark = st.session_state.get('dark_mode', False) - 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 +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 + # Set user_email and user_name separately for journaling page access + st.session_state.user_email = user["email"] + st.session_state.user_name = user["name"] st.rerun() - with col_logout: - if st.button("Logout", key="logout_btn", use_container_width=True): - for key in ["authenticated", "user_email", "user_name", "show_signup"]: - if key in st.session_state: - del st.session_state[key] + else: + st.warning("Invalid email or password.") + st.markdown("Don't have an account? [Sign up](#)", unsafe_allow_html=True) + if st.button("Go to Sign Up"): + st.session_state.show_signup = True + st.rerun() + +def show_signup_ui(): + st.subheader("📝 Sign Up") + name = st.text_input("Name", key="signup_name") + email = st.text_input("Email", key="signup_email") + password = st.text_input("Password", type="password", key="signup_password") + if st.button("Sign Up"): + success, message = register_user(name, email, password) + if success: + st.success("Account created! Please log in.") + st.session_state.show_signup = False st.rerun() + else: + st.error(message) -# --- MAIN UI (only after login) --- -header_col1, header_col2, header_col3 = st.columns([6, 1, 1]) -with header_col1: - st.title(f"Welcome to TalkHeal, {st.session_state.user_name}! 💬") - st.markdown("Navigate to other pages from the sidebar.") +# --- PERFORMANCE OPTIMIZED IMPORTS --- -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 core.utils import ( + get_ai_response, + save_conversations, + load_conversations, + create_new_conversation +) from css.styles import apply_custom_css -from components.header import render_header +from core.config import configure_gemini +from components.profile import apply_global_font_size from components.sidebar import render_sidebar -from components.chat_interface import render_chat_interface, handle_chat_input +from components.header import render_header +from components.theme_toggle import render_theme_toggle from components.emergency_page import render_emergency_page -from components.profile import apply_global_font_size +from components.chat_interface import render_chat_interface, handle_chat_input +# --- Apply CSS and configure --- +apply_custom_css() -# --- 1. INITIALIZE SESSION STATE --- -if "chat_history" not in st.session_state: - st.session_state.chat_history = [] +# Initialize required session state variables if "conversations" not in st.session_state: - st.session_state.conversations = load_conversations() + st.session_state.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" + +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", @@ -131,6 +140,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']}

@@ -150,20 +160,4 @@ def get_tone_prompt(): } setTimeout(scrollToBottom, 100); -""", unsafe_allow_html=True) - -st.markdown(""" - """, unsafe_allow_html=True) \ No newline at end of file diff --git a/components/sidebar.py b/components/sidebar.py index 462617e..199511e 100644 --- a/components/sidebar.py +++ b/components/sidebar.py @@ -426,16 +426,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..95e0437 100644 --- a/components/theme_toggle.py +++ b/components/theme_toggle.py @@ -2,67 +2,60 @@ 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 maximum performance optimization.""" current_theme = get_current_theme() is_dark = current_theme["name"] == "Dark" - # Create a container for the theme toggle + # Create a container for the theme toggle with minimal re-rendering with st.container(): # Use columns to position the toggle on the right 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" + # Optimized button text with minimal emoji processing + button_text = "🌙" if not is_dark else "☀️" + # Use static key to prevent unnecessary widget recreations + button_key = "theme_toggle_optimized" + + # Ultra-fast button with minimal state management 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 ): + # Direct theme toggle without version tracking toggle_theme() - # Add some custom CSS to style the toggle button + # Minimal CSS with hardware acceleration for maximum performance st.markdown(""" """, unsafe_allow_html=True) \ No newline at end of file diff --git a/core/theme.py b/core/theme.py index a52edc3..04fdc2c 100644 --- a/core/theme.py +++ b/core/theme.py @@ -237,13 +237,53 @@ 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 maximum performance optimization.""" initialize_theme_state() + + # Optimized throttling - reduced to 20ms for faster response while preventing spam + import time + current_time = time.time() + last_toggle_time = st.session_state.get("last_theme_toggle", 0) + + # Minimal throttling to prevent accidental double-clicks only + if current_time - last_toggle_time < 0.02: + return + + st.session_state.last_theme_toggle = current_time + + # Direct state change without extra flags st.session_state.dark_mode = not st.session_state.dark_mode - st.session_state.theme_changed = True - st.rerun() \ No newline at end of file + + # Mark that CSS needs to be reloaded + st.session_state.force_css_reload = True + + # Optimized rerun - use fastest available method + try: + # Try the newer, faster rerun method first + import streamlit.runtime.scriptrunner.script_runner as sr + sr.get_script_run_ctx().request_rerun() + except (AttributeError, ImportError): + try: + st.experimental_rerun() + except AttributeError: + st.rerun() \ No newline at end of file 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 diff --git a/css/styles.py b/css/styles.py index 18ec9cd..d4b9340 100644 --- a/css/styles.py +++ b/css/styles.py @@ -1,14 +1,37 @@ 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=300, show_spinner=False) # Cache for 5 minutes, faster invalidation +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'] + 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(): + """Apply CSS with performance optimizations to reduce theme toggle delays.""" from core.theme import get_current_theme theme_config = get_current_theme() + + # Ultra-fast CSS change detection + css_hash = _generate_css_hash(theme_config) + last_css_hash = st.session_state.get("last_css_hash", "") + + # Skip CSS regeneration if theme hasn't changed (major performance boost) + if css_hash == last_css_hash and not st.session_state.get("force_css_reload", False): + return + + # Update cache state with minimal overhead + st.session_state.last_css_hash = css_hash + st.session_state.force_css_reload = False theme_overrides = { 'primary': '#6366f1', 'primary_light': '#818cf8', @@ -56,11 +79,15 @@ 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; --glass-effect: linear-gradient(135deg, rgba(255,255,255,0.10), rgba(255,255,255,0.05)); - --transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + --transition: all 0.1s ease-out; }} .stApp > header {{ @@ -126,10 +153,9 @@ def apply_custom_css(): font-weight: 470; line-height: 1.5; position: relative; - backdrop-filter: blur(11px); display: block; - opacity: 0; - animation: floatUp 0.5s cubic-bezier(0.25,0.8,0.25,1) forwards; + opacity: 1; + transform: none; }} /* User message styling */ @@ -151,10 +177,10 @@ def apply_custom_css(): margin-right: auto; }} - /* Message animation */ + /* Simplified message animation for better performance */ @keyframes floatUp {{ - from {{ transform: translateY(20px); opacity: 0; }} - to {{ transform: none; opacity: 1; }} + from {{ opacity: 0.7; }} + to {{ opacity: 1; }} }} /* Message timestamp styling */ @@ -169,7 +195,7 @@ def apply_custom_css(): color: #c4d0e0; }} - /* Welcome message styling */ + /* Welcome message styling - performance optimized */ .welcome-message {{ background: linear-gradient(120deg, rgba(99,102,241,0.75) 0%, rgba(236,72,153,0.75) 100%); color: #f7fafb; @@ -177,24 +203,22 @@ def apply_custom_css(): margin: 38px auto 32px auto; border-radius: var(--radius-xl); max-width: 680px; - box-shadow: 0 6px 38px 0 rgba(96,100,255,0.11); + box-shadow: 0 4px 24px 0 rgba(96,100,255,0.08); font-size: 1.14em; text-align: center; - backdrop-filter: blur(12px); position: relative; overflow: hidden; - transform: scale(0.97); - opacity: 0; - animation: scaleIn 0.7s cubic-bezier(0.22,0.68,0.32,1.18) .1s forwards; + opacity: 1; + transform: scale(1); }} - /* Welcome message animation */ + /* Simplified animation for better performance */ @keyframes scaleIn {{ - from {{ transform: scale(0.97); opacity: 0; }} - to {{ transform: scale(1); opacity: 1; }} + from {{ opacity: 0.8; }} + to {{ opacity: 1; }} }} - /* Main header styling */ + /* Header styling - performance optimized */ .main-header {{ --gradient: linear-gradient(100deg, var(--primary-color), var(--secondary-color)); text-align: center; @@ -207,21 +231,13 @@ def apply_custom_css(): border: 1px solid var(--border-light); position: relative; overflow: hidden; - backdrop-filter: blur(10px); }} - /* Header gradient animation */ + /* Simplified header accent - no animation for better performance */ .main-header::before {{ content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--gradient); - animation: gradientFlow 7s linear infinite; - background-size: 200% 200%; - }} - @keyframes gradientFlow {{ - 0% {{ background-position: 0% 50%; }} - 50% {{ background-position: 100% 50%;}} - 100% {{ background-position: 0% 50%;}} }} /* Header title styling */ @@ -261,7 +277,6 @@ def apply_custom_css(): font-weight: 600; font-size: 1.1em; border: 1px solid rgba(239, 68, 68, 0.8) !important; - backdrop-filter: blur(5px); text-decoration: none !important; position: relative; overflow: hidden; @@ -302,11 +317,10 @@ def apply_custom_css(): /* Sidebar styling */ [data-testid="stSidebar"] {{ background: linear-gradient(120deg, rgba(236,72,153,0.45), rgba(219,39,119,0.25), rgba(236,72,153,0.15)) !important; - backdrop-filter: blur(15px) !important; border-right: 2px solid rgba(236,72,153,0.35) !important; - box-shadow: 8px 0 48px rgba(236,72,153,0.25) !important; + box-shadow: 4px 0 24px rgba(236,72,153,0.15) !important; color: #e2e8f0 !important; - transition: background .32s cubic-bezier(.5,.13,.36,1.19); + transition: background .2s ease-out; }} [data-testid="stSidebar"] * {{ color: #f5f7fb !important; @@ -327,8 +341,7 @@ def apply_custom_css(): height: 40px !important; font-size: 20px !important; font-weight: 900 !important; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3) !important; - backdrop-filter: blur(10px) !important; + box-shadow: 0 3px 12px rgba(0, 0, 0, 0.2) !important; }} /* Sidebar toggle button hover effects */ @@ -378,53 +391,79 @@ 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 */ + }} + + .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; }} - /* General button styling */ + .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 - optimized for speed */ 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; + box-shadow: 0 2px 8px rgba(0,0,0,0.05) !important; + transition: all 0.05s ease-out !important; + transform: translateZ(0) !important; }} - /* General button hover effects */ + /* General button hover effects - instant feedback */ 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: translateZ(0) !important; + box-shadow: 0 3px 10px rgba(99,102,241,0.15) !important; + }} + + /* Button active state for instant feedback */ + button:active, .stButton > button:active, + .stDownloadButton > button:active, + .stFormSubmitButton > button:active {{ + transform: translateZ(0) !important; + transition: all 0.02s ease-out !important; }} /* Primary buttons */ @@ -508,7 +547,7 @@ def apply_custom_css(): opacity: .77 !important; }} - /* Floating action button styling */ + /* Floating action button styling - optimized */ .floating-action-button {{ position: fixed; bottom: 28px; right: 28px; @@ -518,16 +557,16 @@ def apply_custom_css(): color: #fff; display: flex; align-items: center; justify-content: center; font-size: 23px; - box-shadow: 0 6px 24px rgba(0,0,0,0.14); + box-shadow: 0 4px 16px rgba(0,0,0,0.1); cursor: pointer; z-index: 120; border: none; - transition: .16s cubic-bezier(.47,.43,.41,1.35); + transition: transform 0.1s ease-out; }} - /* Floating action button hover effects */ + /* Floating action button hover effects - reduced animation */ .floating-action-button:hover {{ - transform: translateY(-4px) scale(1.10); - box-shadow: 0 10px 32px rgba(0,0,0,0.27); + transform: translateY(-2px) scale(1.05); + box-shadow: 0 6px 20px rgba(0,0,0,0.2); }} /* Expander styling */