Spaces:
Sleeping
Sleeping
Update user_management.py
Browse files- user_management.py +1020 -69
user_management.py
CHANGED
@@ -1,75 +1,1026 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import streamlit as st
|
2 |
-
from
|
3 |
-
#
|
4 |
-
from
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
else:
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
else:
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
else:
|
56 |
-
with st.spinner("
|
57 |
-
|
58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
else:
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
else:
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import io
|
2 |
+
import os
|
3 |
+
import re
|
4 |
+
from datetime import datetime, timedelta
|
5 |
+
|
6 |
+
import numpy as np
|
7 |
+
import pandas as pd
|
8 |
+
import requests
|
9 |
import streamlit as st
|
10 |
+
from dotenv import load_dotenv
|
11 |
+
from fuzzywuzzy import fuzz # Make sure fuzzywuzzy is installed (pip install fuzzywuzzy python-levenshtein)
|
12 |
+
from groq import Groq
|
13 |
+
from reportlab.lib.pagesizes import letter
|
14 |
+
from reportlab.pdfgen import canvas
|
15 |
+
|
16 |
+
# Import database and user management functions
|
17 |
+
from database import get_user_history
|
18 |
+
from user_management import show_user_management, save_history_if_logged_in
|
19 |
+
from utils import extract_keyword # Ensure utils.py has the extract_keyword function
|
20 |
+
|
21 |
+
# Load environment variables from .env.local for local development.
|
22 |
+
# On Hugging Face Spaces, st.secrets will be used.
|
23 |
+
load_dotenv(dotenv_path=".env.local")
|
24 |
+
|
25 |
+
# --- Configuration and Initial Setup (MUST BE FIRST) ---
|
26 |
+
st.set_page_config(page_title="MediBot - Health Assistant", page_icon="🏥", layout="wide")
|
27 |
+
|
28 |
+
# Initialize Groq client
|
29 |
+
groq_client = None
|
30 |
+
GROQ_AVAILABLE = False
|
31 |
+
try:
|
32 |
+
# Prefer st.secrets for Hugging Face Spaces deployment
|
33 |
+
GROQ_API_KEY = st.secrets.get("GROQ_API_KEY")
|
34 |
+
if not GROQ_API_KEY:
|
35 |
+
# Fallback to environment variable for local development if not in secrets
|
36 |
+
# You might not want to write raw file content to the app in production
|
37 |
+
# f = open(".env", "r")
|
38 |
+
# st.write(f.read())
|
39 |
+
# f.close()
|
40 |
+
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
|
41 |
+
if GROQ_API_KEY:
|
42 |
+
groq_client = Groq(api_key=GROQ_API_KEY)
|
43 |
+
GROQ_AVAILABLE = True
|
44 |
+
else:
|
45 |
+
st.error("Groq API Key not found. Groq chatbot will not be available.")
|
46 |
+
except Exception as e:
|
47 |
+
st.error(f"Error initializing Groq client: {e}. Groq chatbot will not be available.")
|
48 |
+
|
49 |
+
# Initialize Hugging Face Inference API client details for DATEXIS/CORe-clinical-diagnosis-prediction
|
50 |
+
HF_MODEL_AVAILABLE = False
|
51 |
+
HF_API_TOKEN = None
|
52 |
+
try:
|
53 |
+
# Assuming 'med_model' is the name of your Hugging Face API key in st.secrets
|
54 |
+
HF_API_TOKEN = st.secrets.get("med_model")
|
55 |
+
if not HF_API_TOKEN:
|
56 |
+
# Fallback to environment variable for local development
|
57 |
+
HF_API_TOKEN = os.getenv("MED_MODEL") # Using MED_MODEL for consistency with environment variables
|
58 |
+
if HF_API_TOKEN:
|
59 |
+
HF_MODEL_AVAILABLE = True
|
60 |
else:
|
61 |
+
st.warning("Hugging Face 'med_model' API Key not found. Clinical diagnosis assessment will not be available.")
|
62 |
+
except Exception as e:
|
63 |
+
st.warning(f"Error retrieving Hugging Face API key: {e}. Clinical diagnosis assessment will not be available.")
|
64 |
+
|
65 |
+
# Initialize session state variables
|
66 |
+
if "chat_history" not in st.session_state:
|
67 |
+
st.session_state.chat_history = []
|
68 |
+
if "feedback" not in st.session_state:
|
69 |
+
st.session_state.feedback = []
|
70 |
+
if "show_welcome" not in st.session_state:
|
71 |
+
st.session_state.show_welcome = True
|
72 |
+
if "chat_input_value" not in st.session_state: # To clear the text area after submission
|
73 |
+
st.session_state.chat_input_value = ""
|
74 |
+
if "last_chat_response" not in st.session_state: # To persist the last chat response
|
75 |
+
st.session_state.last_chat_response = ""
|
76 |
+
if "feedback_input" not in st.session_state: # For clearing feedback text area
|
77 |
+
st.session_state.feedback_input = ""
|
78 |
+
# Ensure user_id is initialized for saving history
|
79 |
+
if "user_id" not in st.session_state:
|
80 |
+
st.session_state.user_id = None
|
81 |
+
if "logged_in_user" not in st.session_state:
|
82 |
+
st.session_state.logged_in_user = None
|
83 |
+
|
84 |
+
# --- HARDCODED DATA LOADING FROM CSVs ---
|
85 |
+
@st.cache_data # Cache this function to avoid reloading data on every rerun
|
86 |
+
def load_csv_data():
|
87 |
+
try:
|
88 |
+
# These paths assume the CSVs are directly in the same directory as app.py
|
89 |
+
dataset_df = pd.read_csv('dataset.csv').fillna('') # Fill NaN with empty string
|
90 |
+
description_df = pd.read_csv('symptom_Description.csv').fillna('')
|
91 |
+
precaution_df = pd.read_csv('symptom_precaution.csv').fillna('')
|
92 |
+
severity_df = pd.read_csv('Symptom-severity.csv').fillna('') # Load symptom severity
|
93 |
+
# Prepare data for quick lookup
|
94 |
+
# Dataset mapping diseases to their symptoms
|
95 |
+
disease_symptoms_map = {}
|
96 |
+
for index, row in dataset_df.iterrows():
|
97 |
+
disease = row['Disease']
|
98 |
+
# Get all symptoms for this disease, filtering out empty strings and 'Disease' column itself
|
99 |
+
symptoms = [s.strip().replace('_', ' ') for s in row.values[1:] if s.strip()]
|
100 |
+
disease_symptoms_map[disease] = symptoms
|
101 |
+
# Disease descriptions map
|
102 |
+
disease_description_map = {row['Disease']: row['Description'] for index, row in description_df.iterrows()}
|
103 |
+
# Disease precautions map
|
104 |
+
disease_precaution_map = {row['Disease']: [p.strip() for p in row.values[1:] if p.strip()] for index, row in precaution_df.iterrows()}
|
105 |
+
# Symptom severity map
|
106 |
+
# Ensure symptom names are consistent (e.g., lowercase and spaces instead of underscores)
|
107 |
+
symptom_severity_map = {row['Symptom'].strip().replace('_', ' ').lower(): row['weight'] for index, row in severity_df.iterrows()}
|
108 |
+
# Extract all unique symptoms for the multiselect
|
109 |
+
all_unique_symptoms = sorted(list(set(symptom for symptoms_list in disease_symptoms_map.values() for symptom in symptoms_list)))
|
110 |
+
return disease_symptoms_map, disease_description_map, disease_precaution_map, all_unique_symptoms, symptom_severity_map
|
111 |
+
except FileNotFoundError as e:
|
112 |
+
st.error(f"Error: Required CSV file not found. Make sure 'dataset.csv', 'symptom_Description.csv', 'symptom_precaution.csv', and 'Symptom-severity.csv' are in the correct directory. Details: {e}")
|
113 |
+
st.stop() # Stop the app if crucial files are missing
|
114 |
+
except Exception as e:
|
115 |
+
st.error(f"Error loading CSV data: {e}")
|
116 |
+
st.stop() # Stop the app if data loading fails
|
117 |
+
|
118 |
+
disease_symptoms_map, disease_description_map, disease_precaution_map, hardcoded_symptoms, symptom_severity_map = load_csv_data()
|
119 |
+
|
120 |
+
# --- Custom CSS for extensive UI improvements ---
|
121 |
+
st.markdown("""
|
122 |
+
<style>
|
123 |
+
/* Basic Page Layout & Background */
|
124 |
+
.stApp {
|
125 |
+
background-color: #f8f9fa; /* Very light grey */
|
126 |
+
font-family: 'Arial', sans-serif; /* Modern sans-serif font */
|
127 |
+
color: #343a40; /* Darker grey for primary text */
|
128 |
+
}
|
129 |
+
/* Main Container Styling (for sections like Home, History) */
|
130 |
+
/* Streamlit's main content block often has a data-testid="stVerticalBlock" */
|
131 |
+
.stApp > header, .stApp > div:first-child > div:first-child > div:first-child {
|
132 |
+
/* This targets the container that usually holds the main content in Streamlit */
|
133 |
+
padding: 2.5rem; /* Increased padding */
|
134 |
+
border-radius: 12px; /* More rounded corners */
|
135 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); /* Stronger, softer shadow */
|
136 |
+
margin-top: 2rem; /* Space from top elements */
|
137 |
+
margin-bottom: 2rem;
|
138 |
+
background-color: #ffffff; /* White background */
|
139 |
+
}
|
140 |
+
|
141 |
+
/* Headers */
|
142 |
+
h1, h2, h3, h4, h5, h6 {
|
143 |
+
color: #004d99; /* A professional darker blue */
|
144 |
+
font-weight: 700; /* Bold headers */
|
145 |
+
margin-top: 1.5em;
|
146 |
+
margin-bottom: 0.8em;
|
147 |
+
}
|
148 |
+
h1 { font-size: 2.8em; text-align: center; color: #003366; } /* Larger, darker blue for main title */
|
149 |
+
h2 { font-size: 2.2em; border-bottom: 2px solid #e0e7ee; padding-bottom: 0.5em; margin-bottom: 1em; } /* Subtle line under h2 */
|
150 |
+
h3 { font-size: 1.6em; }
|
151 |
+
|
152 |
+
/* Main Tabs Styling (Home, History, Feedback, About, Insights) */
|
153 |
+
.stTabs [data-baseweb="tab-list"] {
|
154 |
+
gap: 20px; /* More space between tabs */
|
155 |
+
margin-bottom: 30px; /* More space below tabs */
|
156 |
+
justify-content: center; /* Center the main tabs */
|
157 |
+
flex-wrap: wrap; /* Allow tabs to wrap on smaller screens */
|
158 |
+
}
|
159 |
+
.stTabs [data-baseweb="tab"] {
|
160 |
+
height: 60px; /* Taller tabs */
|
161 |
+
padding: 15px 30px; /* More padding */
|
162 |
+
background-color: #e9f0f6; /* Light blue-grey background for tabs */
|
163 |
+
border-radius: 12px 12px 0 0; /* Rounded top corners */
|
164 |
+
font-size: 1.25em; /* Larger font size */
|
165 |
+
font-weight: 600; /* Semi-bold */
|
166 |
+
color: #4a6a8c; /* Muted blue text color */
|
167 |
+
border: none; /* Remove default border */
|
168 |
+
border-bottom: 4px solid transparent; /* Thicker highlight bottom border */
|
169 |
+
transition: all 0.3s ease-in-out; /* Smooth transitions */
|
170 |
+
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05); /* Subtle shadow */
|
171 |
+
outline: none !important; /* Explicitly remove outline */
|
172 |
+
text-align: center; /* Center text within tab */
|
173 |
+
min-width: 120px; /* Ensure a minimum width for tabs on mobile */
|
174 |
+
}
|
175 |
+
.stTabs [data-baseweb="tab"]:hover {
|
176 |
+
background-color: #dbe4ee; /* Slightly darker blue-grey on hover */
|
177 |
+
color: #004d99; /* Darker blue text on hover */
|
178 |
+
transform: translateY(-2px); /* Slight lift effect */
|
179 |
+
outline: none !important; /* Ensure no outline on hover either */
|
180 |
+
}
|
181 |
+
/* Updated styling for selected main tabs */
|
182 |
+
.stTabs [data-baseweb="tab"][aria-selected="true"] {
|
183 |
+
background-color: #004d99; /* Sober darker blue for selected tab */
|
184 |
+
color: white; /* White text for selected tab */
|
185 |
+
border-bottom: 4px solid #004d99; /* Keep the consistent bottom border */
|
186 |
+
box-shadow: 0 -4px 15px rgba(0, 0, 0, 0.15); /* More pronounced shadow for selected */
|
187 |
+
outline: none !important; /* Ensure no outline when selected */
|
188 |
+
}
|
189 |
+
/* Ensure no outline on focus for tabs */
|
190 |
+
.stTabs [data-baseweb="tab"]:focus {
|
191 |
+
outline: none !important;
|
192 |
+
box-shadow: 0 0 0 0.25rem rgba(0, 123, 255, 0.25) !important; /* Subtle focus glow if needed */
|
193 |
+
}
|
194 |
+
|
195 |
+
/* Nested Tabs Styling (Symptom Checker, Chat with MediBot) */
|
196 |
+
/* This targets tabs that are children of other tabs, making them smaller */
|
197 |
+
.stTabs [data-baseweb="tab-list"]:has(.stTabs [data-baseweb="tab-list"]) [data-baseweb="tab"],
|
198 |
+
.stTabs [data-baseweb="tab-list"] [role="tablist"] [data-baseweb="tab"] { /* More robust selector for nested tabs */
|
199 |
+
height: 45px; /* Slightly smaller height for sub-tabs */
|
200 |
+
padding: 10px 20px; /* Slightly smaller padding */
|
201 |
+
font-size: 1.1em;
|
202 |
+
background-color: #f0f5f9; /* Lighter blue-grey for sub-tabs */
|
203 |
+
border-radius: 10px; /* Fully rounded corners for sub-tabs */
|
204 |
+
border: 1px solid #d8e2ed;
|
205 |
+
margin: 0 8px; /* Small margin between sub-tabs */
|
206 |
+
box-shadow: none; /* No individual shadow for sub-tabs */
|
207 |
+
color: #5f7a96;
|
208 |
+
outline: none !important; /* Remove outline on focus */
|
209 |
+
min-width: unset; /* Remove min-width for sub-tabs */
|
210 |
+
}
|
211 |
+
.stTabs [data-baseweb="tab-list"]:has(.stTabs [data-baseweb="tab-list"]) [data-baseweb="tab"]:hover,
|
212 |
+
.stTabs [data-baseweb="tab-list"] [role="tablist"] [data-baseweb="tab"]:hover {
|
213 |
+
background-color: #e3ecf5;
|
214 |
+
color: #004d99;
|
215 |
+
transform: none; /* No lift for sub-tabs */
|
216 |
+
outline: none !important; /* Ensure no outline on hover either */
|
217 |
+
}
|
218 |
+
/* Updated styling for selected nested tabs */
|
219 |
+
.stTabs [data-baseweb="tab-list"]:has(.stTabs [data-baseweb="tab-list"]) [aria-selected="true"],
|
220 |
+
.stTabs [data-baseweb="tab-list"] [role="tablist"] [aria-selected="true"] {
|
221 |
+
background-color: #0056b3; /* Slightly darker blue for selected sub-tab */
|
222 |
+
color: white;
|
223 |
+
border: 1px solid #0056b3;
|
224 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.15); /* Subtle shadow for selected sub-tab */
|
225 |
+
outline: none !important; /* Ensure no outline when selected */
|
226 |
+
}
|
227 |
+
|
228 |
+
/* General Button Styling */
|
229 |
+
.stButton>button {
|
230 |
+
background-color: #007bff; /* Primary blue button */
|
231 |
+
color: white;
|
232 |
+
border-radius: 10px; /* More rounded */
|
233 |
+
padding: 12px 28px; /* More padding */
|
234 |
+
font-weight: 600; /* Semi-bold */
|
235 |
+
border: none;
|
236 |
+
cursor: pointer;
|
237 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.1); /* Softer, more diffuse shadow */
|
238 |
+
transition: all 0.3s ease-in-out; /* Smooth transitions */
|
239 |
+
font-size: 1.05em; /* Slightly larger text */
|
240 |
+
outline: none; /* Remove outline on focus */
|
241 |
+
width: auto; /* Allow buttons to size naturally, but consider max-width on mobile */
|
242 |
+
min-width: 100px; /* Ensure minimum tappable area */
|
243 |
+
}
|
244 |
+
.stButton>button:hover {
|
245 |
+
background-color: #0056b3; /* Darker blue on hover */
|
246 |
+
box-shadow: 0 8px 20px rgba(0,0,0,0.15); /* Larger shadow on hover */
|
247 |
+
transform: translateY(-3px); /* More pronounced lift */
|
248 |
+
outline: none; /* Ensure no outline on hover either */
|
249 |
+
}
|
250 |
+
/* Full width buttons in sidebar for better mobile experience */
|
251 |
+
.sidebar .stButton>button {
|
252 |
+
width: 100%; /* Full width buttons in sidebar */
|
253 |
+
margin-bottom: 10px;
|
254 |
+
}
|
255 |
+
|
256 |
+
|
257 |
+
/* Input Fields, Selects, etc. */
|
258 |
+
.stTextInput>div>div>input,
|
259 |
+
.stTextArea>div>div>textarea, /* Target textarea as well */
|
260 |
+
.stDateInput>div>div>input,
|
261 |
+
.stMultiSelect>div>div {
|
262 |
+
border-radius: 10px; /* More rounded */
|
263 |
+
border: 1px solid #ced4da; /* Light grey border */
|
264 |
+
padding: 10px 15px; /* More padding */
|
265 |
+
box-shadow: inset 0 1px 4px rgba(0,0,0,0.05); /* Inner shadow */
|
266 |
+
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
267 |
+
background-color: #fefefe; /* Slightly off-white background */
|
268 |
+
outline: none; /* Remove default outline */
|
269 |
+
}
|
270 |
+
.stTextInput>div>div>input:focus,
|
271 |
+
.stTextArea>div>div>textarea:focus,
|
272 |
+
.stDateInput>div>div>input:focus,
|
273 |
+
.stMultiSelect>div>div:focus-within {
|
274 |
+
border-color: #007bff; /* Primary blue border on focus */
|
275 |
+
box-shadow: 0 0 0 0.25rem rgba(0,123,255,.25); /* Focus glow */
|
276 |
+
outline: none; /* Remove default outline */
|
277 |
+
}
|
278 |
+
/* Specific styling for the multiselect selected items */
|
279 |
+
.stMultiSelect div[data-baseweb="tag"] {
|
280 |
+
background-color: #e0f2f7 !important; /* Light blue tag background */
|
281 |
+
color: #004d99 !important; /* Dark blue tag text */
|
282 |
+
border-radius: 5px !important;
|
283 |
+
padding: 5px 10px !important;
|
284 |
+
margin: 2px !important;
|
285 |
+
}
|
286 |
+
/* Expander (for disease info) */
|
287 |
+
.stExpander {
|
288 |
+
border: 1px solid #e0e7ee; /* Lighter grey border */
|
289 |
+
border-radius: 12px; /* More rounded */
|
290 |
+
margin-bottom: 15px; /* More space below */
|
291 |
+
background-color: #ffffff; /* White background */
|
292 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.05); /* Softer shadow */
|
293 |
+
overflow: hidden; /* Ensure rounded corners clip content */
|
294 |
+
}
|
295 |
+
.stExpander > div > div > .streamlit-expanderHeader {
|
296 |
+
background-color: #f0f5f9; /* Light blue header */
|
297 |
+
padding: 15px 20px;
|
298 |
+
font-weight: 600;
|
299 |
+
font-size: 1.1em;
|
300 |
+
color: #004d99;
|
301 |
+
border-bottom: 1px solid #e0e7ee;
|
302 |
+
outline: none !important; /* Remove outline on focus */
|
303 |
+
}
|
304 |
+
.stExpander > div > div > .streamlit-expanderHeader:focus {
|
305 |
+
outline: none !important; /* Ensure no outline on focus */
|
306 |
+
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25) !important; /* Subtle focus ring */
|
307 |
+
}
|
308 |
+
.stExpander > div > div > .streamlit-expanderContent {
|
309 |
+
padding: 20px; /* Increased padding inside */
|
310 |
+
}
|
311 |
+
/* Sidebar Styling */
|
312 |
+
/* Streamlit's sidebar elements are usually inside a div with role="complementary" */
|
313 |
+
.st-emotion-cache-vk33as { /* This targets the Streamlit sidebar div with padding */
|
314 |
+
background-color: #ffffff; /* White sidebar background */
|
315 |
+
padding: 25px;
|
316 |
+
border-radius: 12px;
|
317 |
+
box-shadow: 0 8px 24px rgba(0,0,0,0.08); /* Consistent shadow */
|
318 |
+
margin-top: 2rem;
|
319 |
+
}
|
320 |
+
.sidebar .stButton>button {
|
321 |
+
width: 100%; /* Full width buttons in sidebar */
|
322 |
+
margin-bottom: 10px;
|
323 |
+
}
|
324 |
+
.sidebar h2, .sidebar h3 {
|
325 |
+
color: #004d99;
|
326 |
+
text-align: center;
|
327 |
+
margin-bottom: 1.5em;
|
328 |
+
}
|
329 |
+
.sidebar .stTextInput, .sidebar .stDateInput, .sidebar .stSelectbox {
|
330 |
+
margin-bottom: 1em; /* Space between sidebar inputs */
|
331 |
+
}
|
332 |
+
/* Alerts */
|
333 |
+
.stAlert {
|
334 |
+
border-radius: 10px; /* Rounded corners for alerts */
|
335 |
+
padding: 15px;
|
336 |
+
font-size: 1.05em;
|
337 |
+
}
|
338 |
+
.stAlert.st-success { background-color: #d4edda; color: #155724; border-color: #c3e6cb; }
|
339 |
+
.stAlert.st-info { background-color: #d1ecf1; color: #0c5460; border-color: #bee5eb; }
|
340 |
+
.stAlert.st-warning { background-color: #fff3cd; color: #856404; border-color: #ffeeba; }
|
341 |
+
.stAlert.st-error { background-color: #f8d7da; color: #721c24; border-color: #f5c6cb; }
|
342 |
+
/* Spinners */
|
343 |
+
.stSpinner { color: #007bff; } /* Spinner color matching primary button */
|
344 |
+
|
345 |
+
/* Disclaimer Box - using a more reliable target */
|
346 |
+
div[data-testid="stVerticalBlock"] > div:first-child > div:has(p > strong:contains("Disclaimer")) {
|
347 |
+
background-color: #fff8e1; /* Light yellow for warnings/disclaimers */
|
348 |
+
border: 1px solid #ffeeba;
|
349 |
+
padding: 1.5rem;
|
350 |
+
border-radius: 10px;
|
351 |
+
margin-bottom: 2rem;
|
352 |
+
color: #856404;
|
353 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
354 |
+
}
|
355 |
+
/* This targets the specific disclaimer block by looking for its content */
|
356 |
+
.stAlert[data-testid="stAlert"] p strong:contains("Important Disclaimer") {
|
357 |
+
color: #664d03;
|
358 |
+
}
|
359 |
+
|
360 |
+
/* General text improvements */
|
361 |
+
p {
|
362 |
+
line-height: 1.7;
|
363 |
+
margin-bottom: 1em;
|
364 |
+
}
|
365 |
+
ul, ol {
|
366 |
+
margin-bottom: 1em;
|
367 |
+
padding-left: 25px;
|
368 |
+
}
|
369 |
+
li {
|
370 |
+
margin-bottom: 0.5em;
|
371 |
+
}
|
372 |
+
/* Increased font size for history expander titles */
|
373 |
+
.stExpander > div > div > .streamlit-expanderHeader {
|
374 |
+
font-size: 1.2em; /* Increased font size */
|
375 |
+
}
|
376 |
+
|
377 |
+
/* Media queries for specific mobile adjustments if needed (example) */
|
378 |
+
@media (max-width: 768px) {
|
379 |
+
h1 {
|
380 |
+
font-size: 2.2em; /* Slightly smaller H1 on mobile */
|
381 |
+
}
|
382 |
+
h2 {
|
383 |
+
font-size: 1.8em; /* Slightly smaller H2 on mobile */
|
384 |
+
}
|
385 |
+
.stTabs [data-baseweb="tab"] {
|
386 |
+
font-size: 1em; /* Smaller tab font size on mobile */
|
387 |
+
padding: 10px 15px; /* Reduced padding for tabs on mobile */
|
388 |
+
height: auto; /* Allow height to adjust */
|
389 |
+
min-width: unset; /* Remove min-width to allow more flexibility */
|
390 |
+
flex: 1 1 auto; /* Allow tabs to grow and shrink more flexibly */
|
391 |
+
}
|
392 |
+
.stTabs [data-baseweb="tab-list"] {
|
393 |
+
gap: 10px; /* Smaller gap between tabs on mobile */
|
394 |
+
}
|
395 |
+
/* Make multiselect items slightly smaller on mobile for better fit */
|
396 |
+
.stMultiSelect div[data-baseweb="tag"] {
|
397 |
+
font-size: 0.9em !important;
|
398 |
+
padding: 3px 8px !important;
|
399 |
+
}
|
400 |
+
}
|
401 |
+
</style>
|
402 |
+
""", unsafe_allow_html=True)
|
403 |
+
|
404 |
+
# --- Groq API Response Function (updated system prompt) ---
|
405 |
+
def get_groq_response(user_query, severity_label="Undetermined Severity", model="llama-3.3-70b-versatile"):
|
406 |
+
"""
|
407 |
+
Function to get a response from Groq API for health questions.
|
408 |
+
Augments the system prompt with severity information if available.
|
409 |
+
"""
|
410 |
+
if not GROQ_AVAILABLE or groq_client is None:
|
411 |
+
return "AI assistant is not available."
|
412 |
+
|
413 |
+
# Augment the system prompt with severity information from clinical diagnosis model
|
414 |
+
# Note: 'severity_label' comes from the symptom checker. If asking a direct question,
|
415 |
+
# it might default to "Undetermined Severity" unless explicitly passed from a prior analysis.
|
416 |
+
system_prompt = (
|
417 |
+
"You are an experienced physician specialized in diagnosis and clinical decision-making. "
|
418 |
+
"Your primary role is to analyze presented symptoms or health queries and provide a differential diagnosis along with evidence-based recommendations for next steps. "
|
419 |
+
f"Based on initial analysis, the perceived severity of the user's condition is: {severity_label}. "
|
420 |
+
"Adjust your tone and recommendations accordingly, emphasizing urgency if severity is 'High Severity'.\n\n"
|
421 |
+
"When a user describes symptoms or asks a health question, you should:\n\n"
|
422 |
+
"1. **Prioritized Differential Diagnosis**: Provide a prioritized list of possible diagnoses based on the information given, indicating their relative likelihood or confidence (e.g., 'most likely', 'possible', 'less likely').\n"
|
423 |
+
"2. **Reasoning**: Briefly explain the clinical reasoning for each diagnosis, referencing common clinical features, pathophysiology, or typical presentations.\n"
|
424 |
+
"3. **Recommended Investigations**: Suggest appropriate next diagnostic tests or investigations to confirm or rule out these conditions.\n"
|
425 |
+
"4. **Initial Management/Treatment**: Propose evidence-based initial management or general treatment options (e.g., symptomatic relief, lifestyle changes, over-the-counter suggestions).\n"
|
426 |
+
"5. **Red Flags/Urgency**: Clearly highlight any red flags or warning signs that would require immediate emergency care or urgent specialist referral.\n\n"
|
427 |
+
"Always maintain a professional, empathetic, and medically accurate tone. Present information clearly, using bullet points or numbered lists where appropriate. "
|
428 |
+
"Crucially, always conclude your response by strongly advising the user to consult a qualified healthcare professional for a definitive diagnosis and personalized treatment plan, "
|
429 |
+
"as this AI is for informational purposes only and not a substitute for professional medical advice."
|
430 |
+
)
|
431 |
+
try:
|
432 |
+
chat_completion = groq_client.chat.completions.create(
|
433 |
+
messages=[
|
434 |
+
{"role": "system", "content": system_prompt},
|
435 |
+
{"role": "user", "content": user_query},
|
436 |
+
],
|
437 |
+
model=model,
|
438 |
+
temperature=0.7,
|
439 |
+
max_tokens=800, # Increased max_tokens to allow for more detailed responses
|
440 |
+
top_p=1,
|
441 |
+
stop=None,
|
442 |
+
stream=False,
|
443 |
+
)
|
444 |
+
return chat_completion.choices[0].message.content
|
445 |
+
except Exception as e:
|
446 |
+
st.error(f"Groq API call failed: {e}")
|
447 |
+
return "Sorry, I am unable to process that request at the moment due to an AI service error."
|
448 |
+
|
449 |
+
# --- Clinical Diagnosis Model Integration (DATEXIS/CORe-clinical-diagnosis-prediction) ---
|
450 |
+
def get_diagnosis_and_severity_from_model(symptoms_text):
|
451 |
+
"""
|
452 |
+
Function to get diagnosis predictions and infer severity from DATEXIS/CORe-clinical-diagnosis-prediction
|
453 |
+
using Hugging Face Inference API.
|
454 |
+
"""
|
455 |
+
if not HF_MODEL_AVAILABLE or not HF_API_TOKEN:
|
456 |
+
st.warning("Hugging Face API key not available. Cannot assess severity.")
|
457 |
+
return "Severity Assessment Unavailable (API Key Missing)", []
|
458 |
+
|
459 |
+
API_URL = "https://api-inference.huggingface.co/models/DATEXIS/CORe-clinical-diagnosis-prediction"
|
460 |
+
headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
|
461 |
+
payload = {"inputs": symptoms_text}
|
462 |
+
try:
|
463 |
+
response = requests.post(API_URL, headers=headers, json=payload)
|
464 |
+
response.raise_for_status()
|
465 |
+
result = response.json()
|
466 |
+
|
467 |
+
raw_predicted_diagnoses = []
|
468 |
+
threshold = 0.5
|
469 |
+
if result and isinstance(result, list) and len(result) > 0 and isinstance(result[0], list):
|
470 |
+
for prediction_set in result:
|
471 |
+
for item in prediction_set:
|
472 |
+
if item['score'] >= threshold:
|
473 |
+
raw_predicted_diagnoses.append(item['label'])
|
474 |
+
|
475 |
+
filtered_diagnoses = []
|
476 |
+
generic_filter_keywords = [
|
477 |
+
'unspecified', 'acute', 'chronic', 'use', 'status', 'other', 'not elsewhere classified',
|
478 |
+
'no diagnosis', 'history of', 'finding', 'problem', 'syndrome', 'disease',
|
479 |
+
'disorder', 'condition', 'code', 'category', 'episode', 'complication',
|
480 |
+
'sequelae', 'factor', 'manifestation', 'procedure', 'examination', 'observation',
|
481 |
+
'symptoms', 'sign', 'unconfirmed', 'type', 'group', 'normal', 'unknown', 'level',
|
482 |
+
'positive', 'negative', 'patient', 'value', 'test', 'result', 'diagnosis',
|
483 |
+
'kidney', 'stage', 'without', 'essential',
|
484 |
+
'organ', 'function', 'system', 'body', 'region', 'with', 'due to', 'related to',
|
485 |
+
'clinical', 'consideration', 'presence', 'absence', 'mild', 'moderate', 'severe',
|
486 |
+
'manifesting', 'affecting', 'affect', 'area', 'part', 'general', 'specific',
|
487 |
+
'diagnosis of', 'history of', 'finding of', 'problem of', 'type of', 'group of',
|
488 |
+
'unlikely', 'possible', 'likely',
|
489 |
+
'symptom', 'sign', 'pain', 'fever', 'cough', 'vomiting', 'nausea', 'rash', 'headache', 'fatigue', 'diarrhea', 'sore throat', 'hemorrhage'
|
490 |
+
]
|
491 |
+
|
492 |
+
for diagnosis_label in raw_predicted_diagnoses:
|
493 |
+
lower_diag = diagnosis_label.lower()
|
494 |
+
is_generic = False
|
495 |
+
for generic_kw in generic_filter_keywords:
|
496 |
+
if re.search(r'\b' + re.escape(generic_kw) + r'\b', lower_diag) and len(lower_diag.split()) <= 2:
|
497 |
+
is_generic = True
|
498 |
+
break
|
499 |
+
if lower_diag.replace('.', '').isdigit() and len(lower_diag.replace('.', '')) < 5:
|
500 |
+
is_generic = True
|
501 |
+
if len(lower_diag.split()) <= 2 and lower_diag in ['pain', 'fever', 'cough', 'vomiting', 'nausea', 'rash', 'headache', 'fatigue', 'diarrhea', 'sore throat', 'hemorrhage']:
|
502 |
+
is_generic = True
|
503 |
+
|
504 |
+
if not is_generic:
|
505 |
+
filtered_diagnoses.append(diagnosis_label)
|
506 |
+
|
507 |
+
filtered_diagnoses = list(dict.fromkeys(filtered_diagnoses))
|
508 |
+
if len(filtered_diagnoses) > 5:
|
509 |
+
filtered_diagnoses = filtered_diagnoses[:5]
|
510 |
+
|
511 |
+
if not filtered_diagnoses:
|
512 |
+
return "Overall Symptom Severity (AI Assessment): Undetermined Severity", []
|
513 |
+
|
514 |
+
severity_map_keywords = {
|
515 |
+
"High Severity": ['heart attack', 'stroke', 'failure', 'hemorrhage', 'cancer', 'acute respiratory', 'sepsis', 'cardiac arrest', 'severe', 'malignant', 'emergency', 'rupture', 'infarction', 'coma', 'shock', 'decompensation', 'crisis', 'perforation', 'ischemia', 'embolism', 'aneurysm', 'critical'],
|
516 |
+
"Moderate Severity": ['hypertension', 'diabetes', 'pneumonia', 'infection', 'chronic', 'inflammation', 'moderate', 'insufficiency', 'fracture', 'ulcer', 'hepatitis', 'renal', 'vascular', 'disease', 'disorder', 'syndrome', 'acute', 'bronchitis', 'appendicitis', 'gallstones', 'pancreatitis', 'lupus', 'arthritis'],
|
517 |
+
"Low Severity": ['headache', 'pain', 'mild', 'allergy', 'fever', 'cough', 'common cold', 'dermatitis', 'arthritis', 'influenza', 'viral', 'sprain', 'strain', 'gastritis', 'sore throat', 'conjunctivitis', 'sinusitis', 'bruise', 'rash', 'minor']
|
518 |
+
}
|
519 |
+
|
520 |
+
overall_severity = "Low Severity"
|
521 |
+
for diagnosis_label in filtered_diagnoses:
|
522 |
+
diag_lower = diagnosis_label.lower()
|
523 |
+
if any(keyword in diag_lower for keyword in severity_map_keywords["High Severity"]):
|
524 |
+
return "Overall Symptom Severity (AI Assessment): High Severity", filtered_diagnoses
|
525 |
+
if any(keyword in diag_lower for keyword in severity_map_keywords["Moderate Severity"]):
|
526 |
+
if overall_severity == "Low Severity":
|
527 |
+
overall_severity = "Moderate Severity"
|
528 |
+
# Keep looping to see if a High Severity diagnosis is found later in the filtered list
|
529 |
+
# Don't return here if a Moderate is found, continue to check for High
|
530 |
+
|
531 |
+
return f"Overall Symptom Severity (AI Assessment): {overall_severity}", filtered_diagnoses
|
532 |
+
|
533 |
+
except requests.exceptions.RequestException as e:
|
534 |
+
st.error(f"Network or API Error during clinical diagnosis model call: {e}. Check API key and internet connection.")
|
535 |
+
return "Severity Assessment Unavailable (Network Error)", []
|
536 |
+
except Exception as e:
|
537 |
+
st.error(f"Error processing clinical diagnosis model response: {e}. Unexpected data format or mapping issue.")
|
538 |
+
return "Severity Assessment Unavailable (Processing Error)", []
|
539 |
+
|
540 |
+
# --- Functions for PDF generation (retained and adjusted for DB history) ---
|
541 |
+
def generate_pdf_report(history_data):
|
542 |
+
buffer = io.BytesIO()
|
543 |
+
c = canvas.Canvas(buffer, pagesize=letter)
|
544 |
+
width, height = letter
|
545 |
+
c.setFont("Helvetica-Bold", 16)
|
546 |
+
c.drawString(50, height - 50, "MediBot Health Report")
|
547 |
+
c.setFont("Helvetica", 10)
|
548 |
+
c.drawString(50, height - 70, f"Report Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
549 |
+
y = height - 100
|
550 |
+
|
551 |
+
for entry in history_data:
|
552 |
+
# history_data comes as list of tuples: (history_id, symptoms, predicted_diseases, query_timestamp)
|
553 |
+
timestamp, symptoms_text, predicted_diseases = entry[3], entry[1], entry[2]
|
554 |
+
|
555 |
+
if y < 100:
|
556 |
+
c.showPage()
|
557 |
+
c.setFont("Helvetica-Bold", 16)
|
558 |
+
c.drawString(50, height - 50, "MediBot Health Report (Continued)")
|
559 |
+
c.setFont("Helvetica", 10)
|
560 |
+
y = height - 100
|
561 |
+
|
562 |
+
c.setFont("Helvetica-Bold", 10)
|
563 |
+
c.drawString(50, y, f"Timestamp: {timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
|
564 |
+
y -= 15
|
565 |
+
c.setFont("Helvetica", 10)
|
566 |
+
|
567 |
+
symptoms_line = f"Symptoms/Question: {symptoms_text}"
|
568 |
+
insight_line = f"Insight/Response: {predicted_diseases}"
|
569 |
+
|
570 |
+
textobject = c.beginText(50, y)
|
571 |
+
textobject.setFont("Helvetica", 10)
|
572 |
+
|
573 |
+
# Helper to wrap text within PDF
|
574 |
+
def draw_wrapped_text(text_obj, text, max_width):
|
575 |
+
nonlocal y
|
576 |
+
words = text.split(' ')
|
577 |
+
current_line_words = []
|
578 |
+
for word_idx, word in enumerate(words):
|
579 |
+
temp_line = ' '.join(current_line_words + [word])
|
580 |
+
if c.stringWidth(temp_line, "Helvetica", 10) < max_width:
|
581 |
+
current_line_words.append(word)
|
582 |
else:
|
583 |
+
text_obj.textLine(' '.join(current_line_words))
|
584 |
+
y -= 12 # Line height
|
585 |
+
current_line_words = [word]
|
586 |
+
# Handle very long words that exceed max_width
|
587 |
+
if c.stringWidth(word, "Helvetica", 10) >= max_width:
|
588 |
+
# If a single word is too long, break it
|
589 |
+
chunk_size = 50 # Arbitrary chunk size for breaking long words
|
590 |
+
for i in range(0, len(word), chunk_size):
|
591 |
+
text_obj.textLine(word[i:i + chunk_size])
|
592 |
+
y -= 12
|
593 |
+
current_line_words = [] # Reset after a long word is broken
|
594 |
+
continue
|
595 |
+
if current_line_words: # Draw any remaining words
|
596 |
+
text_obj.textLine(' '.join(current_line_words))
|
597 |
+
y -= 12 # Line height
|
598 |
+
|
599 |
+
draw_wrapped_text(textobject, symptoms_line, width - 100)
|
600 |
+
draw_wrapped_text(textobject, insight_line, width - 100)
|
601 |
+
c.drawText(textobject)
|
602 |
+
y -= 20
|
603 |
+
|
604 |
+
c.save()
|
605 |
+
buffer.seek(0)
|
606 |
+
return buffer
|
607 |
+
|
608 |
+
# --- Symptom Checker Logic (Now using hardcoded data and severity) ---
|
609 |
+
def get_disease_info_from_csv(selected_symptoms: list, disease_symptoms_map, disease_description_map, disease_precaution_map, symptom_severity_map) -> list[tuple[str, str, list[str]]]:
|
610 |
+
"""
|
611 |
+
Finds potential diseases, descriptions, and precautions based on selected symptoms
|
612 |
+
using the hardcoded CSV data, incorporating symptom severity.
|
613 |
+
Returns a list of tuples: (disease_name, description, [precautions])
|
614 |
+
"""
|
615 |
+
matching_diseases_with_scores = {} # Stores disease -> weighted_score
|
616 |
+
# Normalize selected symptoms for consistent matching
|
617 |
+
normalized_selected_symptoms = [s.strip().lower() for s in selected_symptoms]
|
618 |
+
|
619 |
+
for disease, symptoms_list in disease_symptoms_map.items():
|
620 |
+
# Convert the symptoms_list from map to lowercase for comparison
|
621 |
+
normalized_disease_symptoms = [s.lower() for s in symptoms_list]
|
622 |
+
weighted_score = 0
|
623 |
+
for selected_symptom in normalized_selected_symptoms:
|
624 |
+
if selected_symptom in normalized_disease_symptoms:
|
625 |
+
# Add severity weight if available, otherwise a default of 1
|
626 |
+
# The .get() method is safe if a symptom is in dataset.csv but not in Symptom-severity.csv
|
627 |
+
weight = symptom_severity_map.get(selected_symptom, 1)
|
628 |
+
weighted_score += weight
|
629 |
+
if weighted_score > 0:
|
630 |
+
matching_diseases_with_scores[disease] = weighted_score
|
631 |
+
|
632 |
+
# Sort diseases by the weighted score (highest first)
|
633 |
+
sorted_matching_diseases = sorted(matching_diseases_with_scores.items(), key=lambda item: item[1], reverse=True)
|
634 |
+
|
635 |
+
results = []
|
636 |
+
# Limit to top 5 most matching diseases (or fewer if less than 5 matches)
|
637 |
+
for disease_name, _ in sorted_matching_diseases[:5]:
|
638 |
+
description = disease_description_map.get(disease_name, "No description available.")
|
639 |
+
precautions = disease_precaution_map.get(disease_name, ["No precautions available."])
|
640 |
+
results.append((disease_name, description, precautions))
|
641 |
+
return results
|
642 |
+
|
643 |
+
# --- Function to parse insight text for analytics ---
|
644 |
+
def parse_insight_text_for_conditions(insight_text: str) -> list[str]:
|
645 |
+
"""
|
646 |
+
Parses the combined insight text from history to extract condition names.
|
647 |
+
Expected format: "Severity. Dataset Conditions: Cond1, Cond2; AI Suggestions: AICond1, AIC2"
|
648 |
+
or just "Severity. No specific insights found."
|
649 |
+
"""
|
650 |
+
conditions = []
|
651 |
+
# Regex to find "Dataset Conditions: ..." and "AI Suggestions: ..." parts
|
652 |
+
dataset_match = re.search(r"Dataset Conditions:\s*([^;]+)", insight_text)
|
653 |
+
ai_match = re.search(r"AI Suggestions:\s*(.+)", insight_text)
|
654 |
+
|
655 |
+
if dataset_match:
|
656 |
+
dataset_str = dataset_match.group(1).strip()
|
657 |
+
if dataset_str and dataset_str.lower() != "no specific insights found":
|
658 |
+
conditions.extend([c.strip() for c in dataset_str.split(',') if c.strip()])
|
659 |
+
|
660 |
+
if ai_match:
|
661 |
+
ai_str = ai_match.group(1).strip()
|
662 |
+
if ai_str and ai_str.lower() != "no specific insights found" and ai_str.lower() != "ai assistant is not available.":
|
663 |
+
conditions.extend([c.strip() for c in ai_str.split(',') if c.strip()])
|
664 |
+
|
665 |
+
# Remove duplicates and return
|
666 |
+
return list(set(conditions))
|
667 |
+
|
668 |
+
# --- Main Application Logic ---
|
669 |
+
# Ensure feedback_input is initialized if not already (redundant if done at start, but harmless)
|
670 |
+
if "feedback_input" not in st.session_state:
|
671 |
+
st.session_state.feedback_input = ""
|
672 |
+
|
673 |
+
|
674 |
+
if st.session_state.show_welcome:
|
675 |
+
# A more visually appealing welcome page
|
676 |
+
st.markdown("<h1 style='color: #0056b3; font-size: 3.5em; margin-top: 50px; text-shadow: 2px 2px 4px rgba(0,0,0,0.1);'>MediBot - Your Personal Health Assistant 🏥</h1>", unsafe_allow_html=True)
|
677 |
+
st.markdown("""
|
678 |
+
<div style='background-color: #e6f7ff; padding: 30px; border-radius: 15px; text-align: center; margin-top: 30px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);'>
|
679 |
+
<p style='font-size: 1.2em; line-height: 1.8; color: #336699;'>
|
680 |
+
Explore possible causes and precautions for your symptoms and get answers to health-related questions using advanced AI.
|
681 |
+
Your health journey, simplified and supported.
|
682 |
+
</p>
|
683 |
+
<ul style='list-style-type: none; padding: 0; margin-top: 25px; display: inline-block; text-align: left;'>
|
684 |
+
<li style='font-size: 1.1em; margin-bottom: 10px; color: #2a527a;'>✅ <b>Symptom Checker:</b> Get insights from a comprehensive symptom list.</li>
|
685 |
+
<li style='font-size: 1.1em; margin-bottom: 10px; color: #2a527a;'>💬 <b>AI Chatbot:</b> Ask general health questions.</li>
|
686 |
+
<li style='font-size: 1.1em; margin-bottom: 10px; color: #2a527a;'>👤 <b>User Management:</b> Save your health history.</li>
|
687 |
+
<li style='font-size: 1.1em; margin-bottom: 10px; color: #2a527a;'>📜 <b>Persistent History:</b> View and download past records.</li>
|
688 |
+
<li style='font-size: 1.1em; margin-bottom: 10px; color: #2a527a;'>📈 <b>Usage Insights:</b> See trends in conditions.</li>
|
689 |
+
</ul>
|
690 |
+
</div>
|
691 |
+
""", unsafe_allow_html=True)
|
692 |
+
st.markdown("<div style='text-align: center; margin-top: 40px;'>", unsafe_allow_html=True)
|
693 |
+
if st.button("Get Started", key="welcome_button"):
|
694 |
+
st.session_state.show_welcome = False
|
695 |
+
st.rerun() # Force rerun to ensure app reloads to main page
|
696 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
697 |
+
st.markdown("""
|
698 |
+
<div style='background-color: #fff8e1; padding: 20px; border-radius: 10px; margin-top: 50px; text-align: center; color: #8a6d3b; box-shadow: 0 4px 10px rgba(0,0,0,0.05);'>
|
699 |
+
<p style='font-size: 0.9em; margin-bottom: 0;'>
|
700 |
+
<b>Important Disclaimer:</b> This app provides preliminary health information based on symptoms and AI analysis.
|
701 |
+
<b>It is not a substitute for professional medical advice, diagnosis, or treatment.</b> Always consult a qualified healthcare provider for a definitive diagnosis and personalized treatment plan, as this AI is for informational purposes only and not a substitute for professional medical advice.
|
702 |
+
</p>
|
703 |
+
</div>
|
704 |
+
""", unsafe_allow_html=True)
|
705 |
+
else:
|
706 |
+
# Displaying disclaimer at the top of the main app view
|
707 |
+
st.markdown("<div class='stContainer' style='background-color: #fff8e1; border: 1px solid #ffeeba;'>"
|
708 |
+
"<p style='margin-bottom: 0; font-size: 0.95em; color: #856404;'>"
|
709 |
+
"<strong>Disclaimer</strong>: This app provides preliminary health information based on symptoms and AI analysis. "
|
710 |
+
"It is not a substitute for professional medical advice, diagnosis, or treatment. Always consult a qualified healthcare provider."
|
711 |
+
"</p>"
|
712 |
+
"</div>", unsafe_allow_html=True)
|
713 |
+
|
714 |
+
# Sidebar for User Management (remains in sidebar)
|
715 |
+
with st.sidebar:
|
716 |
+
st.header("User Management")
|
717 |
+
show_user_management()
|
718 |
+
st.markdown("---")
|
719 |
+
st.header("Navigation")
|
720 |
+
# Add a button to clear chat history
|
721 |
+
if st.button("Clear Chat History", help="Clears all messages from the current session."):
|
722 |
+
st.session_state.chat_history = []
|
723 |
+
st.session_state.last_chat_response = ""
|
724 |
+
st.session_state.chat_input_value = ""
|
725 |
+
st.success("Chat history cleared!")
|
726 |
+
# PDF Download Button - available if user is logged in and has history
|
727 |
+
if st.session_state.get("logged_in_user"):
|
728 |
+
user_history = get_user_history(st.session_state["logged_in_user"])
|
729 |
+
if user_history:
|
730 |
+
pdf_buffer = generate_pdf_report(user_history)
|
731 |
+
st.download_button(
|
732 |
+
label="Download Health Report (PDF)",
|
733 |
+
data=pdf_buffer,
|
734 |
+
file_name="medibot_health_report.pdf",
|
735 |
+
mime="application/pdf",
|
736 |
+
help="Download your chat and symptom checker history as a PDF report."
|
737 |
+
)
|
738 |
+
else:
|
739 |
+
st.info("No history to download yet. Interact with MediBot to generate a report.")
|
740 |
+
|
741 |
+
st.markdown("<h1 style='text-align: center; color: #0056b3;'>MediBot - Your Health Assistant 🩺</h1>", unsafe_allow_html=True)
|
742 |
+
|
743 |
+
# Top-level tabs: Home, History, Feedback, About, Insights
|
744 |
+
# Re-ordered tabs slightly to put Insights closer to History
|
745 |
+
main_tab_home, main_tab_history, main_tab_insights, main_tab_feedback, main_tab_about = st.tabs(["Home", "History", "Insights", "Feedback", "About"])
|
746 |
+
|
747 |
+
with main_tab_home:
|
748 |
+
st.markdown("<h2>How can I help you today?</h2>", unsafe_allow_html=True)
|
749 |
+
# Nested tabs for Symptom Checker and Chat with MediBot
|
750 |
+
tab_symptom_checker, tab_chatbot = st.tabs(["Symptom Checker", "Chat with MediBot"])
|
751 |
+
|
752 |
+
with tab_symptom_checker:
|
753 |
+
st.markdown("<h3>Select from common symptoms to get insights:</h3>", unsafe_allow_html=True)
|
754 |
+
selected_symptoms = st.multiselect(
|
755 |
+
"Select your symptoms:",
|
756 |
+
options=hardcoded_symptoms, # Use symptoms derived from CSV
|
757 |
+
default=[],
|
758 |
+
key="symptom_select",
|
759 |
+
help="Choose one or more symptoms you are experiencing."
|
760 |
+
)
|
761 |
+
|
762 |
+
if st.button("Get Health Insight", key="diagnose_button"):
|
763 |
+
if not selected_symptoms:
|
764 |
+
st.error("Please select at least one symptom.")
|
765 |
else:
|
766 |
+
with st.spinner("Analyzing symptoms..."):
|
767 |
+
# Get overall severity and predicted diagnoses from the Hugging Face model
|
768 |
+
combined_symptoms_text = ", ".join(selected_symptoms)
|
769 |
+
severity_assessment_text, predicted_diagnoses_from_model = get_diagnosis_and_severity_from_model(combined_symptoms_text)
|
770 |
+
|
771 |
+
# Get relevant disease info from your CSV datasets
|
772 |
+
diseases_from_csv = get_disease_info_from_csv(selected_symptoms, disease_symptoms_map, disease_description_map, disease_precaution_map, symptom_severity_map)
|
773 |
+
|
774 |
+
st.markdown(f"**{severity_assessment_text}**", unsafe_allow_html=True)
|
775 |
+
st.markdown("<h2>Possible Conditions and Insights:</h2>", unsafe_allow_html=True) # Unified Heading
|
776 |
+
|
777 |
+
# --- Process and display CSV-based diseases first ---
|
778 |
+
if diseases_from_csv:
|
779 |
+
st.markdown("<h4>From our Medical Knowledge Base:</h4>", unsafe_allow_html=True)
|
780 |
+
for disease_name, description, precautions_list in diseases_from_csv:
|
781 |
+
with st.expander(disease_name):
|
782 |
+
st.write(f"**Description**: {description}")
|
783 |
+
st.write("**Precautions**:")
|
784 |
+
if precautions_list:
|
785 |
+
for precaution in precautions_list:
|
786 |
+
if precaution: # Only display non-empty precautions
|
787 |
+
st.write(f"- {precaution}")
|
788 |
+
else:
|
789 |
+
st.write("- No specific precautions listed.")
|
790 |
+
st.markdown("---") # Separator for each disease in expander
|
791 |
else:
|
792 |
+
st.info("No common conditions found for these symptoms in our dataset. Please try adding more specific symptoms or switch to 'Chat with MediBot' for a general query.")
|
793 |
+
|
794 |
+
# --- Integrate relevant AI Model diagnoses not covered by CSV ---
|
795 |
+
additional_ai_diagnoses = []
|
796 |
+
# Create a set of normalized disease names from CSV for quick lookup and fuzzy matching
|
797 |
+
csv_disease_names_normalized = {d[0].lower() for d in diseases_from_csv}
|
798 |
+
|
799 |
+
for model_diag in predicted_diagnoses_from_model:
|
800 |
+
model_diag_normalized = model_diag.lower()
|
801 |
+
is_covered_by_csv = False
|
802 |
+
# Check for strong fuzzy match with CSV diseases
|
803 |
+
for csv_diag_name in csv_disease_names_normalized:
|
804 |
+
# Use token_set_ratio for better matching of multi-word terms, less sensitive to word order
|
805 |
+
if fuzz.token_set_ratio(model_diag_normalized, csv_diag_name) > 85: # High threshold for a strong match
|
806 |
+
is_covered_by_csv = True
|
807 |
+
break
|
808 |
+
if not is_covered_by_csv:
|
809 |
+
additional_ai_diagnoses.append(model_diag)
|
810 |
+
|
811 |
+
if additional_ai_diagnoses:
|
812 |
+
st.markdown("<h4>Additional AI-Suggested Clinical Considerations:</h4>", unsafe_allow_html=True)
|
813 |
+
st.write("The AI model suggests the following clinical terms based on your symptoms:")
|
814 |
+
for diag in additional_ai_diagnoses:
|
815 |
+
st.write(f"- **{diag}**: This is a medical term that the AI found relevant. Please consult a healthcare professional for more details.")
|
816 |
+
st.info("These are general medical terms and require professional interpretation. They may not have specific descriptions or precautions available in our dataset.")
|
817 |
+
elif not diseases_from_csv: # Only if no CSV diseases AND no unique AI diagnoses
|
818 |
+
st.info("The AI model could not confidently predict specific or unique diagnoses from the provided symptoms. Try different symptoms or consult a doctor.")
|
819 |
+
|
820 |
+
st.write("---") # Final Separator
|
821 |
+
|
822 |
+
# --- Save to history (adjusting content to reflect unified response) ---
|
823 |
+
history_content_parts = []
|
824 |
+
if diseases_from_csv:
|
825 |
+
history_content_parts.append("Dataset Conditions: " + ", ".join([d[0] for d in diseases_from_csv]))
|
826 |
+
if additional_ai_diagnoses:
|
827 |
+
history_content_parts.append("AI Suggestions: " + ", ".join(additional_ai_diagnoses))
|
828 |
+
|
829 |
+
# Combine all insight for history
|
830 |
+
predicted_diseases_str_for_history = f"{severity_assessment_text.replace('Overall Symptom Severity (AI Assessment): ', '')}. " + "; ".join(history_content_parts)
|
831 |
+
if not history_content_parts:
|
832 |
+
predicted_diseases_str_for_history = f"{severity_assessment_text.replace('Overall Symptom Severity (AI Assessment): ', '')}. No specific insights found."
|
833 |
+
|
834 |
+
if st.session_state.get("user_id"):
|
835 |
+
# Corrected call: Removed the first argument (user_id)
|
836 |
+
save_history_if_logged_in(", ".join(selected_symptoms), predicted_diseases_str_for_history)
|
837 |
+
st.success("Analysis saved to your history.")
|
838 |
+
else:
|
839 |
+
st.info("Log in to save this analysis to your history.")
|
840 |
+
|
841 |
+
with tab_chatbot: # Ask a Health Question (Chatbot)
|
842 |
+
st.markdown("<h3>Describe your symptoms or ask a general health question:</h3>", unsafe_allow_html=True)
|
843 |
+
# Use st.form with clear_on_submit=True to clear the text area
|
844 |
+
with st.form("chat_form", clear_on_submit=True):
|
845 |
+
user_question = st.text_area(
|
846 |
+
"Input your issue:",
|
847 |
+
value=st.session_state.chat_input_value, # Use session state for value
|
848 |
+
key="chat_input_widget", # Unique widget key
|
849 |
+
placeholder="e.g., I am having a severe fever and body aches. Or, what are the causes of high blood pressure?",
|
850 |
+
height=120 # Slightly taller text area
|
851 |
+
)
|
852 |
+
chat_submit_button = st.form_submit_button("Ask MediBot")
|
853 |
+
|
854 |
+
if chat_submit_button:
|
855 |
+
if user_question:
|
856 |
+
# For the chatbot, if it's a direct question, severity is undetermined
|
857 |
+
# unless passed from a preceding symptom check.
|
858 |
+
# For simplicity, if not from symptom checker, pass "Undetermined Severity".
|
859 |
+
# You could enhance this to try to infer severity from the question itself if needed.
|
860 |
+
severity_for_groq_prompt = "Undetermined Severity"
|
861 |
+
# If you want to use the severity from a recent symptom check,
|
862 |
+
# you'd need to store it in session_state and pass it here.
|
863 |
+
# For now, we're keeping the Q&A separate.
|
864 |
+
with st.spinner("MediBot is thinking..."):
|
865 |
+
groq_answer = get_groq_response(user_question, severity_label=severity_for_groq_prompt)
|
866 |
+
st.markdown("**MediBot's Answer:**")
|
867 |
+
st.write(groq_answer)
|
868 |
+
|
869 |
+
# Save Q&A to chat history (if desired, not necessarily DB)
|
870 |
+
st.session_state.chat_history.append({"user": user_question, "bot": groq_answer})
|
871 |
+
|
872 |
+
# Save to database history if logged in
|
873 |
+
if st.session_state.get("user_id"):
|
874 |
+
# Corrected call: Removed the first argument (user_id)
|
875 |
+
save_history_if_logged_in(f"Question: {user_question}", groq_answer)
|
876 |
+
st.success("Your question and answer have been saved to history.")
|
877 |
+
else:
|
878 |
+
st.info("Log in to save this Q&A to your history.")
|
879 |
+
else:
|
880 |
+
st.warning("Please type your question to get an answer.")
|
881 |
+
|
882 |
+
with main_tab_history:
|
883 |
+
st.header("Your Health History")
|
884 |
+
user_id = st.session_state.get("user_id")
|
885 |
+
if user_id:
|
886 |
+
st.info("This section shows your saved symptom analyses and health questions.")
|
887 |
+
history_data = get_user_history(user_id) # Fetch history from database
|
888 |
+
|
889 |
+
if history_data:
|
890 |
+
st.subheader("Past Interactions:")
|
891 |
+
# Display history in reverse chronological order using expanders
|
892 |
+
for entry in reversed(history_data):
|
893 |
+
timestamp, symptoms_text, insight_text = entry[3], entry[1], entry[2]
|
894 |
+
|
895 |
+
# Determine summary title based on the nature of the input
|
896 |
+
summary_title_prefix = ""
|
897 |
+
expander_icon = "" # Initialize icon here
|
898 |
+
if symptoms_text.startswith("Question: "):
|
899 |
+
keyword = extract_keyword(symptoms_text, hardcoded_symptoms)
|
900 |
+
summary_title_prefix = f"Question: {keyword}" if keyword != "Unknown" else "General Question"
|
901 |
+
expander_icon = "💬"
|
902 |
+
else: # Symptom checker entry (🩺)
|
903 |
+
summary_conditions = parse_insight_text_for_conditions(insight_text)
|
904 |
+
if summary_conditions:
|
905 |
+
summary_title_prefix = ", ".join(summary_conditions[:3])
|
906 |
+
if len(summary_conditions) > 3:
|
907 |
+
summary_title_prefix += "..."
|
908 |
+
else:
|
909 |
+
# MODIFIED LOGIC: If no conditions identified, try to use input symptoms
|
910 |
+
input_symptoms_match = re.search(r"Symptoms/Question: (.*)", symptoms_text) # Adjusted regex for "Symptoms/Question:"
|
911 |
+
if input_symptoms_match:
|
912 |
+
extracted_input_symptoms = input_symptoms_match.group(1).strip()
|
913 |
+
# Clean up potential extra spaces/commas, take first few if too many
|
914 |
+
clean_symptoms = [s.strip() for s in extracted_input_symptoms.split(',') if s.strip()]
|
915 |
+
if clean_symptoms:
|
916 |
+
summary_title_prefix = ", ".join(clean_symptoms[:3])
|
917 |
+
if len(clean_symptoms) > 3:
|
918 |
+
summary_title_prefix += "..."
|
919 |
else:
|
920 |
+
summary_title_prefix = "No conditions identified" # Fallback if no symptoms extracted
|
921 |
+
else:
|
922 |
+
summary_title_prefix = "No conditions identified" # Fallback if input format is unexpected
|
923 |
+
expander_icon = "🩺"
|
924 |
+
|
925 |
+
# MODIFIED LINE: Use Markdown heading (e.g., ###) for larger font and bolding
|
926 |
+
# Experiment with ## or ### to find the desired size.
|
927 |
+
expander_label = f"### **{timestamp.strftime('%Y-%m-%d %H:%M')}** - {expander_icon} *{summary_title_prefix}*"
|
928 |
+
with st.expander(expander_label):
|
929 |
+
st.write(f"**Your Input:** {symptoms_text}")
|
930 |
+
st.write(f"**MediBot's Insight:** {insight_text}")
|
931 |
+
|
932 |
+
# PDF Download button
|
933 |
+
pdf_buffer = generate_pdf_report(history_data)
|
934 |
+
st.download_button(
|
935 |
+
label="Download History as PDF",
|
936 |
+
data=pdf_buffer,
|
937 |
+
file_name="MediBot_Health_Report.pdf",
|
938 |
+
mime="application/pdf"
|
939 |
+
)
|
940 |
+
else:
|
941 |
+
st.info("No history found for your account. Start interacting with MediBot!")
|
942 |
+
else:
|
943 |
+
st.warning("Please log in to view your health history.")
|
944 |
+
|
945 |
+
with main_tab_insights:
|
946 |
+
st.header("Your Health Insights")
|
947 |
+
user_id = st.session_state.get("user_id")
|
948 |
+
if user_id:
|
949 |
+
st.info("This section provides insights into the conditions identified in your past interactions.")
|
950 |
+
history_data = get_user_history(user_id)
|
951 |
+
if history_data:
|
952 |
+
# Filter data for the last 30 days
|
953 |
+
thirty_days_ago = datetime.now() - timedelta(days=30)
|
954 |
+
recent_history = [
|
955 |
+
entry for entry in history_data
|
956 |
+
if entry[3] and entry[3] > thirty_days_ago # entry[3] is query_timestamp
|
957 |
+
]
|
958 |
+
|
959 |
+
if recent_history:
|
960 |
+
all_conditions = []
|
961 |
+
for entry in recent_history:
|
962 |
+
# entry[2] is predicted_diseases string
|
963 |
+
conditions_from_entry = parse_insight_text_for_conditions(entry[2])
|
964 |
+
all_conditions.extend(conditions_from_entry)
|
965 |
+
|
966 |
+
if all_conditions:
|
967 |
+
# Count occurrences of each condition
|
968 |
+
condition_counts = pd.Series(all_conditions).value_counts().reset_index()
|
969 |
+
condition_counts.columns = ['Condition', 'Count']
|
970 |
+
st.subheader("Most Frequent Conditions in Last 30 Days:")
|
971 |
+
# Display as a bar chart
|
972 |
+
st.bar_chart(condition_counts, x='Condition', y='Count', use_container_width=True)
|
973 |
+
st.write("This chart shows how many times each condition (from both dataset and AI suggestions) appeared in your analyses over the past 30 days.")
|
974 |
+
else:
|
975 |
+
st.info("No specific conditions were identified in your recent history (last 30 days).")
|
976 |
+
else:
|
977 |
+
st.info("You have no health interactions in the last 30 days to generate insights.")
|
978 |
+
else:
|
979 |
+
st.info("No history found for your account. Start interacting with MediBot to see insights!")
|
980 |
+
else:
|
981 |
+
st.warning("Please log in to view your health insights.")
|
982 |
+
|
983 |
+
with main_tab_feedback:
|
984 |
+
st.header("Share Your Feedback")
|
985 |
+
st.write("We appreciate your feedback to improve MediBot.")
|
986 |
+
with st.form("feedback_form", clear_on_submit=True):
|
987 |
+
feedback_text = st.text_area("Your feedback:", height=150, key="feedback_text_area")
|
988 |
+
rating = st.slider("Rate your experience (1-5 stars):", 1, 5, 3, key="feedback_rating")
|
989 |
+
feedback_submit_button = st.form_submit_button("Submit Feedback")
|
990 |
+
|
991 |
+
if feedback_submit_button:
|
992 |
+
if feedback_text:
|
993 |
+
# In a real app, you would save this to a database
|
994 |
+
st.session_state.feedback.append({"text": feedback_text, "rating": rating, "timestamp": datetime.now()})
|
995 |
+
st.success("Thank you for your feedback! It has been submitted.")
|
996 |
+
st.session_state.feedback_input = "" # Clear after submission
|
997 |
+
else:
|
998 |
+
st.warning("Please enter your feedback before submitting.")
|
999 |
+
|
1000 |
+
with main_tab_about:
|
1001 |
+
st.header("About MediBot")
|
1002 |
+
st.write("""
|
1003 |
+
MediBot is a health assistant designed to provide general information based on your symptoms and answer health-related questions.
|
1004 |
+
It utilizes various AI models and medical datasets to offer insights.
|
1005 |
+
|
1006 |
+
**Technology Stack:**
|
1007 |
+
- **Streamlit**: For the interactive web application interface.
|
1008 |
+
- **Groq API**: Powers the conversational AI for health questions using high-performance language models (e.g., Llama-3), enabling rapid and relevant responses.
|
1009 |
+
- **Hugging Face Inference API**: Integrates with specialized medical text classification models, specifically `DATEXIS/CORe-clinical-diagnosis-prediction`, to predict possible clinical diagnoses (often mapping to ICD-9 codes) from symptom inputs.
|
1010 |
+
- **Pandas**: For efficient data handling and processing of local CSV medical datasets (symptoms, descriptions, precautions).
|
1011 |
+
- **ReportLab**: For generating downloadable PDF reports of your personalized health history.
|
1012 |
+
|
1013 |
+
**Datasets Used:**
|
1014 |
+
- Symptom-to-Disease mapping, Descriptions, and Precautions sourced from:
|
1015 |
+
"Disease Symptom Prediction" dataset available on Kaggle.
|
1016 |
+
[https://www.kaggle.com/datasets/itachi9604/disease-symptom-description-dataset](https://www.kaggle.com/datasets/itachi9604/disease-symptom-description-dataset)
|
1017 |
+
*Credit to Itachi9604 for compiling this valuable dataset.*
|
1018 |
+
|
1019 |
+
**Important Disclaimer**: This application is for informational purposes only and should not be used as a substitute for professional medical advice, diagnosis, or treatment. Always consult with a qualified healthcare provider for any health concerns or before making any decisions related to your health.
|
1020 |
+
""")
|
1021 |
+
st.markdown("[Learn more about Streamlit](https://streamlit.io/)")
|
1022 |
+
st.markdown("[Learn more about Groq](https://groq.com/)")
|
1023 |
+
st.markdown("[Learn more about Hugging Face](https://huggingface.co/)")
|
1024 |
+
|
1025 |
+
st.markdown("---")
|
1026 |
+
st.markdown("For professional medical advice, always consult a qualified healthcare provider.")
|