Spaces:
Sleeping
Sleeping
| # ============================================================ | |
| # FILE: app/components/dashboard_ui.py | |
| # PURPOSE: Renders the Clinical Overview dashboard page. | |
| # Organized into two tabs: | |
| # Tab 1 β "π Analytics": metrics, charts, history table | |
| # Tab 2 β "π¬ XAI Report": renders the latest SHAP HTML | |
| # report so clinicians can review explanations | |
| # without going back to the chat. | |
| # ============================================================ | |
| import streamlit as st # Core UI framework | |
| import streamlit.components.v1 as components # For embedding raw HTML (SHAP report) | |
| import pandas as pd # For converting SQL rows into a DataFrame | |
| import os # For checking if the SHAP file exists | |
| import sys # For fixing the import path | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PATH SETUP BLOCK | |
| # Same pattern as api.py: this file is inside app/components/ | |
| # so we need to walk up TWO levels to reach the project root | |
| # where the src/ package lives. | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Get the absolute path of this file (app/components/dashboard_ui.py) | |
| current_dir = os.path.dirname(os.path.abspath(__file__)) | |
| # Walk UP two directories: components/ β app/ β project_root/ | |
| project_root = os.path.abspath(os.path.join(current_dir, "../../")) | |
| # Add project_root to Python's search path so "from src.xxx import yyy" works | |
| if project_root not in sys.path: | |
| sys.path.append(project_root) | |
| # Import our database helper class from src/database/db_operations.py | |
| from src.database.db_operations import MindGuardDatabase | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # SHAP REPORT PATH | |
| # shap_explainer.py ALWAYS writes its output to this exact path. | |
| # We define it once here so any function in this file can use it. | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SHAP_HTML_PATH = os.path.join( | |
| project_root, # e.g., /home/user/project/ | |
| "artifacts", # The artifacts/ folder at project root | |
| "shap_report.html" # Fixed filename β shap_explainer.py never changes this | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # MAIN RENDER FUNCTION | |
| # Called by main.py when the user selects "π Clinical Dashboard" | |
| # in the sidebar navigation. | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def render_dashboard(): | |
| # Page header in the main content area | |
| st.title("π Clinical Overview") | |
| st.markdown("Real-time emotional tracking, risk assessment, and XAI reports.") | |
| # ββ Create Two Tabs βββββββββββββββββββββββββββββββββββββββ | |
| # st.tabs() returns a list of tab context managers. | |
| # We unpack them into tab1 and tab2 immediately. | |
| tab1, tab2 = st.tabs(["π Analytics", "π¬ XAI Report"]) | |
| # ========================================================= | |
| # TAB 1: ANALYTICS | |
| # Shows metrics, bar charts, and a history table built from | |
| # the chat_history table in our SQLite database. | |
| # ========================================================= | |
| with tab1: | |
| # ββ Database Read βββββββββββββββββββββββββββββββββββββ | |
| # Open a database connection to read all historical records | |
| db = MindGuardDatabase() | |
| # Execute a SQL SELECT to fetch the three columns we need, | |
| # sorted newest-first so the table shows recent entries at the top | |
| db.cursor.execute( | |
| "SELECT timestamp, diagnosed_emotion, risk_level " | |
| "FROM chat_history " | |
| "ORDER BY timestamp DESC" | |
| ) | |
| # fetchall() returns a list of sqlite3.Row objects (like dicts) | |
| rows = db.cursor.fetchall() | |
| # Close the DB connection immediately after reading. | |
| # Leaving connections open can cause locking issues. | |
| db.close() | |
| # Guard clause: if there are no rows in the DB yet, | |
| # show an info message and exit the function early. | |
| if not rows: | |
| st.info("No data yet. Start chatting to generate analytics.") | |
| return # Exit render_dashboard() β nothing to display | |
| # Convert the list of sqlite3.Row objects into a Pandas DataFrame. | |
| # dict(r) turns each Row into a regular Python dict first. | |
| # DataFrame() then stacks all those dicts into a table. | |
| df = pd.DataFrame([dict(r) for r in rows]) | |
| # ββ TOP METRICS ROW βββββββββββββββββββββββββββββββββββ | |
| # Compute the three summary statistics shown as metric cards | |
| total = len(df) # Total number of chat interactions recorded | |
| high_risk = len(df[df["risk_level"] == "High"]) # Count rows where risk_level equals "High" | |
| # mode()[0] returns the most-frequently-occurring value in that column | |
| top_emotion = df["diagnosed_emotion"].mode()[0] if not df.empty else "N/A" | |
| # st.columns(3) creates three equal-width side-by-side columns | |
| col1, col2, col3 = st.columns(3) | |
| col1.metric("Total Interactions", total) | |
| # delta_color="inverse" makes the delta arrow red on increase | |
| # (since more high-risk flags is BAD, not good) | |
| col2.metric("High Risk Flags", high_risk, delta_color="inverse") | |
| col3.metric("Primary Emotion", top_emotion) | |
| # ββ RISK LEVEL SUMMARY ββββββββββββββββββββββββββββββββ | |
| st.divider() # Horizontal separator line | |
| st.subheader("Risk Level Summary") | |
| # Create three columns, one per risk level | |
| risk_cols = st.columns(3) | |
| # Loop through each risk level and display its count | |
| for i, level in enumerate(["High", "Medium", "Low"]): | |
| # Count rows where risk_level matches this level | |
| count = len(df[df["risk_level"] == level]) | |
| # Pick an icon that visually reinforces the severity | |
| icons = {"High": "π¨", "Medium": "β οΈ", "Low": "β "} | |
| # Display as a Streamlit metric card in the correct column | |
| risk_cols[i].metric(f"{icons[level]} {level}", count) | |
| # ββ BAR CHARTS βββββββββββββββββββββββββββββββββββββββ | |
| st.divider() | |
| # Two charts side by side using columns | |
| chart_col1, chart_col2 = st.columns(2) | |
| with chart_col1: | |
| st.subheader("Emotion Frequency") | |
| # value_counts() counts how many times each emotion appeared. | |
| # st.bar_chart() renders it as an interactive bar chart automatically. | |
| st.bar_chart(df["diagnosed_emotion"].value_counts()) | |
| with chart_col2: | |
| st.subheader("Risk Level Distribution") | |
| # Same pattern: count occurrences of each risk level | |
| st.bar_chart(df["risk_level"].value_counts()) | |
| # ββ RECENT HISTORY TABLE βββββββββββββββββββββββββββββ | |
| st.divider() | |
| st.subheader("Recent Session History") | |
| # Show only the 20 most recent rows to keep the table manageable. | |
| # .rename() gives the columns friendly display names for the UI. | |
| st.dataframe( | |
| df.head(20).rename(columns={ | |
| "timestamp": "Time", # Raw DB column name β human label | |
| "diagnosed_emotion": "Emotion", | |
| "risk_level": "Risk" | |
| }), | |
| use_container_width=True # Stretch table to fill available width | |
| ) | |
| # ========================================================= | |
| # TAB 2: XAI REPORT | |
| # Loads and renders the SHAP HTML report that was saved to disk | |
| # by shap_explainer.generate_visual_report() during the last chat. | |
| # This gives clinicians a dedicated view of the explanation. | |
| # ========================================================= | |
| with tab2: | |
| st.subheader("π¬ Last SHAP Word-Level Explanation") | |
| # Explanation of what the user is looking at | |
| st.markdown( | |
| "This report shows **which words** drove the model's emotion prediction. " | |
| "**Red highlights** = words that pushed the model TOWARDS that emotion. " | |
| "**Blue highlights** = words that pushed AGAINST it. " | |
| "Generated using SHAP (SHapley Additive exPlanations) β Game Theory math." | |
| ) | |
| # os.path.exists() checks whether the file has been created yet. | |
| # It won't exist on the very first run before any chat messages. | |
| if os.path.exists(SHAP_HTML_PATH): | |
| # Read the entire SHAP HTML report file into a Python string | |
| with open(SHAP_HTML_PATH, "r", encoding="utf-8") as f: | |
| shap_html = f.read() | |
| # components.html() injects the raw HTML string into an iframe | |
| # inside the Streamlit page. This is how we display SHAP's | |
| # interactive visualization without needing a Jupyter notebook. | |
| # height=500 gives enough space to see word highlights clearly. | |
| # scrolling=True allows vertical scroll if the report is tall. | |
| components.html(shap_html, height=500, scrolling=True) | |
| # Show the file path as a small caption below the report | |
| # so developers can quickly find the file for debugging | |
| st.caption(f"Report path: `{SHAP_HTML_PATH}`") | |
| else: | |
| # No report exists yet β guide the user to generate one | |
| st.info( | |
| "No SHAP report found yet. " | |
| "Send a message in the **π¬ Chat Companion** tab " | |
| "and the XAI report will appear here automatically after your first message." | |
| ) |