mgbam commited on
Commit
e9c724f
·
verified ·
1 Parent(s): 04ae885

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +86 -88
app.py CHANGED
@@ -1,4 +1,6 @@
1
  import streamlit as st
 
 
2
  from config.settings import settings
3
  from models import create_db_and_tables, get_session_context, User, ChatMessage, ChatSession
4
  from models.user import UserCreate # For type hinting
@@ -10,7 +12,7 @@ from services.logger import app_logger
10
  # --- Page Configuration ---
11
  st.set_page_config(
12
  page_title=settings.APP_TITLE,
13
- page_icon="⚕️", # You can use an emoji or a path to an image
14
  layout="wide",
15
  initial_sidebar_state="expanded"
16
  )
@@ -26,7 +28,7 @@ init_db()
26
 
27
  # --- Session State Initialization ---
28
  if 'authenticated_user' not in st.session_state:
29
- st.session_state.authenticated_user = None # Stores User object upon successful login
30
  if 'current_chat_session_id' not in st.session_state:
31
  st.session_state.current_chat_session_id = None
32
  if 'chat_messages' not in st.session_state: # For the current active chat
@@ -37,59 +39,76 @@ 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
- 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,40 +122,21 @@ def display_signup_form():
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:
@@ -149,52 +149,50 @@ if not st.session_state.authenticated_user:
149
  with signup_tab:
150
  display_signup_form()
151
  else:
152
- # If authenticated, Streamlit automatically navigates to pages in the `pages/` directory.
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.")
 
1
  import streamlit as st
2
+ from pathlib import Path # Ensure Path is imported if used for settings.LOGO_PATH
3
+
4
  from config.settings import settings
5
  from models import create_db_and_tables, get_session_context, User, ChatMessage, ChatSession
6
  from models.user import UserCreate # For type hinting
 
12
  # --- Page Configuration ---
13
  st.set_page_config(
14
  page_title=settings.APP_TITLE,
15
+ page_icon="⚕️",
16
  layout="wide",
17
  initial_sidebar_state="expanded"
18
  )
 
28
 
29
  # --- Session State Initialization ---
30
  if 'authenticated_user' not in st.session_state:
31
+ st.session_state.authenticated_user = None # Stores User object (ideally one that is "live" or has loaded attributes)
32
  if 'current_chat_session_id' not in st.session_state:
33
  st.session_state.current_chat_session_id = None
34
  if 'chat_messages' not in st.session_state: # For the current active chat
 
39
  def display_login_form():
40
  with st.form("login_form"):
41
  st.subheader("Login")
42
+ # Use unique keys for inputs if there's any chance of collision or for better state tracking
43
+ username_input = st.text_input("Username", key="login_username_input")
44
+ password_input = st.text_input("Password", type="password", key="login_password_input")
45
  submit_button = st.form_submit_button("Login")
46
 
47
  if submit_button:
48
+ # IMPORTANT: 'authenticate_user' should return a User object where essential attributes
49
+ # like 'id' and 'username' are already loaded (not expired).
50
+ # If 'authenticate_user' involves a commit (e.g., updating last_login) and
51
+ # session.expire_on_commit=True (default), it should call db.refresh(user_object)
52
+ # before closing its session and returning the user object.
53
+ user_from_auth = authenticate_user(username_input, password_input)
54
+
55
+ if user_from_auth:
56
+ # FIX 1: For the success message, use the 'username_input' from the form directly.
57
+ # This avoids accessing 'user_from_auth.username' which might be on a detached/expired instance.
58
+ st.success(f"Welcome back, {username_input}!")
59
+ app_logger.info(f"User {username_input} logged in successfully.")
60
+
61
  try:
62
  with get_session_context() as db_session:
63
+ # FIX 2: Re-fetch or merge the user into the current db_session to ensure it's "live"
64
+ # This assumes 'user_from_auth.id' is accessible (i.e., was loaded by authenticate_user).
65
+ # If 'user_from_auth.id' itself is expired, 'authenticate_user' MUST be fixed.
66
+ try:
67
+ user_id = user_from_auth.id
68
+ except AttributeError:
69
+ st.error("Authentication error: User data is incomplete. Please try again.")
70
+ app_logger.error(f"User object from authenticate_user for '{username_input}' lacks 'id' attribute or it's inaccessible.")
71
+ st.session_state.authenticated_user = None # Clear potentially bad state
72
+ st.rerun()
73
+ return
74
+
75
+ live_user = db_session.get(User, user_id)
76
  if not live_user:
77
+ st.error("Critical user session error. Please log out and log in again.")
78
+ app_logger.error(f"Failed to re-fetch user with id {user_id} (username: {username_input}) in new session.")
79
  st.session_state.authenticated_user = None # Clear broken state
80
  st.rerun()
81
  return
82
 
83
+ # Store the "live" user object (attached to db_session or with loaded attributes)
84
+ # in session_state. This 'live_user' will be used by the sidebar.
85
+ st.session_state.authenticated_user = live_user
86
+
87
+ # Now use 'live_user' for creating the chat session
88
  new_chat_session = ChatSession(user_id=live_user.id, title=f"Session for {live_user.username}")
89
  db_session.add(new_chat_session)
90
  db_session.commit()
91
+ db_session.refresh(new_chat_session) # Refresh to get DB-generated values for new_chat_session
92
  st.session_state.current_chat_session_id = new_chat_session.id
93
  st.session_state.chat_messages = [] # Clear previous messages
94
+ st.rerun() # Rerun to reflect login state and navigate
95
  except Exception as e:
96
+ app_logger.error(f"Error creating chat session for user {username_input}: {e}", exc_info=True)
97
  st.error(f"Could not start a new session: {e}")
98
+ # Optionally, clear authenticated_user if session creation is critical
99
+ # st.session_state.authenticated_user = None
100
+ # st.rerun()
101
  else:
102
  st.error("Invalid username or password.")
103
+ app_logger.warning(f"Failed login attempt for username: {username_input}")
104
 
105
  def display_signup_form():
106
  with st.form("signup_form"):
107
  st.subheader("Sign Up")
108
+ new_username = st.text_input("Choose a Username", key="signup_username_input")
109
+ new_email = st.text_input("Email (Optional)", key="signup_email_input")
110
+ new_password = st.text_input("Choose a Password", type="password", key="signup_password_input")
111
+ confirm_password = st.text_input("Confirm Password", type="password", key="signup_confirm_password_input")
112
  submit_button = st.form_submit_button("Sign Up")
113
 
114
  if submit_button:
 
122
  password=new_password,
123
  email=new_email if new_email else None
124
  )
125
+ # 'create_user_in_db' should return a User object with attributes loaded
126
+ # (e.g., by calling session.refresh(user) before its session closes).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  user = create_user_in_db(user_data)
128
  if user:
129
+ # Use 'new_username' (from form input) for the message to avoid DetachedInstanceError
 
 
 
130
  st.success(f"Account created for {new_username}. Please log in.")
131
  app_logger.info(f"Account created for {new_username}.")
132
+ # Optionally log them in directly:
133
+ # To do this safely, 'user' returned by create_user_in_db must be "live"
134
+ # or its attributes fully loaded. You might need to re-fetch it in a new session here
135
+ # if you intend to store it in st.session_state.authenticated_user.
136
+ # For now, redirecting to login is simpler.
 
 
137
  else:
138
+ st.error("Username might already be taken or an error occurred during signup.")
139
+ app_logger.warning(f"Signup failed for username: {new_username}")
140
 
141
  # --- Main App Logic ---
142
  if not st.session_state.authenticated_user:
 
149
  with signup_tab:
150
  display_signup_form()
151
  else:
152
+ # If authenticated
 
 
 
 
153
  with st.sidebar:
154
+ # Accessing st.session_state.authenticated_user.username.
155
+ # This relies on the object stored in authenticated_user having its username loaded.
156
+ # The `display_login_form` now stores the 'live_user' which should have this.
157
  try:
158
  st.markdown(f"### Welcome, {st.session_state.authenticated_user.username}!")
159
+ except AttributeError as e: # Fallback if username somehow isn't accessible
160
  st.markdown(f"### Welcome!")
161
+ app_logger.error(f"Could not access username for authenticated_user in sidebar: {e}. User object: {st.session_state.authenticated_user}")
162
+
163
 
164
+ # Example for conditional logo display
165
+ logo_path_str = getattr(settings, "LOGO_PATH", None)
166
+ if logo_path_str:
167
+ logo_path = Path(logo_path_str)
168
+ if logo_path.exists():
169
+ try:
170
+ st.image(str(logo_path), width=100)
171
+ except Exception as e:
172
+ app_logger.warning(f"Could not load logo from {logo_path_str}: {e}")
173
+ else:
174
+ app_logger.warning(f"Logo path specified but does not exist: {logo_path_str}")
175
  elif settings.APP_TITLE: # Fallback to title if no logo
176
  st.markdown(f"#### {settings.APP_TITLE}")
177
 
 
178
  st.markdown("---")
179
 
180
  if st.button("Logout"):
181
+ if hasattr(st.session_state.authenticated_user, 'username'):
182
+ app_logger.info(f"User {st.session_state.authenticated_user.username} logging out.")
183
+ else:
184
+ app_logger.info("User logging out (username not accessible from session state object).")
185
  st.session_state.authenticated_user = None
186
  st.session_state.current_chat_session_id = None
187
  st.session_state.chat_messages = []
188
  st.success("You have been logged out.")
189
  st.rerun()
190
 
191
+ # Main area content (could be your "Home" page if no `pages/1_Home.py`)
192
+ st.sidebar.success("Select a page from the navigation.") # More generic message
 
 
 
193
  st.header(f"Dashboard - {settings.APP_TITLE}")
194
  st.markdown("Navigate using the sidebar to consult with the AI or view your reports.")
195
  st.markdown("---")
196
+ st.info("This is the main application area. If you have pages in a `pages/` directory, Streamlit will show the selected page here. Otherwise, this content is shown.")
 
 
 
 
197
 
198
  app_logger.info(f"Streamlit app '{settings.APP_TITLE}' initialized and running.")