Spaces:
Runtime error
Runtime error
| import streamlit as st | |
| import os, json, datetime, hashlib | |
| from langchain_community.vectorstores import FAISS | |
| from langchain_community.embeddings import HuggingFaceEmbeddings | |
| from langchain_groq import ChatGroq | |
| from langchain.chains import LLMChain | |
| from langchain.prompts import PromptTemplate | |
| from gtts import gTTS | |
| from pathlib import Path | |
| from dotenv import load_dotenv | |
| from sentence_transformers import SentenceTransformer, util | |
| import altair as alt | |
| import speech_recognition as sr | |
| from transformers import pipeline | |
| import torch | |
| import pickle | |
| import re | |
| import pandas as pd | |
| # Load environment variables | |
| load_dotenv() | |
| GROQ_API_KEY = os.getenv("GROQ_API_KEY") | |
| CRISIS_KEYWORDS = ["suicide", "kill myself", "end it all", "worthless", "can't go on", "hurt myself", "self harm", "want to disappear", "no reason to live"] | |
| # Initialize session state | |
| if "authenticated" not in st.session_state: | |
| st.session_state.authenticated = False | |
| if "username" not in st.session_state: | |
| st.session_state.username = None | |
| if "is_admin" not in st.session_state: | |
| st.session_state.is_admin = False | |
| if "transcribed_text" not in st.session_state: | |
| st.session_state.transcribed_text = "" | |
| # Admin configuration | |
| ADMIN_USERNAME = os.getenv("ADMIN_USERNAME") # Set in HF Spaces secrets | |
| ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD") # Set in HF Spaces secrets | |
| # --- UI/UX Modifications (Applied globally) --- | |
| def set_background_and_styles(): | |
| st.markdown( | |
| """ | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;600;700&display=swap'); | |
| @import url('https://fonts.googleapis.com/css2?family=Merriweather:wght@300;400;700&display=swap'); | |
| .stApp { | |
| background: linear-gradient(135deg, #e0e7ff 0%, #c6e2ff 50%, #b0c4de 100%); /* Light blueish gradient */ | |
| background-size: cover; | |
| background-position: center; | |
| background-repeat: no-repeat; | |
| background-attachment: fixed; | |
| font-family: 'Montserrat', sans-serif; | |
| color: #333; /* Default text color to dark for light background */ | |
| } | |
| h1, h2, h3, h4, h5, h6, .stMarkdown, label { | |
| font-family: 'Merriweather', serif; | |
| color: #1a237e; /* Darker blue for headings */ | |
| } | |
| /* Ensure text area label is dark */ | |
| .stTextArea > label { | |
| color: #333 !important; | |
| } | |
| .stButton>button { | |
| background-image: linear-gradient(to right, #6a11cb 0%, #2575fc 100%); /* Blue-purple gradient */ | |
| color: white; | |
| border-radius: 8px; | |
| border: none; | |
| padding: 10px 20px; | |
| font-size: 16px; | |
| font-weight: bold; | |
| transition: all 0.2s ease-in-out; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| } | |
| .stButton>button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2); | |
| } | |
| /* Secondary button style for "Explore Opportunities" */ | |
| .secondary-button > button { | |
| background-color: white; | |
| color: #6a11cb; | |
| border: 1px solid #6a11cb; | |
| box-shadow: none; | |
| } | |
| .secondary-button > button:hover { | |
| background-color: #f0f4f8; /* Light hover */ | |
| color: #2575fc; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| } | |
| .stTextInput>div>div>input, .stTextArea>div>div>textarea, .stSelectbox>div>div>div { | |
| border-radius: 8px; | |
| border: 1px solid #b3e5fc; /* Light blue border */ | |
| padding: 10px; | |
| background-color: rgba(255, 255, 255, 0.95); /* Nearly opaque white */ | |
| color: #333; /* Input text color dark */ | |
| } | |
| /* Adjusted stSuccess and stInfo for white text on darker transparent black background */ | |
| .stSuccess { | |
| border-left: 5px solid #28a745; | |
| background-color: rgba(0, 0, 0, 0.6); /* Transparent black */ | |
| color: white; /* Make text white */ | |
| border-radius: 8px; | |
| padding: 10px; | |
| margin-bottom: 10px; | |
| } | |
| .stInfo { | |
| border-left: 5px solid #17a2b8; | |
| background-color: rgba(0, 0, 0, 0.6); /* Transparent black */ | |
| color: white; /* Make text white */ | |
| border-radius: 8px; | |
| padding: 10px; | |
| margin-bottom: 10px; | |
| } | |
| /* Keeping warning/error as original light background for now, as black transparent might not suit warnings well */ | |
| .stWarning { | |
| border-left: 5px solid #ffc107; | |
| background-color: rgba(255, 255, 255, 0.9); | |
| color: #333; | |
| border-radius: 8px; | |
| padding: 10px; | |
| margin-bottom: 10px; | |
| } | |
| .stError { | |
| border-left: 5px solid #dc3545; | |
| background-color: rgba(255, 255, 255, 0.9); | |
| color: #333; | |
| border-radius: 8px; | |
| padding: 10px; | |
| margin-bottom: 10px; | |
| } | |
| /* Custom container for content with blur background - used for auth form */ | |
| .auth-content-container { | |
| background-color: rgba(255, 255, 255, 0.8); /* Slightly transparent white */ | |
| backdrop-filter: blur(8px); /* Blur effect */ | |
| border-radius: 15px; | |
| padding: 30px; | |
| margin: 20px auto; | |
| max-width: 450px; /* Slimmer for auth forms */ | |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); | |
| text-align: center; /* Center content within */ | |
| } | |
| .feature-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); /* Adjusted for more flexibility */ | |
| gap: 20px; | |
| margin-top: 30px; | |
| margin-bottom: 30px; | |
| max-width: 900px; /* Max width for the grid itself */ | |
| margin-left: auto; | |
| margin-right: auto; | |
| } | |
| .feature-item { | |
| background-color: rgba(255, 255, 255, 0.9); | |
| border-radius: 12px; | |
| padding: 20px; | |
| text-align: center; | |
| box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08); | |
| transition: transform 0.2s ease-in-out; | |
| border: 1px solid #e0e0e0; | |
| } | |
| .feature-item:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 8px 15px rgba(0, 0, 0, 0.12); | |
| } | |
| .feature-item .icon-img { | |
| height: 60px; /* Adjust icon size as needed */ | |
| margin-bottom: 10px; | |
| display: block; /* Ensure icon is on its own line */ | |
| margin-left: auto; | |
| margin-right: auto; | |
| } | |
| .feature-item h3 { | |
| font-size: 1.2em; | |
| color: #1a237e; | |
| margin-bottom: 8px; | |
| } | |
| .feature-item p { | |
| font-size: 0.9em; | |
| color: #555; | |
| line-height: 1.5; | |
| } | |
| /* Main app content container */ | |
| .main-app-content-container { | |
| background-color: rgba(255, 255, 255, 0.8); /* Slightly transparent white */ | |
| backdrop-filter: blur(8px); /* Blur effect */ | |
| border-radius: 15px; | |
| padding: 30px; | |
| margin: 20px auto; | |
| max-width: 800px; /* Wider for main app content */ | |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); | |
| color: #333; /* Dark text inside */ | |
| } | |
| /* Ensure headers within main app container are dark */ | |
| .main-app-content-container h1, .main-app-content-container h2, | |
| .main-app-content-container h3, .main-app-content-container h4, | |
| .main-app-content-container h5, .main-app-content-container h6, | |
| .main-app-content-container .stMarkdown, .main-app-content-container label { | |
| color: #1a237e; /* Darker blue for headers in main app */ | |
| } | |
| /* Specifically target text within main-app-content-container to be dark */ | |
| .main-app-content-container p, .main-app-content-container li, .main-app-content-container div { | |
| color: #333; | |
| } | |
| /* Override specific Streamlit elements that don't pick up general styles for main app */ | |
| .main-app-content-container .st-emotion-cache-1jmve6n, /* st.subheader */ | |
| .main-app-content-container .st-emotion-cache-1gcs47q, /* st.text or similar */ | |
| .main-app-content-container .st-emotion-cache-10q7f27, /* st.info text */ | |
| .main-app-content-container .st-emotion-cache-1j0qsvo { /* more specific text */ | |
| color: #333 !important; | |
| } | |
| /* Navbar Styling */ | |
| .navbar { | |
| display: flex; | |
| justify-content: center; /* Center the logo and title */ | |
| align-items: center; | |
| padding: 15px 40px; | |
| background-color: rgba(255, 255, 255, 0.8); /* Slightly transparent white */ | |
| backdrop-filter: blur(5px); | |
| border-bottom: 1px solid rgba(0, 0, 0, 0.05); | |
| box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); | |
| border-radius: 10px; | |
| margin-bottom: 20px; | |
| /* position: relative; Removed absolute positioning as logout button is gone */ | |
| } | |
| .navbar-logo-container { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; /* Center the logo and text */ | |
| flex-grow: 1; /* Allows it to take available space for centering */ | |
| } | |
| .navbar-logo-text { | |
| font-family: 'Merriweather', serif; | |
| font-size: 24px; | |
| font-weight: bold; | |
| color: #1a237e; | |
| margin-left: 10px; /* Space between logo image and text */ | |
| } | |
| .navbar-logo-img { | |
| height: 30px; /* Size of your logo image */ | |
| vertical-align: middle; | |
| } | |
| .navbar-links { /* This block might not be needed if links are removed */ | |
| display: flex; | |
| gap: 30px; | |
| } | |
| .navbar-link { /* This block might not be needed if links are removed */ | |
| font-family: 'Montserrat', sans-serif; | |
| font-size: 16px; | |
| color: #3f51b5; /* Medium blue */ | |
| text-decoration: none; | |
| padding: 5px 10px; | |
| transition: color 0.2s ease-in-out; | |
| cursor: pointer; | |
| } | |
| .navbar-link:hover { /* This block might not be needed if links are removed */ | |
| color: #6a11cb; /* Purple hover */ | |
| } | |
| .navbar-button { /* This block might not be needed if links are removed */ | |
| background-image: linear-gradient(to right, #6a11cb 0%, #2575fc 100%); | |
| color: white; | |
| border-radius: 8px; | |
| padding: 8px 15px; | |
| font-size: 14px; | |
| font-weight: bold; | |
| text-decoration: none; | |
| transition: all 0.2s ease-in-out; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| cursor: pointer; | |
| } | |
| .navbar-button:hover { /* This block might not be needed if links are removed */ | |
| transform: translateY(-1px); | |
| box_shadow: 0 4px 8px rgba(0, 0, 0, 0.15); | |
| } | |
| /* AI Powered Educational Platform banner style */ | |
| .ai-powered-banner { | |
| display: inline-flex; /* Use inline-flex to shrink-wrap content */ | |
| align-items: center; | |
| background-color: rgba(255, 255, 255, 0.7); /* Light, semi-transparent */ | |
| border-radius: 20px; | |
| padding: 8px 15px; | |
| margin-bottom: 20px; | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: #3f51b5; /* Blue text */ | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.05); | |
| text-align: center; /* Ensure text is centered if container grows */ | |
| margin-left: auto; /* Center horizontally */ | |
| margin-right: auto; /* Center horizontally */ | |
| } | |
| .centered-container { | |
| display: flex; | |
| justify-content: center; | |
| width: 100%; | |
| } | |
| /* Metrics section styles (50K+ Students Helped, etc.) */ | |
| .metrics-container { | |
| display: flex; | |
| justify-content: space-around; | |
| flex-wrap: wrap; | |
| margin-top: 40px; | |
| gap: 20px; | |
| /* border: 1px solid red; REMOVED for debugging */ | |
| } | |
| .metric-item { | |
| text-align: center; | |
| background-color: rgba(255, 255, 255, 0.7); | |
| padding: 20px; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.05); | |
| flex: 1; /* Allow items to grow/shrink */ | |
| min-width: 180px; /* Minimum width before wrapping */ | |
| } | |
| .metric-item h2 { | |
| font-size: 32px; | |
| color: #6a11cb; /* Purple */ | |
| margin-bottom: 5px; | |
| } | |
| .metric-item p { | |
| font-size: 16px; | |
| color: #555; | |
| } | |
| .metric-item img { /* Style for icon images if used */ | |
| height: 40px; | |
| margin-bottom: 10px; | |
| } | |
| /* Center plots */ | |
| .st-emotion-cache-1pxazr6 { /* Specific Streamlit container for pyplot */ | |
| display: flex; | |
| justify-content: center; | |
| } | |
| /* Override specific Streamlit elements that don't pick up general styles */ | |
| .st-emotion-cache-1jmve6n { /* This class is often for st.subheader */ | |
| color: #1a237e !important; /* Darker blue */ | |
| } | |
| .st-emotion-cache-1gcs47q { /* This class can be for specific text elements */ | |
| color: #333 !important; | |
| } | |
| .st-emotion-cache-10q7f27 { /* Example for st.info text */ | |
| color: #333 !important; | |
| } | |
| /* Hide the top grey bar */ | |
| header.st-emotion-cache-1gh8zsi { | |
| display: none !important; | |
| } | |
| div.st-emotion-cache-fis6y8 { | |
| padding-top: 0 !important; | |
| } | |
| div.st-emotion-cache-z5inrg { | |
| display: none !important; | |
| } | |
| /* Adjust overall container width */ | |
| .st-emotion-cache-fg4lbf { | |
| max-width: 1000px !important; /* Adjusted for wider layout */ | |
| padding-left: 0 !important; | |
| padding-right: 0 !important; | |
| } | |
| .block-container { | |
| padding-left: 1rem; | |
| padding-right: 1rem; | |
| color: #333; /* Default text color within block-container */ | |
| } | |
| /* Specific style for button alignment */ | |
| /* Updated .button-row to stack and add spacing */ | |
| .button-row { | |
| display: flex; | |
| flex-direction: column; /* Stack buttons vertically */ | |
| align-items: center; /* Center buttons horizontally */ | |
| gap: 20px; /* Space between buttons */ | |
| margin-top: 20px; | |
| width: 100%; /* Ensure row takes full width to center its content */ | |
| } | |
| .button-row > div { /* Target Streamlit column divs within button-row if still used */ | |
| width: 100%; /* Make columns take full width to center buttons */ | |
| display: flex; | |
| justify-content: center; /* Center the buttons inside their columns */ | |
| } | |
| /* Custom footer style */ | |
| .app-footer { | |
| position: fixed; | |
| left: 0; | |
| bottom: 0; | |
| width: 100%; | |
| background-color: #f0f2f6; /* Streamlit's default background color */ | |
| color: #31333F; /* Streamlit's default text color */ | |
| text-align: center; | |
| padding: 10px; | |
| font-size: 14px; | |
| border-top: 1px solid #d3d3d3; | |
| z-index: 1000; /* Ensure footer is on top */ | |
| } | |
| /* Adjust app padding to prevent content from being hidden by footer */ | |
| .stApp { | |
| padding-bottom: 50px; /* Approx height of footer */ | |
| } | |
| /* History entry styling */ | |
| .history-entry { | |
| background-color: rgba(255, 255, 255, 0.9); | |
| border-radius: 10px; | |
| padding: 15px; | |
| margin-bottom: 10px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| border: 1px solid #e0e0e0; | |
| } | |
| .history-entry h4 { | |
| color: #2575fc; /* Blue for history entry title */ | |
| margin-top: 0; | |
| margin-bottom: 5px; | |
| } | |
| .history-entry p { | |
| color: #444; | |
| font-size: 0.95em; | |
| } | |
| .history-entry strong { | |
| color: #1a237e; | |
| } | |
| /* New style for output text color from DilBot */ | |
| .dilbot-response-text { | |
| color: black !important; /* Force black text */ | |
| } | |
| </style> | |
| """, | |
| unsafe_allow_html=True | |
| ) | |
| # User management functions | |
| def hash_password(password): | |
| """Hash password using SHA-256 with salt""" | |
| salt = "dilbot_secure_salt_2024" # You can change this | |
| return hashlib.sha256((password + salt).encode()).hexdigest() | |
| def get_secure_users_path(): | |
| """Get path to users file in a hidden directory""" | |
| secure_dir = ".secure_data" | |
| os.makedirs(secure_dir, exist_ok=True) | |
| return os.path.join(secure_dir, "users_encrypted.json") | |
| def load_users(): | |
| """Load users from secure file""" | |
| users_path = get_secure_users_path() | |
| if os.path.exists(users_path): | |
| try: | |
| with open(users_path, "r") as f: | |
| return json.load(f) | |
| except: | |
| return {} | |
| return {} | |
| def save_users(users): | |
| """Save users to secure file""" | |
| users_path = get_secure_users_path() | |
| with open(users_path, "w") as f: | |
| json.dump(users, f, indent=4) | |
| def create_user_directory(username): | |
| """Create user-specific directory structure""" | |
| user_dir = f"users/{username}" | |
| os.makedirs(user_dir, exist_ok=True) | |
| return user_dir | |
| def get_user_file_path(username, filename): | |
| """Get path to user-specific file""" | |
| user_dir = f"users/{username}" | |
| return os.path.join(user_dir, filename) | |
| def signup(username, password, email): | |
| """Register new user""" | |
| users = load_users() | |
| if username in users: | |
| return False, "Username already exists" | |
| email_pattern = r"^[\w\.-]+@[\w\.-]+\.\w+$" | |
| if not re.match(email_pattern, email): | |
| return False, "Invalid email format" | |
| users[username] = { | |
| "password": hash_password(password), | |
| "email": email, | |
| "created_at": str(datetime.datetime.now()) | |
| } | |
| save_users(users) | |
| create_user_directory(username) | |
| return True, "Account created successfully!" | |
| def login(username, password): | |
| """Authenticate user or admin""" | |
| # Check if admin login | |
| if username == ADMIN_USERNAME and password == ADMIN_PASSWORD: | |
| return True, "Admin login successful!", True | |
| # Regular user login | |
| users = load_users() | |
| if username not in users: | |
| return False, "User not found.Please signup.", False | |
| if users[username]["password"] == hash_password(password): | |
| return True, "Login successful!", False | |
| return False, "Incorrect password", False | |
| # Emotion detection | |
| def load_emotion_model(): | |
| return pipeline( | |
| "text-classification", | |
| model="j-hartmann/emotion-english-distilroberta-base", | |
| top_k=1, | |
| device=-1 | |
| ) | |
| def detect_emotion(text): | |
| emotion_pipeline = load_emotion_model() | |
| prediction = emotion_pipeline(text)[0][0] | |
| return prediction['label'].lower(), prediction['score'] | |
| # Authentication UI | |
| def show_auth_page(): | |
| set_background_and_styles() | |
| st.markdown( | |
| """ | |
| <div class="navbar"> | |
| <div class="navbar-logo-container"> | |
| <img src="https://e7.pngegg.com/pngimages/498/917/png-clipart-computer-icons-desktop-chatbot-icon-blue-angle-thumbnail.png" class="navbar-logo-img"> | |
| <div class="navbar-logo-text">DilBot</div> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True | |
| ) | |
| st.markdown("<div class='centered-container'><div class='ai-powered-banner'><span>✨</span>AI-Powered Emotional Companion</div></div>", unsafe_allow_html=True) | |
| st.markdown( | |
| """ | |
| <h1 style='text-align: center; font-size: 3.5em; color: #1a237e;'>Transform Your Emotional Journey</h1> | |
| <p style='text-align: center; font-size: 1.2em; color: #555;'> | |
| DilBot empowers individuals with AI-driven insights for emotional well-being, personal growth, and self-discovery worldwide. | |
| </p> | |
| """, unsafe_allow_html=True | |
| ) | |
| st.markdown('<div class="button-row">', unsafe_allow_html=True) | |
| # Buttons placed directly in a single column layout | |
| if st.button("Paid Plan", key="paid_plan_btn", use_container_width=True): | |
| st.info("Paid version coming soon") # Text for paid plan button | |
| st.markdown('<div class="secondary-button" style="margin-top: 20px;">', unsafe_allow_html=True) # Added margin for spacing | |
| if st.button("Login Below FOR FREE", key="login_free_btn", use_container_width=True): | |
| st.info("Slide down below") # Text for free login button | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) # Close button-row | |
| # --- Section for icons and descriptions --- | |
| st.markdown("<div class='feature-grid'>", unsafe_allow_html=True) | |
| # First Image: Users vast amount satisfied (https://cdn-icons-png.flaticon.com/512/33/33308.png) | |
| st.markdown(f""" | |
| <div class="feature-item"> | |
| <img src="https://cdn-icons-png.flaticon.com/512/33/33308.png" class="icon-img"> | |
| <h3>Users Vastly Satisfied</h3> | |
| <p>Our community experiences significant positive emotional shifts.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Second Image: Successful User's Emotion's results a happy lifestyle (https://cdn-icons-png.flaticon.com/512/10809/10809501.png) | |
| st.markdown(f""" | |
| <div class="feature-item"> | |
| <img src="https://cdn-icons-png.flaticon.com/512/10809/10809501.png" class="icon-img"> | |
| <h3>Happy Lifestyle</h3> | |
| <p>Empowering users to achieve emotional well-being and a happier life.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Third Image: 10,000+ Daily Users (https://www.clipartmax.com/png/middle/225-2254363_checklist-comments-form-approved-icon.png) | |
| st.markdown(f""" | |
| <div class="feature-item"> | |
| <img src="https://www.clipartmax.com/png/middle/225-2254363_checklist-comments-form-appr.png" class="icon-img"> | |
| <h3>10,000+ Daily Users</h3> | |
| <p>Join our growing community finding support and growth daily.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Re-adding the original Empathetic Chatbot feature from previous response for completeness | |
| st.markdown(""" | |
| <div class="feature-item"> | |
| <span class="icon-img"></span> <h3>Empathetic Chatbot</h3> | |
| <p>Connect with an AI that listens and understands your feelings.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # --- End of icon and description section --- | |
| st.markdown("<div class='auth-content-container'>", unsafe_allow_html=True) # Container for login/signup | |
| tab1, tab2 = st.tabs(["Login", "Sign Up"]) | |
| with tab1: | |
| st.subheader("Login to Your Account( Admin will use own creditionals )") | |
| login_username = st.text_input("Username", key="login_user") | |
| login_password = st.text_input("Password", type="password", key="login_pass") | |
| if st.button("Login", key="login_btn"): | |
| if login_username and login_password: | |
| success, message, is_admin = login(login_username, login_password) | |
| if success: | |
| st.session_state.authenticated = True | |
| st.session_state.username = login_username | |
| st.session_state.is_admin = is_admin | |
| st.session_state.page = "main_app" if not is_admin else "admin_dashboard" # Direct to correct page | |
| st.rerun() | |
| else: | |
| st.error(message) | |
| else: | |
| st.warning("Please fill in all fields") | |
| with tab2: | |
| st.subheader("Create New Account") | |
| signup_username = st.text_input("Choose Username", key="signup_user") | |
| signup_email = st.text_input("Email Address", key="signup_email") | |
| signup_password = st.text_input("Choose Password", type="password", key="signup_pass") | |
| signup_confirm = st.text_input("Confirm Password", type="password", key="signup_confirm") | |
| if st.button("Create Account", key="signup_btn"): | |
| if all([signup_username, signup_email, signup_password, signup_confirm]): | |
| if signup_password != signup_confirm: | |
| st.error("Passwords don't match!") | |
| elif len(signup_password) < 6: | |
| st.error("Password must be at least 6 characters long!") | |
| else: | |
| success, message = signup(signup_username, signup_password, signup_email) | |
| if success: | |
| st.success(message) | |
| st.info("You can now login with your credentials!") | |
| else: | |
| st.error(message) | |
| else: | |
| st.warning("Please fill in all fields") | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # Main app functions | |
| def build_user_vectorstore(username, quotes): | |
| """Build and save user-specific vectorstore""" | |
| embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2") | |
| vectorstore = FAISS.from_texts(quotes, embedding=embeddings) | |
| # Save vectorstore for user | |
| vectorstore_path = get_user_file_path(username, "vectorstore") | |
| vectorstore.save_local(vectorstore_path) | |
| return vectorstore | |
| def load_user_vectorstore(username): | |
| """Load user-specific vectorstore""" | |
| vectorstore_path = get_user_file_path(username, "vectorstore") | |
| if os.path.exists(vectorstore_path): | |
| embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2") | |
| return FAISS.load_local(vectorstore_path, embeddings, allow_dangerous_deserialization=True) | |
| return None | |
| def save_user_journal(username, user_input, emotion, score, response): | |
| """Save journal entry for specific user""" | |
| journal_path = get_user_file_path(username, "journal.json") | |
| entry = { | |
| "date": str(datetime.date.today()), | |
| "timestamp": str(datetime.datetime.now()), | |
| "user_input": user_input, | |
| "emotion": emotion, | |
| "confidence": round(score * 100, 2), | |
| "response": response | |
| } | |
| journal = [] | |
| if os.path.exists(journal_path): | |
| with open(journal_path, "r") as f: | |
| journal = json.load(f) | |
| journal.append(entry) | |
| with open(journal_path, "w") as f: | |
| json.dump(journal, f, indent=4) | |
| def load_user_journal(username): | |
| """Load journal for specific user""" | |
| journal_path = get_user_file_path(username, "journal.json") | |
| if os.path.exists(journal_path): | |
| with open(journal_path, "r") as f: | |
| return json.load(f) | |
| return [] | |
| def get_admin_stats(): | |
| """Get comprehensive admin statistics""" | |
| users = load_users() | |
| stats = { | |
| "total_users": len(users), | |
| "users_today": 0, | |
| "users_this_week": 0, | |
| "total_conversations": 0, | |
| "active_users": 0, | |
| "user_details": [] | |
| } | |
| today = datetime.date.today() | |
| week_ago = today - datetime.timedelta(days=7) | |
| for username, user_data in users.items(): | |
| created_date = datetime.datetime.fromisoformat(user_data["created_at"]).date() | |
| # Count registrations | |
| if created_date == today: | |
| stats["users_today"] += 1 | |
| if created_date >= week_ago: | |
| stats["users_this_week"] += 1 | |
| # Get user journal stats | |
| journal_data = load_user_journal(username) | |
| conversation_count = len(journal_data) | |
| stats["total_conversations"] += conversation_count | |
| if conversation_count > 0: | |
| stats["active_users"] += 1 | |
| last_activity = journal_data[-1]["date"] if journal_data else "Never" | |
| else: | |
| last_activity = "Never" | |
| # Get emotion breakdown | |
| emotions = [entry['emotion'] for entry in journal_data] | |
| emotion_counts = {} | |
| for emotion in emotions: | |
| emotion_counts[emotion] = emotion_counts.get(emotion, 0) + 1 | |
| most_common_emotion = max(emotion_counts, key=emotion_counts.get) if emotion_counts else "None" | |
| stats["user_details"].append({ | |
| "username": username, | |
| "email": user_data["email"], | |
| "joined": created_date.strftime("%Y-%m-%d"), | |
| "conversations": conversation_count, | |
| "last_activity": last_activity, | |
| "most_common_emotion": most_common_emotion.capitalize(), | |
| "emotions_breakdown": emotion_counts | |
| }) | |
| return stats | |
| def log_admin_activity(action, details=""): | |
| """Log admin activities""" | |
| admin_log_path = "data/admin_log.json" # Persistent and visible | |
| log_entry = { | |
| "timestamp": str(datetime.datetime.now()), | |
| "action": action, | |
| "details": details, | |
| "admin": st.session_state.username | |
| } | |
| admin_log = [] | |
| if os.path.exists(admin_log_path): | |
| with open(admin_log_path, "r") as f: | |
| admin_log = json.load(f) | |
| admin_log.append(log_entry) | |
| # Keep only last 100 entries | |
| admin_log = admin_log[-100:] | |
| os.makedirs("data", exist_ok=True) | |
| with open(admin_log_path, "w") as f: | |
| json.dump(admin_log, f, indent=4) | |
| def get_admin_logs(): | |
| """Get admin activity logs""" | |
| admin_log_path = "data/admin_log.json" | |
| if os.path.exists(admin_log_path): | |
| with open(admin_log_path, "r") as f: | |
| return json.load(f) | |
| return [] | |
| def is_crisis(text): | |
| """Check for crisis keywords""" | |
| return any(phrase in text.lower() for phrase in CRISIS_KEYWORDS) | |
| def show_admin_dashboard(): | |
| """Admin dashboard for monitoring users and app usage""" | |
| st.set_page_config(page_title="DilBot Admin Dashboard", page_icon="👑", layout="wide") | |
| # --- ENHANCED CUSTOM CSS FOR ADMIN DASHBOARD (Consistent with main app) --- | |
| st.markdown(""" | |
| <style> | |
| /* Global Styles & Background (Consistent with main app) */ | |
| .stApp { | |
| background: linear-gradient(to bottom right, #f8f9fa, #e9ecef); | |
| color: #343a40; | |
| font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| -webkit-font-smoothing: antialiased; | |
| padding-top: 2rem; | |
| } | |
| /* Streamlit's main block container for content centering and width */ | |
| .main .block-container { | |
| max-width: 1200px; | |
| padding-top: 2rem; | |
| padding-bottom: 2rem; | |
| padding-left: 1rem; | |
| padding-right: 1rem; | |
| } | |
| /* Hide Streamlit's header/footer if you want a fully custom layout */ | |
| .stApp > header { | |
| display: none; | |
| } | |
| .stApp > footer { | |
| display: none; | |
| } | |
| /* Header & Titles (Consistent with main app) */ | |
| h1, h2, h3, h4, h5, h6 { | |
| color: #212529; | |
| margin-top: 1.8rem; | |
| margin-bottom: 0.9rem; | |
| font-weight: 700; | |
| } | |
| h1 { | |
| font-size: 2.8em; | |
| font-weight: 800; | |
| color: #5d6dbe; | |
| letter-spacing: -0.8px; | |
| text-shadow: 1px 1px 2px rgba(0,0,0,0.05); | |
| } | |
| .header-tagline { /* Added for general sub-titles */ | |
| font-size: 1.2em; | |
| color: #6c757d; | |
| margin-top: -0.5rem; | |
| margin-bottom: 2.5rem; | |
| font-weight: 400; | |
| } | |
| h2 { | |
| font-size: 2em; | |
| font-weight: 700; | |
| border-bottom: 2px solid #e9ecef; | |
| padding-bottom: 0.7rem; | |
| margin-bottom: 2rem; | |
| color: #343a40; | |
| } | |
| h3 { | |
| font-size: 1.6em; | |
| font-weight: 600; | |
| color: #495057; | |
| margin-top: 2rem; | |
| margin-bottom: 1.2rem; | |
| } | |
| h4 { /* Added for sub-sections */ | |
| font-size: 1.3em; | |
| font-weight: 600; | |
| color: #343a40; | |
| margin-top: 1.5rem; | |
| margin-bottom: 1rem; | |
| } | |
| h5 { | |
| font-size: 1.1em; | |
| font-weight: 600; | |
| color: #495057; | |
| margin-bottom: 1rem; | |
| } | |
| /* Metrics (Consistent with main app) */ | |
| [data-testid="stMetric"] { | |
| background-color: #ffffff; | |
| border: 1px solid #dee2e6; | |
| border-radius: 12px; | |
| padding: 20px; | |
| box-shadow: 0 8px 20px rgba(0,0,0,0.08); | |
| text-align: center; | |
| margin-bottom: 1.5rem; | |
| transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; | |
| } | |
| [data-testid="stMetric"]:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 12px 30px rgba(0,0,0,0.12); | |
| } | |
| [data-testid="stMetricLabel"] { | |
| font-size: 1em; | |
| color: #6c757d; | |
| margin-bottom: 8px; | |
| font-weight: 500; | |
| } | |
| [data-testid="stMetricValue"] { /* Overriding the admin-specific green with main app blue */ | |
| font-size: 2.8em; | |
| font-weight: 800; | |
| color: #5d6dbe; /* Main app's primary blue */ | |
| } | |
| /* Buttons (Consistent with main app) */ | |
| .stButton>button { | |
| background-color: #5d6dbe; | |
| color: white; | |
| border: none; | |
| border-radius: 10px; | |
| padding: 12px 25px; | |
| font-size: 1.1em; | |
| font-weight: bold; | |
| transition: background-color 0.3s ease, transform 0.2s ease, box-shadow 0.3s ease; | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.1); | |
| cursor: pointer; | |
| letter-spacing: 0.5px; | |
| } | |
| .stButton>button:hover { | |
| background-color: #4a5c9d; | |
| transform: translateY(-3px); | |
| box-shadow: 0 8px 20px rgba(0,0,0,0.15); | |
| } | |
| .stButton>button:active { | |
| background-color: #3b4b80; | |
| transform: translateY(0); | |
| box-shadow: 0 3px 10px rgba(0,0,0,0.1); | |
| } | |
| /* Logout button specific style */ | |
| [key="admin_logout"] > button { | |
| background-color: #dc3545; | |
| box-shadow: 0 5px 15px rgba(220,53,69,0.2); | |
| } | |
| [key="admin_logout"] > button:hover { | |
| background-color: #c82333; | |
| box-shadow: 0 8px 20px rgba(220,53,69,0.3); | |
| } | |
| /* "Back to Main App" button style */ | |
| [key="admin_back_btn"] > button { | |
| background-color: #6c757d; /* Grey for back button */ | |
| box-shadow: 0 5px 15px rgba(108,117,125,0.2); | |
| } | |
| [key="admin_back_btn"] > button:hover { | |
| background-color: #5a6268; | |
| box-shadow: 0 8px 20px rgba(108,117,125,0.3); | |
| } | |
| /* Reset Data buttons for users */ | |
| .stButton > button[kind="secondary"] { /* Target buttons that are not primary, e.g., Reset/Confirm */ | |
| background-color: #ffc107; /* Warning yellow */ | |
| color: #343a40; /* Dark text on yellow */ | |
| border: none; | |
| } | |
| .stButton > button[kind="secondary"]:hover { | |
| background-color: #e0a800; /* Darker yellow on hover */ | |
| transform: translateY(-2px); | |
| } | |
| .stButton > button[key*="confirm_reset_"] { | |
| background-color: #dc3545; /* Danger red for confirm reset */ | |
| color: white; | |
| } | |
| .stButton > button[key*="confirm_reset_"]:hover { | |
| background-color: #c82333; | |
| } | |
| .stButton > button[key*="cancel_reset_"] { | |
| background-color: #6c757d; /* Grey for cancel */ | |
| color: white; | |
| } | |
| .stButton > button[key*="cancel_reset_"]:hover { | |
| background-color: #5a6268; | |
| } | |
| /* Text Inputs and Text Areas (Consistent with main app) */ | |
| .stTextInput>div>div>input, .stTextArea>div>div>textarea, .stNumberInput>div>div>input { | |
| border-radius: 10px; | |
| border: 1px solid #ced4da; | |
| padding: 14px 18px; | |
| font-size: 1.05em; | |
| color: #343a40; | |
| background-color: #000000; | |
| box-shadow: inset 0 2px 5px rgba(0,0,0,0.03); | |
| transition: border-color 0.3s ease, box-shadow 0.3s ease; | |
| } | |
| .stTextInput>div>div>input:focus, .stTextArea>div>div>textarea:focus, .stNumberInput>div>div>input:focus { | |
| border-color: #5d6dbe; | |
| box-shadow: 0 0 0 0.25rem rgba(93,109,190,0.25); | |
| outline: none; | |
| } | |
| .stTextInput>label, .stTextArea>label, .stNumberInput>label { | |
| font-weight: 600; | |
| color: #495057; | |
| margin-bottom: 0.6rem; | |
| } | |
| /* Information, Success, Error, Warning Boxes (Consistent with main app) */ | |
| [data-testid="stAlert"] { | |
| border-radius: 10px; | |
| padding: 18px 25px; | |
| margin-bottom: 1.8rem; | |
| font-size: 1.05em; | |
| line-height: 1.6; | |
| text-align: left; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.05); | |
| } | |
| [data-testid="stAlert"] .streamlit-warning { | |
| background-color: #fef7e0; color: #7a5f00; border-left: 6px solid #ffcc00; | |
| } | |
| [data-testid="stAlert"] .streamlit-success { | |
| background-color: #e6ffe6; | |
| border-left: 6px solid #4CAF50; | |
| color: black !important; | |
| } | |
| [data-testid="stAlert"] .streamlit-success p, | |
| [data-testid="stAlert"] .streamlit-success span, | |
| [data-testid="stAlert"] .streamlit-success div, | |
| [data-testid="stAlert"] .streamlit-success strong { | |
| color: black !important; | |
| -webkit-text-fill-color: black !important; | |
| opacity: 1 !important; | |
| } | |
| [data-testid="stAlert"] .streamlit-error { | |
| background-color: #ffe6e6; color: #8c0a0a; border-left: 6px solid #e74c3c; | |
| } | |
| .stInfo { /* Ensure st.info is also styled consistently */ | |
| background-color: #e6f7ff; | |
| border-left: 6px solid #64b5f6; | |
| color: black !important; | |
| border-radius: 10px; | |
| padding: 18px 25px; | |
| margin-top: 2rem; | |
| font-style: italic; | |
| font-size: 1.05em; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.05); | |
| } | |
| .stInfo p, .stInfo span, .stInfo strong, .stInfo div { | |
| color: black !important; | |
| -webkit-text-fill-color: black !important; | |
| opacity: 1 !important; | |
| } | |
| /* Container for sections (Consistent with main app) */ | |
| .stContainer { | |
| border-radius: 12px; | |
| box-shadow: 0 8px 20px rgba(0,0,0,0.08); | |
| border: 1px solid #e0e0e0; | |
| padding: 2rem; | |
| margin-bottom: 2.5rem; | |
| background-color: #ffffff; | |
| } | |
| .stContainer.has-border { | |
| border: 1px solid #e0e0e0; | |
| } | |
| /* Expander styling (Consistent with main app) */ | |
| .streamlit-expanderHeader { | |
| background-color: #ffffff; | |
| border: 1px solid #e0e0e0; | |
| border-radius: 10px; | |
| padding: 12px 18px; | |
| margin-bottom: 0.8rem; | |
| cursor: pointer; | |
| transition: background-color 0.2s ease, box-shadow 0.2s ease; | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.05); | |
| } | |
| .streamlit-expanderHeader:hover { | |
| background-color: #f7f9fb; | |
| box-shadow: 0 6px 15px rgba(0,0,0,0.08); | |
| } | |
| .streamlit-expanderHeader > div > div > p { | |
| font-weight: 600; | |
| color: #343a40; | |
| font-size: 1.05em; | |
| } | |
| .streamlit-expanderContent { | |
| background-color: #f8f9fa; | |
| border-left: 4px solid #ced4da; | |
| padding: 15px 20px; | |
| border-bottom-left-radius: 10px; | |
| border-bottom-right-radius: 10px; | |
| margin-top: -10px; | |
| box-shadow: inset 0 2px 5px rgba(0,0,0,0.03); | |
| line-height: 1.6; | |
| } | |
| /* Ensure text within expander content is black */ | |
| .streamlit-expanderContent p, | |
| .streamlit-expanderContent span, | |
| .streamlit-expanderContent div, | |
| .streamlit-expanderContent strong { | |
| color: black !important; | |
| -webkit-text-fill-color: black !important; | |
| opacity: 1 !important; | |
| } | |
| /* Table styling (for dataframes) */ | |
| .stDataFrame { | |
| border: 1px solid #e9ecef; | |
| border-radius: 10px; | |
| overflow: hidden; /* Ensures rounded corners */ | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.05); | |
| } | |
| /* General text color in case Streamlit overrides */ | |
| div[data-testid="stVerticalBlock"] div > p, | |
| div[data-testid="stHorizontalBlock"] div > p, | |
| div[data-testid="stText"] p { | |
| color: #343a40 !important; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # --- END CUSTOM CSS --- | |
| # Header | |
| col1, col2 = st.columns([4, 1]) | |
| with col1: | |
| st.markdown(f"<h1>👑 DilBot Admin Dashboard</h1>", unsafe_allow_html=True) | |
| st.markdown("<p class='header-tagline'>Monitor users, conversations, and app analytics</p>", unsafe_allow_html=True) | |
| with col2: | |
| st.markdown("<div style='height: 40px;'></div>", unsafe_allow_html=True) # Spacer | |
| if st.button("Logout", key="admin_logout", use_container_width=True): | |
| st.session_state.authenticated = False | |
| st.session_state.username = None | |
| st.session_state.is_admin = False | |
| st.rerun() | |
| # Log admin access | |
| log_admin_activity("Dashboard Access", "Viewed admin dashboard") | |
| # Get statistics | |
| stats = get_admin_stats() | |
| # Overview metrics | |
| st.markdown("<h2> Overview</h2>", unsafe_allow_html=True) | |
| with st.container(border=True): # Wrap metrics in a container for styling | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.metric("Total Users", stats["total_users"]) | |
| with col2: | |
| st.metric("Active Users", stats["active_users"]) | |
| with col3: | |
| st.metric("New Today", stats["users_today"]) | |
| with col4: | |
| st.metric("Total Conversations", stats["total_conversations"]) | |
| # User registration trend | |
| st.markdown("<h2> User Registration Trend</h2>", unsafe_allow_html=True) | |
| with st.container(border=True): # Wrap chart in a container | |
| if stats["user_details"]: | |
| # Create registration data | |
| reg_data = {} | |
| for user in stats["user_details"]: | |
| date = user["joined"] | |
| reg_data[date] = reg_data.get(date, 0) + 1 | |
| # Convert to DataFrame for Altair compatibility and sorting | |
| chart_data_list = [{"date": date, "registrations": count} for date, count in reg_data.items()] | |
| chart_df = pd.DataFrame(chart_data_list).sort_values("date") | |
| if not chart_df.empty: | |
| chart = alt.Chart(chart_df).mark_line(point=True).encode( | |
| x=alt.X('date:T', title='Date'), | |
| y=alt.Y('registrations:Q', title='New Registrations'), | |
| tooltip=[alt.Tooltip('date:T', title='Date'), 'registrations:Q'] | |
| ).properties( | |
| title="Daily User Registrations" | |
| ).interactive() # Make interactive for zoom/pan | |
| st.altair_chart(chart, use_container_width=True) | |
| else: | |
| st.info("No registration data available to display trend.") | |
| else: | |
| st.info("No user data available to display registration trend.") | |
| # Detailed user table | |
| st.markdown("<h2>👥 User Details</h2>", unsafe_allow_html=True) | |
| with st.container(border=True): # Wrap user details section in a container | |
| # Search and filter | |
| col1, col2 = st.columns([2, 1]) | |
| with col1: | |
| search_term = st.text_input(" Search users", placeholder="Search by username or email", label_visibility="visible") | |
| with col2: | |
| min_conversations = st.number_input("Min conversations", min_value=0, value=0, label_visibility="visible") | |
| # Filter users | |
| filtered_users = stats["user_details"] | |
| if search_term: | |
| filtered_users = [u for u in filtered_users if | |
| search_term.lower() in u["username"].lower() or | |
| search_term.lower() in u["email"].lower()] | |
| if min_conversations > 0: | |
| filtered_users = [u for u in filtered_users if u["conversations"] >= min_conversations] | |
| # Display user table | |
| if filtered_users: | |
| for user in filtered_users: | |
| with st.expander(f"👤 **{user['username']}** ({user['conversations']} conversations)"): # Bold username | |
| col1_detail, col2_detail = st.columns(2) # Renamed columns to avoid conflict | |
| with col1_detail: | |
| st.write(f"**Email:** {user['email']}") | |
| st.write(f"**Joined:** {user['joined']}") | |
| st.write(f"**Last Activity:** {user['last_activity']}") | |
| with col2_detail: | |
| st.write(f"**Conversations:** {user['conversations']}") | |
| st.write(f"**Most Common Emotion:** {user['most_common_emotion'].capitalize()}") # Capitalize emotion | |
| # Show emotion breakdown | |
| if user['emotions_breakdown']: | |
| st.write("**Emotion Breakdown:**") | |
| # Convert to DataFrame for a nicer table display | |
| emotions_df = pd.DataFrame([{"Emotion": e.capitalize(), "Count": c} for e, c in user['emotions_breakdown'].items()]) | |
| st.dataframe(emotions_df, hide_index=True, use_container_width=True) | |
| else: | |
| st.info("No emotion data for this user.") | |
| st.markdown("---") # Separator for actions | |
| # Quick actions | |
| col1_actions, col2_actions, col3_actions = st.columns(3) # Renamed columns | |
| with col1_actions: | |
| if st.button(f"View Journal", key=f"view_{user['username']}", use_container_width=True): | |
| # Show user's recent conversations | |
| user_journal = load_user_journal(user['username']) | |
| if user_journal: | |
| st.subheader(f"Recent conversations for {user['username']}") | |
| for entry in user_journal[-5:]: # Last 5 activities | |
| st.text_area( | |
| f"{entry['date']} - {entry['emotion'].capitalize()} ({round(entry['confidence'] * 100)}/ confidence)", | |
| f"User: {entry['user_input']}\nDilBot: {entry['response']}", | |
| height=100, | |
| disabled=True, | |
| key=f"journal_entry_{user['username']}_{entry['date']}_{hash(entry['user_input'])}" # Unique key for text_area | |
| ) | |
| else: | |
| st.info("No conversations found for this user.") | |
| with col2_actions: | |
| reset_key = f"reset_{user['username']}" | |
| confirm_key = f"confirm_{user['username']}" | |
| # Use a persistent state for confirmation | |
| if confirm_key not in st.session_state: | |
| st.session_state[confirm_key] = False | |
| if st.button(f"Reset Data", key=reset_key, use_container_width=True): | |
| st.session_state[confirm_key] = True # Flag to show confirmation | |
| st.rerun() # Rerun to display confirmation message/buttons | |
| if st.session_state.get(confirm_key, False): | |
| st.warning(f"Are you sure you want to reset ALL data for {user['username']}? This action is irreversible.") | |
| col_confirm_yes, col_confirm_no = st.columns(2) | |
| with col_confirm_yes: | |
| if st.button(f"Yes, Reset", key=f"confirm_reset_{user['username']}", use_container_width=True): | |
| # Clear user's journal | |
| journal_path = get_user_file_path(user['username'], "journal.json") | |
| if os.path.exists(journal_path): | |
| os.remove(journal_path) | |
| # Also remove vectorstore if it exists | |
| vectorstore_path = get_user_file_path(user['username'], "faiss_index") | |
| if os.path.exists(vectorstore_path): | |
| import shutil | |
| shutil.rmtree(vectorstore_path) # Remove directory for FAISS | |
| log_admin_activity("User Data Reset", f"Reset data for {user['username']}") | |
| st.success(f"Data reset for {user['username']}!") | |
| st.session_state[confirm_key] = False # Reset confirmation flag | |
| st.rerun() | |
| with col_confirm_no: | |
| if st.button(f"Cancel", key=f"cancel_reset_{user['username']}", use_container_width=True): | |
| st.session_state[confirm_key] = False # Cancel confirmation | |
| st.info(f"Reset for {user['username']} cancelled.") | |
| st.rerun() # Rerun to hide confirmation buttons | |
| #with col3_actions: | |
| # Placeholder for other actions like "Deactivate User" | |
| #st.button("Deactivate User (WIP)", key=f"deactivate_{user['username']}", disabled=True, use_container_width=True) | |
| else: | |
| st.info("No users found matching your criteria.") | |
| # System Analytics | |
| st.markdown("<h2> System Analytics</h2>", unsafe_allow_html=True) | |
| with st.container(border=True): # Wrap system analytics in a container | |
| col1_analytics, col2_analytics = st.columns(2) # Renamed columns | |
| with col1_analytics: | |
| st.markdown("<h4>Emotion Distribution (All Users)</h4>", unsafe_allow_html=True) | |
| # Aggregate all emotions | |
| all_emotions = {} | |
| for user in stats["user_details"]: | |
| for emotion, count in user['emotions_breakdown'].items(): | |
| all_emotions[emotion] = all_emotions.get(emotion, 0) + count | |
| if all_emotions: | |
| emotion_chart_data = [{"emotion": emotion.capitalize(), "count": count} | |
| for emotion, count in all_emotions.items()] | |
| emotion_chart = alt.Chart(pd.DataFrame(emotion_chart_data)).mark_bar().encode( | |
| x=alt.X('emotion:N', title='Emotion', sort='-y'), # Sort by count descending | |
| y=alt.Y('count:Q', title='Frequency'), | |
| color=alt.Color('emotion:N', legend=None, scale=alt.Scale( | |
| range=['#4CAF50', '#FFC107', '#E74C3C', '#3498DB', '#9B59B6', '#1ABC9C', '#FF5733'])), # More colors | |
| tooltip=['emotion:N', 'count:Q'] | |
| ).properties( | |
| title="Overall Emotion Distribution" | |
| ).interactive() | |
| st.altair_chart(emotion_chart, use_container_width=True) | |
| else: | |
| st.info("No emotion data available for overall analysis.") | |
| with col2_analytics: | |
| st.markdown("<h4>User Activity Levels</h4>", unsafe_allow_html=True) | |
| activity_levels = {"Inactive (0)": 0, "Light (1-5)": 0, "Moderate (6-20)": 0, "Heavy (21+)": 0} | |
| for user in stats["user_details"]: | |
| conv_count = user["conversations"] | |
| if conv_count == 0: | |
| activity_levels["Inactive (0)"] += 1 | |
| elif conv_count <= 5: | |
| activity_levels["Light (1-5)"] += 1 | |
| elif conv_count <= 20: | |
| activity_levels["Moderate (6-20)"] += 1 | |
| else: | |
| activity_levels["Heavy (21+)"] += 1 | |
| activity_data = [{"level": level, "users": count} for level, count in activity_levels.items()] | |
| if activity_data: | |
| activity_chart = alt.Chart(pd.DataFrame(activity_data)).mark_arc(outerRadius=120, innerRadius=80).encode( # Donut chart | |
| theta=alt.Theta('users:Q'), | |
| color=alt.Color('level:N', title="Activity Level", scale=alt.Scale( | |
| range=['#6c757d', '#64b5f6', '#5d6dbe', '#4a5c9d'])), # Consistent color scheme | |
| order=alt.Order('users:Q', sort='descending'), | |
| tooltip=['level:N', 'users:Q'] | |
| ).properties( | |
| title="User Activity Distribution" | |
| ).interactive() | |
| st.altair_chart(activity_chart, use_container_width=True) | |
| else: | |
| st.info("No user activity data to display.") | |
| # Admin logs | |
| st.markdown("<h2> Admin Activity Logs</h2>", unsafe_allow_html=True) | |
| with st.container(border=True): # Wrap admin logs in a container | |
| admin_logs = get_admin_logs() | |
| if admin_logs: | |
| st.subheader("Recent Admin Activities (Last 10)") | |
| # Display logs in a more readable format, perhaps a table or structured text | |
| for log_entry in reversed(admin_logs[-10:]): # Last 10 activities, reversed to show newest first | |
| timestamp = datetime.datetime.fromisoformat(log_entry["timestamp"]) | |
| st.markdown(f"**{timestamp.strftime('%Y-%m-%d %H:%M:%S')}** - **{log_entry['action']}**: `{log_entry['details']}`") | |
| st.markdown("---") # Small separator | |
| else: | |
| st.info("No admin activities logged yet.") | |
| # Data Export and Admin Log Clearing | |
| st.markdown("<h2>Data Management</h2>", unsafe_allow_html=True) | |
| with st.container(border=True): # Wrap data export in a container | |
| col1_export, col2_export = st.columns(2) # Renamed columns | |
| with col1_export: | |
| st.markdown("<h4>Export Application Data</h4>", unsafe_allow_html=True) | |
| if st.button("Export All Data (JSON)", key="export_data_btn", use_container_width=True): | |
| export_data = { | |
| "export_timestamp": str(datetime.datetime.now()), | |
| "statistics": stats, | |
| "admin_logs": admin_logs | |
| } | |
| st.download_button( | |
| label="Download Exported Data", | |
| data=json.dumps(export_data, indent=4), | |
| file_name=f"dilbot_admin_export_{datetime.date.today().isoformat()}.json", | |
| mime="application/json", | |
| key="download_export_btn", | |
| use_container_width=True | |
| ) | |
| log_admin_activity("Data Export", "Initiated data export") | |
| st.success("Data export ready for download!") | |
| with col2_export: | |
| st.markdown("<h4>Clear Admin Activity Logs</h4>", unsafe_allow_html=True) | |
| # Add a confirmation step for clearing logs | |
| clear_log_confirm_key = "clear_log_confirm" | |
| if clear_log_confirm_key not in st.session_state: | |
| st.session_state[clear_log_confirm_key] = False | |
| if st.button("Clear Admin Logs", key="clear_admin_logs_btn", use_container_width=True): | |
| st.session_state[clear_log_confirm_key] = True | |
| st.rerun() | |
| if st.session_state.get(clear_log_confirm_key, False): | |
| st.warning("Are you sure you want to clear ALL admin logs? This action is irreversible.") | |
| col_clear_yes, col_clear_no = st.columns(2) | |
| with col_clear_yes: | |
| if st.button("Yes, Clear Logs", key="confirm_clear_logs_btn", use_container_width=True): | |
| admin_log_path = "data/admin_log.json" | |
| if os.path.exists(admin_log_path): | |
| os.remove(admin_log_path) | |
| st.success("Admin logs cleared successfully!") | |
| log_admin_activity("Admin Logs Cleared", "All admin activity logs were cleared") | |
| st.session_state[clear_log_confirm_key] = False | |
| st.rerun() | |
| with col_clear_no: | |
| if st.button("Cancel", key="cancel_clear_logs_btn", use_container_width=True): | |
| st.session_state[clear_log_confirm_key] = False | |
| st.info("Clearing admin logs cancelled.") | |
| st.rerun() | |
| st.markdown("---") | |
| # Back to main app button (aligned left in a container) | |
| st.markdown("<p class='footer-caption'>DilBot Admin Panel | Built by Members of CSG Hackathon Team</p>", unsafe_allow_html=True) | |
| def speak(text, username): | |
| """Generate and play audio response""" | |
| tts = gTTS(text=text, lang='en') | |
| audio_path = get_user_file_path(username, "response.mp3") | |
| tts.save(audio_path) | |
| st.audio(audio_path, format="audio/mp3") | |
| def transcribe_audio_file(uploaded_audio): | |
| """Transcribe uploaded audio file""" | |
| recognizer = sr.Recognizer() | |
| try: | |
| with sr.AudioFile(uploaded_audio) as source: | |
| audio_data = recognizer.record(source) | |
| text = recognizer.recognize_google(audio_data) | |
| return text | |
| except Exception as e: | |
| return f"Error: {str(e)}" | |
| def show_main_app(): | |
| """Main DilBot application""" | |
| username = st.session_state.username | |
| st.set_page_config(page_title="DilBot - Emotional AI", page_icon="🧠", layout="wide") | |
| quote_categories = { | |
| "Grief": ["Grief is the price we pay for love.", "Tears are the silent language of grief.", "What we have once enjoyed we can never lose; all that we love deeply becomes a part of us."], | |
| "Motivation": ["Believe in yourself and all that you are.", "Tough times never last, but tough people do.", "The only way to do great work is to love what you do."], | |
| "Healing": ["Every wound has its own time to heal.", "It's okay to take your time to feel better.", "Healing is not linear, and that's perfectly okay."], | |
| "Relationships": ["The best relationships are built on trust.", "Love is not about possession but appreciation.", "Healthy relationships require both people to show up authentically."] | |
| } | |
| # --- CUSTOM CSS --- | |
| st.markdown(""" | |
| <style> | |
| /* Global Styles & Background */ | |
| .stApp { | |
| background: linear-gradient(to bottom right, #f8f9fa, #e9ecef); | |
| color: #343a40; | |
| font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| -webkit-font-smoothing: antialiased; | |
| padding-top: 2rem; | |
| } | |
| /* Streamlit's main block container for content centering and width */ | |
| .main .block-container { | |
| max-width: 1200px; | |
| padding-top: 2rem; | |
| padding-bottom: 2rem; | |
| padding-left: 1rem; | |
| padding-right: 1rem; | |
| } | |
| /* Hide Streamlit's header/footer if you want a fully custom layout */ | |
| .stApp > header { | |
| display: none; | |
| } | |
| .stApp > footer { | |
| display: none; | |
| } | |
| /* Header & Titles */ | |
| h1, h2, h3, h4, h5, h6 { | |
| color: #212529; | |
| margin-top: 1.8rem; | |
| margin-bottom: 0.9rem; | |
| font-weight: 700; | |
| } | |
| h1 { | |
| font-size: 2.8em; | |
| font-weight: 800; | |
| color: #5d6dbe; | |
| letter-spacing: -0.8px; | |
| text-shadow: 1px 1px 2px rgba(0,0,0,0.05); | |
| } | |
| .header-tagline { | |
| font-size: 1.2em; | |
| color: #6c757d; | |
| margin-top: -0.5rem; | |
| margin-bottom: 2.5rem; | |
| font-weight: 400; | |
| } | |
| h2 { | |
| font-size: 2em; | |
| font-weight: 700; | |
| border-bottom: 2px solid #e9ecef; | |
| padding-bottom: 0.7rem; | |
| margin-bottom: 2rem; | |
| color: #343a40; | |
| } | |
| h3 { | |
| font-size: 1.6em; | |
| font-weight: 600; | |
| color: #495057; | |
| margin-top: 2rem; | |
| margin-bottom: 1.2rem; | |
| } | |
| h5 { | |
| font-size: 1.1em; | |
| font-weight: 600; | |
| color: #495057; | |
| margin-bottom: 1rem; | |
| } | |
| /* Metrics (Total Conversations, Most Common Emotion, Avg. Confidence) */ | |
| [data-testid="stMetric"] { | |
| background-color: #ffffff; | |
| border: 1px solid #dee2e6; | |
| border-radius: 12px; | |
| padding: 20px; | |
| box-shadow: 0 8px 20px rgba(0,0,0,0.08); | |
| text-align: center; | |
| margin-bottom: 1.5rem; | |
| transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; | |
| } | |
| [data-testid="stMetric"]:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 12px 30px rgba(0,0,0,0.12); | |
| } | |
| [data-testid="stMetricLabel"] { | |
| font-size: 1em; | |
| color: #6c757d; | |
| margin-bottom: 8px; | |
| font-weight: 500; | |
| } | |
| [data-testid="stMetricValue"] { | |
| font-size: 2.8em; | |
| font-weight: 800; | |
| color: #5d6dbe; | |
| } | |
| /* Buttons */ | |
| .stButton>button { | |
| background-color: #5d6dbe; | |
| color: black; | |
| border: none; | |
| border-radius: 10px; | |
| padding: 12px 25px; | |
| font-size: 1.1em; | |
| font-weight: bold; | |
| transition: background-color 0.3s ease, transform 0.2s ease, box-shadow 0.3s ease; | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.1); | |
| cursor: pointer; | |
| letter-spacing: 0.5px; | |
| } | |
| .stButton>button:hover { | |
| background-color: #4a5c9d; | |
| transform: translateY(-3px); | |
| box-shadow: 0 8px 20px rgba(0,0,0,0.15); | |
| } | |
| .stButton>button:active { | |
| background-color: #3b4b80; | |
| transform: translateY(0); | |
| box-shadow: 0 3px 10px rgba(0,0,0,0.1); | |
| } | |
| /* Logout button specific style */ | |
| [key="logout_btn"] > button { | |
| background-color: #dc3545; | |
| box-shadow: 0 5px 15px rgba(220,53,69,0.2); | |
| } | |
| [key="logout_btn"] > button:hover { | |
| background-color: #c82333; | |
| box-shadow: 0 8px 20px rgba(220,53,69,0.3); | |
| } | |
| /* Text Inputs and Text Areas */ | |
| .stTextInput>div>div>input, .stTextArea>div>div>textarea { | |
| border-radius: 10px; | |
| border: 1px solid #ced4da; | |
| padding: 14px 18px; | |
| font-size: 1.05em; | |
| color: #343a40; | |
| background-color: #fcfcfc; | |
| box-shadow: inset 0 2px 5px rgba(0,0,0,0.03); | |
| transition: border-color 0.3s ease, box-shadow 0.3s ease; | |
| } | |
| .stTextInput>div>div>input:focus, .stTextArea>div>div>textarea:focus { | |
| border-color: #5d6dbe; | |
| box-shadow: 0 0 0 0.25rem rgba(93,109,190,0.25); | |
| outline: none; | |
| } | |
| .stTextInput>label, .stTextArea>label { | |
| font-weight: 600; | |
| color: #495057; | |
| margin-bottom: 0.6rem; | |
| } | |
| /* Selectbox (Dropdown) Text Color Fixes */ | |
| [data-testid="stSelectbox"] > div:first-child > div:first-child { | |
| border-radius: 10px; | |
| border: 1px solid #ced4da; | |
| background-color: #fcfcfc; | |
| box-shadow: inset 0 2px 5px rgba(0,0,0,0.03); | |
| display: flex; | |
| align-items: center; | |
| min-height: 48px; | |
| padding: 0 10px; | |
| } | |
| .stSelectbox>label { | |
| font-weight: 600; | |
| color: #495057; | |
| margin-bottom: 0.6rem; | |
| } | |
| /* Force selected value text to black */ | |
| [data-testid="stSelectbox"] input[type="text"] { | |
| color: black !important; | |
| -webkit-text-fill-color: black !important; /* For Webkit browsers */ | |
| opacity: 1 !important; | |
| flex-grow: 1; | |
| padding: 12px 5px; | |
| font-size: 1.05em; | |
| line-height: 1.2em; | |
| min-height: 1.2em; | |
| background-color: transparent !important; | |
| border: none !important; | |
| outline: none !important; | |
| box-shadow: none !important; | |
| } | |
| /* Force dropdown arrow to black */ | |
| [data-testid="stSelectbox"] button { | |
| background-color: transparent !important; | |
| border: none !important; | |
| padding: 0 5px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| [data-testid="stSelectbox"] svg { | |
| fill: black !important; | |
| color: black !important; | |
| font-size: 1.5em !important; | |
| width: 1em !important; | |
| height: 1em !important; | |
| } | |
| [data-testid="stSelectbox"] svg path { | |
| fill: black !important; | |
| } | |
| /* File Uploader with White Text */ | |
| [data-testid="stFileUploaderDropzone"] { | |
| border-radius: 12px; | |
| border: 2px dashed #a0a8b4; | |
| background-color: #2c3e50; /* Dark background to contrast with white text */ | |
| padding: 25px; | |
| transition: border-color 0.3s ease, background-color 0.3s ease; | |
| margin-bottom: 1.5rem; | |
| } | |
| [data-testid="stFileUploaderDropzone"]:hover { | |
| border-color: #5d6dbe; | |
| background-color: #34495e; /* Slightly lighter on hover */ | |
| } | |
| /* Force ALL text inside file uploader dropzone to WHITE */ | |
| [data-testid="stFileUploaderDropzone"] *, | |
| [data-testid="stFileUploaderDropzone"] p, | |
| [data-testid="stFileUploaderDropzone"] span, | |
| [data-testid="stFileUploaderDropzone"] div, | |
| [data-testid="stFileUploaderDropzone"] small { | |
| color: white !important; | |
| -webkit-text-fill-color: white !important; | |
| opacity: 1 !important; | |
| font-weight: normal !important; | |
| } | |
| /* Specific targeting for drag and drop text */ | |
| [data-testid="stFileUploaderDropzone"] > div > div > div:nth-child(2) > div:first-child { | |
| color: white !important; | |
| } | |
| [data-testid="stFileUploaderDropzone"] > div > div > div > span { | |
| color: white !important; | |
| } | |
| /* Style the Browse files button with white text */ | |
| [data-testid="stFileUploaderDropzone"] button { | |
| background-color: #5d6dbe !important; | |
| color: white !important; | |
| border: none !important; | |
| border-radius: 8px !important; | |
| padding: 10px 20px !important; | |
| font-weight: bold !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| [data-testid="stFileUploaderDropzone"] button:hover { | |
| background-color: #4a5c9d !important; | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.3) !important; | |
| } | |
| /* File uploader label (outside the dropzone) */ | |
| [data-testid="stFileUploader"] label { | |
| color: black !important; | |
| -webkit-text-fill-color: black !important; | |
| opacity: 1 !important; | |
| font-weight: 600 !important; | |
| margin-bottom: 0.5rem !important; | |
| } | |
| /* Uploaded file names */ | |
| [data-testid="stFileUploaderFileName"] { | |
| font-size: 0.95em; | |
| color: black !important; | |
| -webkit-text-fill-color: black !important; | |
| margin-top: 10px; | |
| word-break: break-all; | |
| background-color: rgba(255,255,255,0.1) !important; | |
| padding: 8px 12px !important; | |
| border-radius: 6px !important; | |
| } | |
| [data-testid="stFileUploaderFile"] { | |
| background-color: rgba(255,255,255,0.1) !important; | |
| border-radius: 8px; | |
| padding: 8px 12px; | |
| margin-top: 10px; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.05); | |
| } | |
| /* Style the cloud upload icon to be white */ | |
| [data-testid="stFileUploaderDropzone"] svg { | |
| fill: white !important; | |
| color: white !important; | |
| } | |
| /* Information, Success, Error, Warning Boxes */ | |
| [data-testid="stAlert"] { | |
| border-radius: 10px; | |
| padding: 18px 25px; | |
| margin-bottom: 1.8rem; | |
| font-size: 1.05em; | |
| line-height: 1.6; | |
| text-align: left; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.05)); | |
| } | |
| /* Warning alert style */ | |
| [data-testid="stAlert"] .streamlit-warning { | |
| background-color: #fef7e0; color: #7a5f00; border-left: 6px solid #ffcc00; | |
| } | |
| [data-testid="stAlert"] .streamlit-success { | |
| background-color: #9aff9a; | |
| border-left: 6px solid #4CAF50; | |
| color: black !important; | |
| } | |
| /* Aggressive targeting for specific text elements within st.success */ | |
| [data-testid="stAlert"] .streamlit-success p, | |
| [data-testid="stAlert"] .streamlit-success span, | |
| [data-testid="stAlert"] .streamlit-success div, | |
| [data-testid="stAlert"] .streamlit-success strong { /* Added strong for bold text */ | |
| color: black !important; | |
| -webkit-text-fill-color: black !important; | |
| opacity: 1 !important; | |
| } | |
| /* Error alert style */ | |
| [data-testid="stAlert"] .streamlit-error { | |
| background-color: #ffe6e6; color: #8c0a0a; border-left: 6px solid #e74c3c; | |
| } | |
| .stInfo { | |
| background-color: #e6f7ff; | |
| border-left: 6px solid #64b5f6; | |
| color: black !important; /* Changed from black to white */ | |
| border-radius: 10px; | |
| padding: 18px 25px; | |
| margin-top: 2rem; | |
| font-style: italic; | |
| font-size: 1.05em; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.05); | |
| } | |
| /* Aggressive targeting for specific text elements within st.info */ | |
| .stInfo p, | |
| .stInfo span, | |
| .stInfo strong, | |
| .stInfo div { | |
| color: white !important; | |
| -webkit-text-fill-color: white !important; | |
| opacity: 1 !important; | |
| } | |
| /* Chat Message Bubbles */ | |
| .user-message-container { | |
| display: flex; | |
| justify-content: flex-end; | |
| margin-bottom: 15px; | |
| } | |
| .user-message { | |
| background-color: #e0e6f6; | |
| color: #343a40; | |
| border-radius: 18px 18px 5px 18px; | |
| padding: 12px 18px; | |
| max-width: 75%; | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.08); | |
| line-height: 1.5; | |
| text-align: left; | |
| } | |
| .bot-message-container { | |
| display: flex; | |
| justify-content: flex-start; | |
| margin-bottom: 15px; | |
| } | |
| .bot-message { | |
| background-color: #ffffff; | |
| color: #343a40; | |
| border-radius: 18px 18px 18px 5px; | |
| padding: 12px 18px; | |
| max-width: 75%; | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.08); | |
| line-height: 1.5; | |
| text-align: left; | |
| } | |
| .chat-separator { | |
| border-bottom: 1px dashed #ced4da; | |
| margin: 25px 0; | |
| } | |
| .chat-title { | |
| font-size: 1.8em; | |
| font-weight: 700; | |
| color: #5d6dbe; | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| } | |
| /* Audio Player (kept original styles) */ | |
| .stAudio { | |
| margin-top: 1.5rem; | |
| margin-bottom: 2rem; | |
| } | |
| /* Expander styling (for conversations - kept original styles) */ | |
| .streamlit-expanderHeader { | |
| background-color: #ffffff; | |
| border: 1px solid #e0e0e0; | |
| border-radius: 10px; | |
| padding: 12px 18px; | |
| margin-bottom: 0.8rem; | |
| cursor: pointer; | |
| transition: background-color 0.2s ease, box-shadow 0.2s ease; | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.05); | |
| } | |
| .streamlit-expanderHeader:hover { | |
| background-color: #f7f9fb; | |
| box-shadow: 0 6px 15px rgba(0,0,0,0.08); | |
| } | |
| .streamlit-expanderHeader > div > div > p { | |
| font-weight: 600; | |
| color: #343a40; | |
| font-size: 1.05em; | |
| } | |
| .streamlit-expanderContent { | |
| background-color: #f8f9fa; | |
| border-left: 4px solid #ced4da; | |
| padding: 15px 20px; | |
| border-bottom-left-radius: 10px; | |
| border-bottom-right-radius: 10px; | |
| margin-top: -10px; | |
| box-shadow: inset 0 2px 5px rgba(0,0,0,0.03); | |
| line-height: 1.6; | |
| } | |
| /* Horizontal rule (kept original styles) */ | |
| hr { | |
| border-top: 2px solid #e9ecef; | |
| margin-top: 3rem; | |
| margin-bottom: 3rem; | |
| } | |
| /* Caption (footer - kept original styles) */ | |
| .footer-caption { | |
| color: #868e96; | |
| font-size: 0.9em; | |
| margin-top: 3rem; | |
| display: block; | |
| text-align: center; | |
| letter-spacing: 0.2px; | |
| } | |
| /* Container for sections (kept original styles) */ | |
| .stContainer { | |
| border-radius: 12px; | |
| box-shadow: 0 8px 20px rgba(0,0,0,0.08); | |
| border: 1px solid #e0e0e0; | |
| padding: 2rem; | |
| margin-bottom: 2.5rem; | |
| background-color: #ffffff; | |
| } | |
| .stContainer.has-border { | |
| border: 1px solid #e0e0e0; | |
| } | |
| /*confidence color*/ | |
| .black-text { | |
| color: black !important; | |
| -webkit-text-fill-color: black !important; | |
| opacity: 1 !important; | |
| } | |
| .stCustomSuccess { | |
| background-color: #e6ffe6; | |
| border-left: 6px solid #4CAF50; | |
| border-radius: 10px; | |
| padding: 18px 25px; | |
| margin-bottom: 1.8rem; | |
| font-size: 1.05em; | |
| line-height: 1.6; | |
| text-align: left; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.05); | |
| } | |
| .stCustomSuccess .black-text { | |
| color: black !important; | |
| -webkit-text-fill-color: black !important; | |
| opacity: 1 !important; | |
| } | |
| /*for quotes */ | |
| /* Custom CSS for a reusable "info-box" component */ | |
| .custom-info-box { | |
| background-color: #e6f7ff; | |
| border-left: 6px solid #64b5f6; | |
| border-radius: 10px; | |
| padding: 18px 25px; | |
| margin-top: 2rem; | |
| font-size: 1.05em; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.05); | |
| display: flex; | |
| align-items: center; | |
| } | |
| /* Style for the icon within the custom info box */ | |
| .custom-info-box .info-icon { | |
| font-size: 1.5em; | |
| margin-right: 10px; | |
| color: #64b5f6; | |
| } | |
| /* Styles for the text content within the custom info box */ | |
| .custom-info-box .info-text { | |
| color: black !important; | |
| -webkit-text-fill-color: black !important; | |
| opacity: 1 !important; | |
| font-style: italic; /* Apply italic here directly */ | |
| margin: 0; /* Remove default paragraph margin */ | |
| } | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # --- Streamlit UI Components --- | |
| # Header with logout button | |
| col1, col2 = st.columns([4, 1]) | |
| with col1: | |
| st.markdown(f"<h1>🧠 DilBot</h1>", unsafe_allow_html=True) | |
| st.markdown(f"<p class='header-tagline'>Welcome back, {username}! Your personal emotional AI companion</p>", unsafe_allow_html=True) | |
| with col2: | |
| st.markdown("<div style='height: 40px;'></div>", unsafe_allow_html=True) # Spacer | |
| if st.button("Logout", key="logout_btn", use_container_width=True): | |
| st.session_state.authenticated = False | |
| st.session_state.username = None | |
| st.rerun() | |
| # Input section wrapped in a styled container | |
| st.markdown("<h3>Input Your Thoughts</h3>", unsafe_allow_html=True) | |
| with st.container(border=True): # Streamlit's built-in container with border | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| # Reverting to direct st.selectbox with a visible label | |
| st.markdown(" **Choose a quote theme:**") # Custom label for the selectbox | |
| selected_category = st.selectbox("Select Quote Theme", list(quote_categories.keys()), label_visibility="collapsed") # Original label hidden | |
| # The custom markdown above provides the visual label, while the st.selectbox's internal label is hidden. | |
| with col2: | |
| st.markdown("<h5> Custom Quotes & Voice Input</h5>", unsafe_allow_html=True) | |
| # Ensure file uploader labels are visible | |
| uploaded_quotes = st.file_uploader("Upload your own quotes (.txt)", type=["txt"], key="quote_uploader") | |
| uploaded_audio = st.file_uploader("Upload a voice message (.wav)", type=["wav"], key="audio_uploader") | |
| # Voice transcription button | |
| if uploaded_audio and st.button(" Transcribe Voice Message", key="transcribe_btn", use_container_width=True): | |
| with st.spinner("Transcribing your voice..."): | |
| transcribed = transcribe_audio_file(uploaded_audio) | |
| if transcribed.startswith("Error:"): | |
| st.error(transcribed) | |
| else: | |
| st.session_state.transcribed_text = transcribed | |
| st.success(" Voice transcribed successfully!") | |
| # Handle vectorstore | |
| current_quotes = [] | |
| vectorstore = None | |
| if uploaded_quotes: | |
| custom_quotes = uploaded_quotes.read().decode("utf-8").splitlines() | |
| custom_quotes = [quote.strip() for quote in custom_quotes if quote.strip()] | |
| vectorstore = build_user_vectorstore(username, custom_quotes) | |
| current_quotes = custom_quotes | |
| st.success(f" {len(custom_quotes)} custom quotes uploaded and saved!") | |
| else: | |
| default_quotes = quote_categories[selected_category] | |
| vectorstore = load_user_vectorstore(username) | |
| if vectorstore is None: | |
| vectorstore = build_user_vectorstore(username, default_quotes) | |
| current_quotes = default_quotes | |
| # Input area for user message | |
| st.markdown("<h3>What's on your mind?</h3>", unsafe_allow_html=True) | |
| user_input = st.text_area( | |
| "Share your thoughts, feelings, or experiences...", | |
| value=st.session_state.transcribed_text, | |
| height=180, # Increased height for more typing space | |
| placeholder="Type here or use your transcribed voice message...", | |
| ) | |
| # Re-adding explicit label for text area if needed, to be safe | |
| # st.markdown("<label class='stTextArea>label' style='margin-bottom:0.6rem;padding-left:5px;'>Share your thoughts, feelings, or experiences...</label>", unsafe_allow_html=True) | |
| final_input = user_input.strip() or st.session_state.transcribed_text.strip() | |
| # Main interaction button | |
| if st.button("🧠 Talk to DilBot", type="primary", use_container_width=True): | |
| if not final_input: | |
| st.warning(" Please enter something to share or upload a voice message.") | |
| else: | |
| with st.spinner("DilBot is thinking and feeling..."): | |
| # Emotion detection | |
| emotion, score = detect_emotion(final_input) | |
| # Get AI response | |
| prompt_template = PromptTemplate( | |
| input_variables=["context", "user_input", "username"], | |
| template="""You are DilBot, an empathetic emotional support AI companion for {username}. | |
| Use the following emotional quote context to respond gently, supportively, and personally. | |
| Context quotes: | |
| {context} | |
| User's message: | |
| {user_input} | |
| Respond as DilBot with warmth, empathy, and understanding. Keep it conversational and supportive.""" | |
| ) | |
| # Get similar quotes (ORIGINAL LOGIC - NO CHANGE) | |
| similar_docs = vectorstore.similarity_search(final_input, k=2) | |
| context = "\n".join([doc.page_content for doc in similar_docs]) | |
| # Generate response (ORIGINAL LOGIC - NO CHANGE) | |
| groq_llm = ChatGroq(api_key=GROQ_API_KEY, model="llama3-70b-8192") | |
| chain = LLMChain(llm=groq_llm, prompt=prompt_template) | |
| response = chain.run(context=context, user_input=final_input, username=username) | |
| # Save to user's journal (ORIGINAL LOGIC - NO CHANGE) | |
| save_user_journal(username, final_input, emotion, score, response) | |
| # Display results with new chat bubble styling | |
| st.markdown("<h3 class='chat-title'>DilBot's Conversation:</h3>", unsafe_allow_html=True) | |
| with st.container(border=True): # Container for the conversation output | |
| # User's input presented in a chat bubble | |
| st.markdown(f"<div class='user-message-container'><div class='user-message'>You: {final_input}</div></div>", unsafe_allow_html=True) | |
| #st.success(f"**Emotion Detected:** {emotion.capitalize()} ({round(score*100)}/ confidence)") | |
| st.markdown( | |
| f""" | |
| <div class="stCustomSuccess"> | |
| <p class="black-text"> | |
| <strong>Emotion Detected:</strong> {emotion.capitalize()} ({round(score*100)}% confidence)</p></div> """,unsafe_allow_html=True | |
| ) | |
| if is_crisis(final_input): | |
| st.error(" Crisis detected! Please reach out to a mental health professional immediately. " | |
| "You are not alone. Consider contacting a helpline like the National Suicide Prevention Lifeline (988 in the US) or a local emergency service.") | |
| if current_quotes: | |
| model = SentenceTransformer("all-MiniLM-L6-v2") | |
| quote_embeddings = model.encode(current_quotes, convert_to_tensor=True) | |
| user_embedding = model.encode(final_input, convert_to_tensor=True) | |
| sims = util.pytorch_cos_sim(user_embedding, quote_embeddings)[0] | |
| best_match = sims.argmax().item() | |
| selected_quote = current_quotes[best_match] | |
| #st.info(f" **Quote for you:** *{selected_quote}*") | |
| st.markdown(f"""<div class="custom-info-box"><span class="info-icon">ℹ</span><p class="info-text"> | |
| <strong>Quote for you:</strong> {selected_quote}</p> </div> """, unsafe_allow_html=True) | |
| # DilBot's response presented in a chat bubble | |
| st.markdown(f"<div class='bot-message-container'><div class='bot-message'>DilBot: {response}</div></div>", unsafe_allow_html=True) | |
| speak(response, username) | |
| st.session_state.transcribed_text = "" | |
| # Add a visual separator after each conversation turn (optional) | |
| st.markdown("<div class='chat-separator'></div>", unsafe_allow_html=True) | |
| # User's personal dashboard | |
| st.markdown("---") | |
| st.header(" Your Personal Dashboard") | |
| # Load user's journal | |
| journal_data = load_user_journal(username) | |
| if journal_data: | |
| # Statistics | |
| st.subheader(" Your Emotional Statistics") # Moved statistics to the top of dashboard for prominence | |
| with st.container(border=True): | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric("Total Conversations", len(journal_data)) | |
| with col2: | |
| emotions = [entry['emotion'] for entry in journal_data] | |
| most_common = max(set(emotions), key=emotions.count) if emotions else "None" | |
| st.metric("Most Common Emotion", most_common.capitalize()) | |
| with col3: | |
| if journal_data: | |
| avg_confidence = sum(entry['confidence'] for entry in journal_data) / len(journal_data) | |
| st.metric("Avg. Confidence", f"{avg_confidence:.1f}%") | |
| else: | |
| st.metric("Avg. Confidence", "N/A") # Handle case with no journal data for avg. confidence | |
| # Mood tracker | |
| st.subheader(" Your Daily Mood Tracker") | |
| with st.container(border=True): # Wrap chart in a container | |
| # Prepare data for chart | |
| df_data = [] | |
| for entry in journal_data: | |
| df_data.append({ | |
| "date": entry["date"], | |
| "emotion": entry["emotion"].capitalize(), | |
| "confidence": entry["confidence"] | |
| }) | |
| if df_data: | |
| df_chart = pd.DataFrame(df_data) # Use pandas DataFrame for better Altair integration | |
| chart = alt.Chart(df_chart).mark_bar().encode( | |
| x=alt.X('date:N', title='Date', sort=None), # Sort by date ensures correct order | |
| y=alt.Y('count():Q', title='Frequency'), | |
| color=alt.Color('emotion:N', title='Emotion', scale=alt.Scale(range=['#4CAF50', '#FFC107', '#E74C3C', '#3498DB', '#9B59B6', '#1ABC9C'])), # Custom colors | |
| tooltip=['date:N', 'emotion:N', 'count():Q'] | |
| ).properties( | |
| height=350, # Slightly increased height | |
| title="Your Emotional Journey Over Time" | |
| ).interactive() # Make chart interactive for zoom/pan | |
| st.altair_chart(chart, use_container_width=True) | |
| else: | |
| st.info("No emotional data to display yet. Start interacting with DilBot!") | |
| # Recent conversations | |
| st.subheader("Recent Conversations") | |
| recent_entries = journal_data[-5:] if len(journal_data) >= 5 else journal_data | |
| with st.container(border=True): # Wrap recent conversations in a container | |
| if recent_entries: | |
| for i, entry in enumerate(reversed(recent_entries)): | |
| with st.expander(f"{entry['date']} - {entry['emotion'].capitalize()} ({round(entry['confidence'] * 100)}%)"): # Round confidence for display | |
| st.markdown(f"**You said:** {entry['user_input']}") | |
| st.markdown(f"**DilBot replied:** {entry['response']}") | |
| else: | |
| st.info("No recent conversations yet. Start talking to DilBot!") | |
| else: | |
| st.markdown(""" <div style="background-color: #2c3e50; | |
| border-left: 6px solid #64b5f6; | |
| color: white; | |
| border-radius: 10px; | |
| padding: 18px 25px; | |
| margin-top: 2rem; | |
| font-style: italic; | |
| font-size: 1.05em; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.05); | |
| margin-bottom: 1.5rem; | |
| "> | |
| <p style="color: white; margin: 0; font-size: 1.05em;"> | |
| Start your first conversation with DilBot to see your personal dashboard and insights! | |
| </p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("---") | |
| st.markdown("<p class='footer-caption'>Built by Members of CSG Hackathon Team | Your data is stored privately and securely</p>", unsafe_allow_html=True) | |
| # Main app logic | |
| def main(): | |
| if not st.session_state.authenticated: | |
| show_auth_page() | |
| elif st.session_state.is_admin: | |
| show_admin_dashboard() | |
| else: | |
| show_main_app() | |
| if __name__ == "__main__": | |
| main() |