Update app.py
Browse files
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="⚕️",
|
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
|
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 |
-
|
41 |
-
|
|
|
42 |
submit_button = st.form_submit_button("Login")
|
43 |
|
44 |
if submit_button:
|
45 |
-
# IMPORTANT:
|
46 |
-
#
|
47 |
-
#
|
48 |
-
#
|
49 |
-
#
|
50 |
-
|
51 |
-
|
52 |
-
if
|
53 |
-
|
54 |
-
#
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
try:
|
59 |
with get_session_context() as db_session:
|
60 |
-
#
|
61 |
-
#
|
62 |
-
#
|
63 |
-
|
64 |
-
|
65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
if not live_user:
|
67 |
-
st.error("
|
68 |
-
app_logger.error(f"Failed to re-fetch user with id {
|
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 {
|
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")
|
90 |
-
new_email = st.text_input("Email (Optional)", key="signup_email_input")
|
91 |
-
new_password = st.text_input("Choose a Password", type="password", key="signup_password_input")
|
92 |
-
confirm_password = st.text_input("Confirm Password", type="password", key="signup_confirm_password_input")
|
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 |
-
#
|
107 |
-
#
|
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 |
-
#
|
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 |
-
#
|
132 |
-
#
|
133 |
-
# st.
|
134 |
-
#
|
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
|
139 |
-
app_logger.warning(f"
|
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
|
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 |
-
#
|
159 |
-
#
|
160 |
-
#
|
161 |
try:
|
162 |
st.markdown(f"### Welcome, {st.session_state.authenticated_user.username}!")
|
163 |
-
except AttributeError: # Fallback if username
|
164 |
st.markdown(f"### Welcome!")
|
165 |
-
app_logger.error("Could not access username for authenticated_user in sidebar.")
|
|
|
166 |
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
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 |
-
#
|
187 |
-
|
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
|
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.")
|