Spaces:
Sleeping
Sleeping
Update user_management.py
Browse files- user_management.py +95 -1016
user_management.py
CHANGED
@@ -1,1026 +1,105 @@
|
|
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
|
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 |
-
#
|
50 |
-
|
51 |
-
|
|
|
52 |
try:
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
st.warning("
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
#
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
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 |
-
|
453 |
-
|
454 |
"""
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
|
460 |
-
|
461 |
-
|
462 |
-
|
463 |
-
|
464 |
-
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
if
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
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.
|
881 |
-
|
882 |
-
|
883 |
-
|
884 |
-
|
885 |
-
|
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 |
-
|
923 |
-
|
924 |
-
|
925 |
-
|
926 |
-
|
927 |
-
|
928 |
-
|
929 |
-
|
930 |
-
|
931 |
-
|
932 |
-
|
933 |
-
|
934 |
-
|
935 |
-
|
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.
|
976 |
-
|
977 |
-
|
978 |
-
|
979 |
-
|
980 |
-
|
981 |
-
|
982 |
-
|
983 |
-
|
984 |
-
|
985 |
-
|
986 |
-
|
987 |
-
|
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 |
-
|
1020 |
-
|
1021 |
-
|
1022 |
-
|
1023 |
-
|
|
|
|
|
|
|
|
|
1024 |
|
1025 |
-
st.markdown("---")
|
1026 |
-
st.markdown("For professional medical advice, always consult a qualified healthcare provider.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import streamlit as st
|
2 |
+
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
|
4 |
+
# Import database functions directly here.
|
5 |
+
# user_management.py is now responsible for handling its own database interactions.
|
6 |
+
# It should NOT import anything from app.py
|
7 |
+
# Assuming these functions are defined in database.py
|
8 |
try:
|
9 |
+
from database import register_user, user_exists, check_user_credentials, save_user_history, get_user_history
|
10 |
+
except ImportError:
|
11 |
+
st.error("Could not import database functions in user_management.py. Please ensure database.py exists and contains required functions.")
|
12 |
+
# Define dummy functions to allow the app to run without a real database.py for now
|
13 |
+
def get_user_history(user_id):
|
14 |
+
st.warning("Database function 'get_user_history' not implemented. History will not be persistent.")
|
15 |
+
return []
|
16 |
+
def register_user(user_id, full_name, dob, email, password):
|
17 |
+
st.warning("Database function 'register_user' not implemented. User registration will not be persistent.")
|
18 |
+
return True # Simulate success
|
19 |
+
def user_exists(user_id):
|
20 |
+
st.warning("Database function 'user_exists' not implemented. User existence check will not work.")
|
21 |
+
return False # Simulate user not existing
|
22 |
+
def check_user_credentials(user_id, password):
|
23 |
+
st.warning("Database function 'check_user_credentials' not implemented. Login will not work.")
|
24 |
+
return False # Simulate login failure
|
25 |
+
def save_user_history(user_id, symptoms, predicted_diseases):
|
26 |
+
st.warning("Database function 'save_user_history' not implemented. History saving will not be persistent.")
|
27 |
+
pass
|
28 |
+
|
29 |
+
|
30 |
+
def render_user_management_sidebar(session_state):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
"""
|
32 |
+
Handles user registration and login forms in the sidebar.
|
33 |
+
Receives the Streamlit session_state object to manage user login status.
|
34 |
"""
|
35 |
+
st.sidebar.header("User Management 🧑⚕️")
|
36 |
+
|
37 |
+
# Check if a user is currently logged in via session_state
|
38 |
+
if session_state.get("user_id"):
|
39 |
+
st.sidebar.write(f"Welcome, {session_state.user_id}!")
|
40 |
+
if st.sidebar.button("Logout", key="logout_button_sidebar"): # Unique key for sidebar button
|
41 |
+
session_state.user_id = None
|
42 |
+
session_state.logged_in_user = None # Clear this too upon logout
|
43 |
+
st.success("Logged out successfully!")
|
44 |
+
st.rerun() # Rerun to update UI immediately
|
45 |
+
else:
|
46 |
+
# Toggle between sign-in and register forms
|
47 |
+
form_type = st.sidebar.selectbox("Choose Action", ["Sign In", "Register"], key="form_type_sidebar")
|
48 |
+
|
49 |
+
if form_type == "Sign In":
|
50 |
+
with st.sidebar.form("login_form_sidebar"): # Unique form key
|
51 |
+
st.markdown("### Sign In")
|
52 |
+
login_user_id = st.text_input("User ID", key="login_user_id_input_sidebar") # Unique widget key
|
53 |
+
login_password = st.text_input("Password", type="password", key="login_password_input_sidebar") # Unique widget key
|
54 |
+
submit_login = st.form_submit_button("Sign In")
|
55 |
+
|
56 |
+
if submit_login:
|
57 |
+
if not login_user_id or not login_password:
|
58 |
+
st.sidebar.error("Please enter both User ID and Password.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
else:
|
60 |
+
with st.spinner("Signing in..."): # Spinner for login
|
61 |
+
if check_user_credentials(login_user_id, login_password):
|
62 |
+
session_state.user_id = login_user_id
|
63 |
+
session_state.logged_in_user = login_user_id # Set logged_in_user in session state
|
64 |
+
st.sidebar.success("Signed in successfully!")
|
65 |
+
st.rerun() # Rerun to update UI with logged-in state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
else:
|
67 |
+
st.sidebar.error("Invalid User ID or Password.")
|
68 |
+
else: # Register
|
69 |
+
with st.sidebar.form("register_form_sidebar"): # Unique form key
|
70 |
+
st.markdown("### Register")
|
71 |
+
reg_user_id = st.text_input("Choose a unique User ID", key="reg_user_id_input_sidebar") # Unique widget key
|
72 |
+
full_name = st.text_input("Full Name", key="reg_full_name_input_sidebar") # Unique widget key
|
73 |
+
dob = st.date_input("Date of Birth", min_value=datetime(1900, 1, 1), max_value=datetime.today(), key="reg_dob_input_sidebar") # Unique widget key
|
74 |
+
email = st.text_input("Email (optional)", key="reg_email_input_sidebar") # Unique widget key
|
75 |
+
reg_password = st.text_input("Password", type="password", key="reg_password_input_sidebar") # Unique widget key
|
76 |
+
submit_register = st.form_submit_button("Register")
|
77 |
+
|
78 |
+
if submit_register:
|
79 |
+
if not reg_user_id or not reg_password or not full_name:
|
80 |
+
st.sidebar.error("User ID, Full Name, and Password are required.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
else:
|
82 |
+
with st.spinner("Checking User ID..."): # Spinner for user_exists check
|
83 |
+
if user_exists(reg_user_id):
|
84 |
+
st.sidebar.error("User ID already taken. Please choose another.")
|
85 |
+
else:
|
86 |
+
with st.spinner("Registering user..."): # Spinner for registration
|
87 |
+
# Pass dob as a datetime.date object; database function should handle conversion if needed
|
88 |
+
if register_user(reg_user_id, full_name, dob, email or None, reg_password):
|
89 |
+
session_state.user_id = reg_user_id
|
90 |
+
session_state.logged_in_user = reg_user_id # Set logged_in_user in session state
|
91 |
+
st.sidebar.success("Registered successfully! You are now logged in.")
|
92 |
+
st.rerun() # Rerun to update UI with logged-in state
|
93 |
+
else:
|
94 |
+
st.sidebar.error("Registration failed. Please try again.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
|
96 |
+
def save_history_to_db_if_logged_in(user_id: str, symptoms: str, predicted_diseases: str):
|
97 |
+
"""
|
98 |
+
Saves user interaction history to the database.
|
99 |
+
This function is called from app.py when a user is logged in.
|
100 |
+
It directly calls the database function to save data.
|
101 |
+
"""
|
102 |
+
# This function assumes user_id is already validated (i.e., user is logged in)
|
103 |
+
with st.spinner("Saving history..."): # Spinner for saving history
|
104 |
+
save_user_history(user_id, symptoms, predicted_diseases)
|
105 |
|
|
|
|