Update app.py
Browse files
app.py
CHANGED
@@ -37,34 +37,59 @@ if 'chat_messages' not in st.session_state: # For the current active chat
|
|
37 |
def display_login_form():
|
38 |
with st.form("login_form"):
|
39 |
st.subheader("Login")
|
40 |
-
|
41 |
-
|
42 |
submit_button = st.form_submit_button("Login")
|
43 |
|
44 |
if submit_button:
|
45 |
-
user
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
if user:
|
47 |
st.session_state.authenticated_user = user
|
|
|
|
|
|
|
48 |
st.success(f"Welcome back, {user.username}!")
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
else:
|
59 |
st.error("Invalid username or password.")
|
60 |
|
61 |
def display_signup_form():
|
62 |
with st.form("signup_form"):
|
63 |
st.subheader("Sign Up")
|
64 |
-
new_username = st.text_input("Choose a Username")
|
65 |
-
new_email = st.text_input("Email (Optional)")
|
66 |
-
new_password = st.text_input("Choose a Password", type="password")
|
67 |
-
confirm_password = st.text_input("Confirm Password", type="password")
|
68 |
submit_button = st.form_submit_button("Sign Up")
|
69 |
|
70 |
if submit_button:
|
@@ -78,20 +103,46 @@ def display_signup_form():
|
|
78 |
password=new_password,
|
79 |
email=new_email if new_email else None
|
80 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
user = create_user_in_db(user_data)
|
82 |
if user:
|
83 |
-
|
84 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
# st.session_state.authenticated_user = user
|
86 |
# st.rerun()
|
|
|
|
|
|
|
87 |
else:
|
88 |
-
st.error("Username might already be taken or
|
|
|
89 |
|
90 |
# --- Main App Logic ---
|
91 |
if not st.session_state.authenticated_user:
|
92 |
st.title(f"Welcome to {settings.APP_TITLE}")
|
93 |
st.markdown("Your AI-powered partner for advanced healthcare insights.")
|
94 |
-
|
95 |
login_tab, signup_tab = st.tabs(["Login", "Sign Up"])
|
96 |
with login_tab:
|
97 |
display_login_form()
|
@@ -102,26 +153,48 @@ else:
|
|
102 |
# The content of `app.py` typically acts as the "Home" page if no `1_Home.py` exists,
|
103 |
# or it can be used for global elements like a custom sidebar if not using Streamlit's default page navigation.
|
104 |
|
105 |
-
# Custom Sidebar for logged-in users
|
106 |
with st.sidebar:
|
107 |
-
|
108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
st.markdown("---")
|
110 |
-
|
111 |
if st.button("Logout"):
|
|
|
112 |
st.session_state.authenticated_user = None
|
113 |
st.session_state.current_chat_session_id = None
|
114 |
st.session_state.chat_messages = []
|
115 |
st.success("You have been logged out.")
|
116 |
st.rerun()
|
117 |
-
|
118 |
# This content will show if no other page is selected, or if you don't have a 1_Home.py
|
119 |
# If you have 1_Home.py, Streamlit will show that by default after login.
|
120 |
-
|
121 |
-
|
122 |
-
|
|
|
123 |
st.markdown("Navigate using the sidebar to consult with the AI or view your reports.")
|
124 |
st.markdown("---")
|
125 |
st.info("This is the main application area. If you see this, ensure you have a `pages/1_Home.py` or that this `app.py` is your intended landing page after login.")
|
126 |
|
127 |
-
|
|
|
|
|
|
|
|
|
|
37 |
def display_login_form():
|
38 |
with st.form("login_form"):
|
39 |
st.subheader("Login")
|
40 |
+
username_input = st.text_input("Username", key="login_username_input") # Added key for clarity
|
41 |
+
password_input = st.text_input("Password", type="password", key="login_password_input") # Added key
|
42 |
submit_button = st.form_submit_button("Login")
|
43 |
|
44 |
if submit_button:
|
45 |
+
# IMPORTANT: The 'user' object returned by authenticate_user needs to have
|
46 |
+
# its essential attributes (like id, username) already loaded, or if its session
|
47 |
+
# was closed, those attributes should not be in an "expired" state that requires
|
48 |
+
# a database refresh. Typically, a query like db.query(User)...first() loads these.
|
49 |
+
# If authenticate_user involves a commit and session.expire_on_commit=True (default),
|
50 |
+
# it should call db.refresh(user_object) before closing its session and returning.
|
51 |
+
user = authenticate_user(username_input, password_input)
|
52 |
if user:
|
53 |
st.session_state.authenticated_user = user
|
54 |
+
# Accessing user.username and user.id here.
|
55 |
+
# If 'user' is detached and attributes are expired, this could also fail.
|
56 |
+
# This implies authenticate_user should return a "usable" object.
|
57 |
st.success(f"Welcome back, {user.username}!")
|
58 |
+
try:
|
59 |
+
with get_session_context() as db_session:
|
60 |
+
# Ensure the user object is attached to the current session if needed,
|
61 |
+
# or that its ID is accessible even if detached.
|
62 |
+
# user_id = user.id (if user.id is loaded, this is fine)
|
63 |
+
# live_user = db_session.merge(user) # if user might be detached but has PK
|
64 |
+
# Or better if only ID is needed:
|
65 |
+
live_user = db_session.get(User, user.id) # Re-fetch/attach to current session
|
66 |
+
if not live_user:
|
67 |
+
st.error("User session error. Please log in again.")
|
68 |
+
app_logger.error(f"Failed to re-fetch user with id {user.id} in new session.")
|
69 |
+
st.session_state.authenticated_user = None # Clear broken state
|
70 |
+
st.rerun()
|
71 |
+
return
|
72 |
+
|
73 |
+
new_chat_session = ChatSession(user_id=live_user.id, title=f"Session for {live_user.username}")
|
74 |
+
db_session.add(new_chat_session)
|
75 |
+
db_session.commit()
|
76 |
+
db_session.refresh(new_chat_session) # Refresh new_chat_session
|
77 |
+
st.session_state.current_chat_session_id = new_chat_session.id
|
78 |
+
st.session_state.chat_messages = [] # Clear previous messages
|
79 |
+
st.rerun() # Rerun to reflect login state
|
80 |
+
except Exception as e:
|
81 |
+
app_logger.error(f"Error creating chat session for user {user.username}: {e}")
|
82 |
+
st.error(f"Could not start a new session: {e}")
|
83 |
else:
|
84 |
st.error("Invalid username or password.")
|
85 |
|
86 |
def display_signup_form():
|
87 |
with st.form("signup_form"):
|
88 |
st.subheader("Sign Up")
|
89 |
+
new_username = st.text_input("Choose a Username", key="signup_username_input") # Added key
|
90 |
+
new_email = st.text_input("Email (Optional)", key="signup_email_input") # Added key
|
91 |
+
new_password = st.text_input("Choose a Password", type="password", key="signup_password_input") # Added key
|
92 |
+
confirm_password = st.text_input("Confirm Password", type="password", key="signup_confirm_password_input") # Added key
|
93 |
submit_button = st.form_submit_button("Sign Up")
|
94 |
|
95 |
if submit_button:
|
|
|
103 |
password=new_password,
|
104 |
email=new_email if new_email else None
|
105 |
)
|
106 |
+
# --- Explanation of the DetachedInstanceError and its handling ---
|
107 |
+
# The 'create_user_in_db' function creates a User and commits it to the database.
|
108 |
+
# If the SQLAlchemy session used inside 'create_user_in_db' has 'expire_on_commit=True'
|
109 |
+
# (which is the default), all attributes of the 'user' object are marked as "expired"
|
110 |
+
# after the commit.
|
111 |
+
# If 'create_user_in_db' then closes its session and returns this 'user' object,
|
112 |
+
# the object becomes "detached".
|
113 |
+
# When you later try to access an expired attribute (like 'user.username'),
|
114 |
+
# SQLAlchemy attempts to reload it from the database. Since the object is detached
|
115 |
+
# (not bound to an active session), this reload fails, raising DetachedInstanceError.
|
116 |
+
|
117 |
+
# TO FIX ROBUSTLY (in services/auth.py, inside create_user_in_db):
|
118 |
+
# After `db_session.commit()`, ensure `db_session.refresh(created_user_object)` is called
|
119 |
+
# *before* the session is closed and the object is returned. This loads all attributes
|
120 |
+
# from the database, so they are no longer "expired".
|
121 |
+
|
122 |
user = create_user_in_db(user_data)
|
123 |
if user:
|
124 |
+
# IMMEDIATE FIX for this specific line:
|
125 |
+
# Instead of 'user.username' (which might be on a detached, expired instance),
|
126 |
+
# use 'new_username', which is the value just submitted by the user and
|
127 |
+
# used for creation. This avoids the need to access the potentially problematic 'user' object attribute.
|
128 |
+
st.success(f"Account created for {new_username}. Please log in.")
|
129 |
+
app_logger.info(f"Account created for {new_username}.")
|
130 |
+
|
131 |
+
# If you were to enable direct login here:
|
132 |
# st.session_state.authenticated_user = user
|
133 |
# st.rerun()
|
134 |
+
# ...then it becomes CRITICAL that 'create_user_in_db' returns a 'user' object
|
135 |
+
# whose attributes (like .id, .username) are already loaded and not expired,
|
136 |
+
# as explained in the "TO FIX ROBUSTLY" comment above.
|
137 |
else:
|
138 |
+
st.error("Username might already be taken or another error occurred during signup.")
|
139 |
+
app_logger.warning(f"Failed to create user for username: {new_username}")
|
140 |
|
141 |
# --- Main App Logic ---
|
142 |
if not st.session_state.authenticated_user:
|
143 |
st.title(f"Welcome to {settings.APP_TITLE}")
|
144 |
st.markdown("Your AI-powered partner for advanced healthcare insights.")
|
145 |
+
|
146 |
login_tab, signup_tab = st.tabs(["Login", "Sign Up"])
|
147 |
with login_tab:
|
148 |
display_login_form()
|
|
|
153 |
# The content of `app.py` typically acts as the "Home" page if no `1_Home.py` exists,
|
154 |
# or it can be used for global elements like a custom sidebar if not using Streamlit's default page navigation.
|
155 |
|
156 |
+
# Custom Sidebar for logged-in users
|
157 |
with st.sidebar:
|
158 |
+
# Ensure authenticated_user object is usable. If it was detached with expired attributes
|
159 |
+
# upon login, this access could also fail. This reinforces the need for authenticate_user
|
160 |
+
# to return a "live" or fully-loaded object.
|
161 |
+
try:
|
162 |
+
st.markdown(f"### Welcome, {st.session_state.authenticated_user.username}!")
|
163 |
+
except AttributeError: # Fallback if username is not accessible
|
164 |
+
st.markdown(f"### Welcome!")
|
165 |
+
app_logger.error("Could not access username for authenticated_user in sidebar.")
|
166 |
+
|
167 |
+
if st.session_state.get("SHOW_LOGO_IN_SIDEBAR", True) and settings.LOGO_PATH and Path(settings.LOGO_PATH).exists(): # Example for conditional logo
|
168 |
+
try:
|
169 |
+
st.image(settings.LOGO_PATH, width=100)
|
170 |
+
except Exception as e:
|
171 |
+
app_logger.warning(f"Could not load logo from {settings.LOGO_PATH}: {e}")
|
172 |
+
elif settings.APP_TITLE: # Fallback to title if no logo
|
173 |
+
st.markdown(f"#### {settings.APP_TITLE}")
|
174 |
+
|
175 |
+
|
176 |
st.markdown("---")
|
177 |
+
|
178 |
if st.button("Logout"):
|
179 |
+
app_logger.info(f"User {st.session_state.authenticated_user.username} logging out.")
|
180 |
st.session_state.authenticated_user = None
|
181 |
st.session_state.current_chat_session_id = None
|
182 |
st.session_state.chat_messages = []
|
183 |
st.success("You have been logged out.")
|
184 |
st.rerun()
|
185 |
+
|
186 |
# This content will show if no other page is selected, or if you don't have a 1_Home.py
|
187 |
# If you have 1_Home.py, Streamlit will show that by default after login.
|
188 |
+
st.sidebar.success("Select a page above to get started.") # Or from the main area below if multi-page app
|
189 |
+
|
190 |
+
# Main area content (could be your "Home" page)
|
191 |
+
st.header(f"Dashboard - {settings.APP_TITLE}")
|
192 |
st.markdown("Navigate using the sidebar to consult with the AI or view your reports.")
|
193 |
st.markdown("---")
|
194 |
st.info("This is the main application area. If you see this, ensure you have a `pages/1_Home.py` or that this `app.py` is your intended landing page after login.")
|
195 |
|
196 |
+
|
197 |
+
# Ensure Path is imported if used for logo
|
198 |
+
from pathlib import Path
|
199 |
+
|
200 |
+
app_logger.info(f"Streamlit app '{settings.APP_TITLE}' initialized and running.")
|