File size: 11,055 Bytes
6145bc0
864b488
0b77233
 
 
 
 
 
864b488
0b77233
 
 
 
 
 
 
864b488
0b77233
 
 
 
 
 
64f8a92
0b77233
0a2437f
0b77233
 
 
 
 
 
 
0a2437f
 
0b77233
 
 
 
04ae885
 
0b77233
64f8a92
0b77233
04ae885
 
 
 
 
 
 
0b77233
 
04ae885
 
 
0b77233
04ae885
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b77233
 
64f8a92
0b77233
 
 
04ae885
 
 
 
0b77233
64f8a92
0b77233
 
 
 
 
 
 
 
 
 
 
04ae885
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b77233
 
04ae885
 
 
 
 
 
 
 
0b77233
 
04ae885
 
 
0b77233
04ae885
 
0a2437f
0b77233
 
 
 
04ae885
0b77233
 
 
 
 
 
 
 
 
0a2437f
04ae885
0b77233
04ae885
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b77233
04ae885
0b77233
04ae885
0b77233
 
 
 
 
04ae885
0b77233
 
04ae885
 
 
 
0b77233
 
 
64f8a92
04ae885
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import streamlit as st
from config.settings import settings
from models import create_db_and_tables, get_session_context, User, ChatMessage, ChatSession
from models.user import UserCreate # For type hinting
from services.auth import create_user_in_db, authenticate_user
from services.logger import app_logger
# Agent will be initialized and used in specific pages like Consult.py
# from agent import get_agent_executor # Import in pages where needed

# --- Page Configuration ---
st.set_page_config(
    page_title=settings.APP_TITLE,
    page_icon="⚕️", # You can use an emoji or a path to an image
    layout="wide",
    initial_sidebar_state="expanded"
)

# --- Database Initialization ---
@st.cache_resource # Ensure this runs only once
def init_db():
    app_logger.info("Initializing database and tables...")
    create_db_and_tables()
    app_logger.info("Database initialized.")

init_db()

# --- Session State Initialization ---
if 'authenticated_user' not in st.session_state:
    st.session_state.authenticated_user = None # Stores User object upon successful login
if 'current_chat_session_id' not in st.session_state:
    st.session_state.current_chat_session_id = None
if 'chat_messages' not in st.session_state: # For the current active chat
    st.session_state.chat_messages = []


# --- Authentication Logic ---
def display_login_form():
    with st.form("login_form"):
        st.subheader("Login")
        username_input = st.text_input("Username", key="login_username_input") # Added key for clarity
        password_input = st.text_input("Password", type="password", key="login_password_input") # Added key
        submit_button = st.form_submit_button("Login")

        if submit_button:
            # IMPORTANT: The 'user' object returned by authenticate_user needs to have
            # its essential attributes (like id, username) already loaded, or if its session
            # was closed, those attributes should not be in an "expired" state that requires
            # a database refresh. Typically, a query like db.query(User)...first() loads these.
            # If authenticate_user involves a commit and session.expire_on_commit=True (default),
            # it should call db.refresh(user_object) before closing its session and returning.
            user = authenticate_user(username_input, password_input)
            if user:
                st.session_state.authenticated_user = user
                # Accessing user.username and user.id here.
                # If 'user' is detached and attributes are expired, this could also fail.
                # This implies authenticate_user should return a "usable" object.
                st.success(f"Welcome back, {user.username}!")
                try:
                    with get_session_context() as db_session:
                        # Ensure the user object is attached to the current session if needed,
                        # or that its ID is accessible even if detached.
                        # user_id = user.id (if user.id is loaded, this is fine)
                        # live_user = db_session.merge(user) # if user might be detached but has PK
                        # Or better if only ID is needed:
                        live_user = db_session.get(User, user.id) # Re-fetch/attach to current session
                        if not live_user:
                            st.error("User session error. Please log in again.")
                            app_logger.error(f"Failed to re-fetch user with id {user.id} in new session.")
                            st.session_state.authenticated_user = None # Clear broken state
                            st.rerun()
                            return

                        new_chat_session = ChatSession(user_id=live_user.id, title=f"Session for {live_user.username}")
                        db_session.add(new_chat_session)
                        db_session.commit()
                        db_session.refresh(new_chat_session) # Refresh new_chat_session
                        st.session_state.current_chat_session_id = new_chat_session.id
                        st.session_state.chat_messages = [] # Clear previous messages
                    st.rerun() # Rerun to reflect login state
                except Exception as e:
                    app_logger.error(f"Error creating chat session for user {user.username}: {e}")
                    st.error(f"Could not start a new session: {e}")
            else:
                st.error("Invalid username or password.")

def display_signup_form():
    with st.form("signup_form"):
        st.subheader("Sign Up")
        new_username = st.text_input("Choose a Username", key="signup_username_input") # Added key
        new_email = st.text_input("Email (Optional)", key="signup_email_input") # Added key
        new_password = st.text_input("Choose a Password", type="password", key="signup_password_input") # Added key
        confirm_password = st.text_input("Confirm Password", type="password", key="signup_confirm_password_input") # Added key
        submit_button = st.form_submit_button("Sign Up")

        if submit_button:
            if not new_username or not new_password:
                st.error("Username and password are required.")
            elif new_password != confirm_password:
                st.error("Passwords do not match.")
            else:
                user_data = UserCreate(
                    username=new_username,
                    password=new_password,
                    email=new_email if new_email else None
                )
                # --- Explanation of the DetachedInstanceError and its handling ---
                # The 'create_user_in_db' function creates a User and commits it to the database.
                # If the SQLAlchemy session used inside 'create_user_in_db' has 'expire_on_commit=True'
                # (which is the default), all attributes of the 'user' object are marked as "expired"
                # after the commit.
                # If 'create_user_in_db' then closes its session and returns this 'user' object,
                # the object becomes "detached".
                # When you later try to access an expired attribute (like 'user.username'),
                # SQLAlchemy attempts to reload it from the database. Since the object is detached
                # (not bound to an active session), this reload fails, raising DetachedInstanceError.

                # TO FIX ROBUSTLY (in services/auth.py, inside create_user_in_db):
                # After `db_session.commit()`, ensure `db_session.refresh(created_user_object)` is called
                # *before* the session is closed and the object is returned. This loads all attributes
                # from the database, so they are no longer "expired".

                user = create_user_in_db(user_data)
                if user:
                    # IMMEDIATE FIX for this specific line:
                    # Instead of 'user.username' (which might be on a detached, expired instance),
                    # use 'new_username', which is the value just submitted by the user and
                    # used for creation. This avoids the need to access the potentially problematic 'user' object attribute.
                    st.success(f"Account created for {new_username}. Please log in.")
                    app_logger.info(f"Account created for {new_username}.")

                    # If you were to enable direct login here:
                    # st.session_state.authenticated_user = user
                    # st.rerun()
                    # ...then it becomes CRITICAL that 'create_user_in_db' returns a 'user' object
                    # whose attributes (like .id, .username) are already loaded and not expired,
                    # as explained in the "TO FIX ROBUSTLY" comment above.
                else:
                    st.error("Username might already be taken or another error occurred during signup.")
                    app_logger.warning(f"Failed to create user for username: {new_username}")

# --- Main App Logic ---
if not st.session_state.authenticated_user:
    st.title(f"Welcome to {settings.APP_TITLE}")
    st.markdown("Your AI-powered partner for advanced healthcare insights.")

    login_tab, signup_tab = st.tabs(["Login", "Sign Up"])
    with login_tab:
        display_login_form()
    with signup_tab:
        display_signup_form()
else:
    # If authenticated, Streamlit automatically navigates to pages in the `pages/` directory.
    # The content of `app.py` typically acts as the "Home" page if no `1_Home.py` exists,
    # or it can be used for global elements like a custom sidebar if not using Streamlit's default page navigation.

    # Custom Sidebar for logged-in users
    with st.sidebar:
        # Ensure authenticated_user object is usable. If it was detached with expired attributes
        # upon login, this access could also fail. This reinforces the need for authenticate_user
        # to return a "live" or fully-loaded object.
        try:
            st.markdown(f"### Welcome, {st.session_state.authenticated_user.username}!")
        except AttributeError: # Fallback if username is not accessible
             st.markdown(f"### Welcome!")
             app_logger.error("Could not access username for authenticated_user in sidebar.")

        if st.session_state.get("SHOW_LOGO_IN_SIDEBAR", True) and settings.LOGO_PATH and Path(settings.LOGO_PATH).exists(): # Example for conditional logo
            try:
                st.image(settings.LOGO_PATH, width=100)
            except Exception as e:
                app_logger.warning(f"Could not load logo from {settings.LOGO_PATH}: {e}")
        elif settings.APP_TITLE: # Fallback to title if no logo
             st.markdown(f"#### {settings.APP_TITLE}")


        st.markdown("---")

        if st.button("Logout"):
            app_logger.info(f"User {st.session_state.authenticated_user.username} logging out.")
            st.session_state.authenticated_user = None
            st.session_state.current_chat_session_id = None
            st.session_state.chat_messages = []
            st.success("You have been logged out.")
            st.rerun()

    # This content will show if no other page is selected, or if you don't have a 1_Home.py
    # If you have 1_Home.py, Streamlit will show that by default after login.
    st.sidebar.success("Select a page above to get started.") # Or from the main area below if multi-page app

    # Main area content (could be your "Home" page)
    st.header(f"Dashboard - {settings.APP_TITLE}")
    st.markdown("Navigate using the sidebar to consult with the AI or view your reports.")
    st.markdown("---")
    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.")


# Ensure Path is imported if used for logo
from pathlib import Path

app_logger.info(f"Streamlit app '{settings.APP_TITLE}' initialized and running.")