| | """ |
| | Musora Sentiment Analysis Dashboard |
| | Main Streamlit Application |
| | |
| | Run with: streamlit run app.py |
| | """ |
| | import streamlit as st |
| | import sys |
| | from pathlib import Path |
| | import json |
| |
|
| | |
| | parent_dir = Path(__file__).resolve().parent |
| | sys.path.append(str(parent_dir)) |
| |
|
| | from data.data_loader import SentimentDataLoader |
| | from components.dashboard import render_dashboard |
| | from components.sentiment_analysis import render_sentiment_analysis |
| | from components.reply_required import render_reply_required |
| | from utils.auth import check_authentication, render_login_page, logout, get_current_user |
| |
|
| | |
| | config_path = parent_dir / "config" / "viz_config.json" |
| | with open(config_path, 'r') as f: |
| | config = json.load(f) |
| |
|
| | |
| | st.set_page_config( |
| | page_title=config['page_config']['page_title'], |
| | page_icon=config['page_config']['page_icon'], |
| | layout=config['page_config']['layout'], |
| | initial_sidebar_state=config['page_config']['initial_sidebar_state'] |
| | ) |
| |
|
| | |
| | |
| | |
| | if not check_authentication(): |
| | render_login_page() |
| |
|
| | |
| | data_loader = SentimentDataLoader() |
| |
|
| |
|
| | def _ensure_dashboard_data(): |
| | """ |
| | Load dashboard data once and store in session_state. |
| | Subsequent calls within the same session (or until cache expires) are free. |
| | """ |
| | if 'dashboard_df' not in st.session_state or st.session_state['dashboard_df'] is None: |
| | with st.spinner("Loading dashboard dataβ¦"): |
| | df = data_loader.load_dashboard_data() |
| | st.session_state['dashboard_df'] = df |
| | return st.session_state['dashboard_df'] |
| |
|
| |
|
| | def main(): |
| | |
| | with st.sidebar: |
| | st.image("visualization/img/musora.png", use_container_width=True) |
| |
|
| | |
| | current_user = get_current_user() |
| | if current_user: |
| | st.caption(f"Logged in as **{current_user}**") |
| | if st.button("π Logout", use_container_width=True): |
| | logout() |
| | st.rerun() |
| |
|
| | st.markdown("---") |
| | st.title("Navigation") |
| |
|
| | page = st.radio( |
| | "Select Page", |
| | ["π Sentiment Dashboard", "π Custom Sentiment Queries", "π¬ Reply Required"], |
| | index=0 |
| | ) |
| |
|
| | st.markdown("---") |
| | st.markdown("### π Global Filters") |
| |
|
| | |
| | dashboard_df = _ensure_dashboard_data() |
| |
|
| | if dashboard_df.empty: |
| | st.error("No data available. Please check your Snowflake connection.") |
| | return |
| |
|
| | filter_options = data_loader.get_filter_options(dashboard_df) |
| |
|
| | |
| | prev = st.session_state.get('global_filters', {}) |
| |
|
| | selected_platforms = st.multiselect( |
| | "Platforms", |
| | options=filter_options['platforms'], |
| | default=prev.get('platforms', []) |
| | ) |
| | selected_brands = st.multiselect( |
| | "Brands", |
| | options=filter_options['brands'], |
| | default=prev.get('brands', []) |
| | ) |
| | selected_sentiments = st.multiselect( |
| | "Sentiments", |
| | options=filter_options['sentiments'], |
| | default=prev.get('sentiments', []) |
| | ) |
| |
|
| | |
| | if 'comment_timestamp' in dashboard_df.columns and not dashboard_df.empty: |
| | min_date = dashboard_df['comment_timestamp'].min().date() |
| | max_date = dashboard_df['comment_timestamp'].max().date() |
| |
|
| | prev_range = prev.get('date_range') |
| | default_range = ( |
| | (prev_range[0], prev_range[1]) if prev_range and len(prev_range) == 2 |
| | else (min_date, max_date) |
| | ) |
| | date_range = st.date_input( |
| | "Date Range", |
| | value=default_range, |
| | min_value=min_date, |
| | max_value=max_date |
| | ) |
| | else: |
| | date_range = None |
| |
|
| | |
| | if st.button("π Apply Filters", use_container_width=True): |
| | st.session_state['global_filters'] = { |
| | 'platforms': selected_platforms, |
| | 'brands': selected_brands, |
| | 'sentiments': selected_sentiments, |
| | 'date_range': date_range if date_range and len(date_range) == 2 else None, |
| | } |
| | st.session_state['filters_applied'] = True |
| |
|
| | if st.button("π Reset Filters", use_container_width=True): |
| | st.session_state['global_filters'] = {} |
| | st.session_state['filters_applied'] = False |
| | st.rerun() |
| |
|
| | st.markdown("---") |
| |
|
| | |
| | st.markdown("### π Data Management") |
| | if st.button("β»οΈ Reload Data", use_container_width=True): |
| | st.cache_data.clear() |
| | st.session_state.pop('dashboard_df', None) |
| | st.rerun() |
| |
|
| | |
| | st.markdown("---") |
| | st.markdown("### βΉοΈ Data Info") |
| | st.info(f"**Total Records:** {len(dashboard_df):,}") |
| | if 'processed_at' in dashboard_df.columns and not dashboard_df.empty: |
| | last_update = dashboard_df['processed_at'].max() |
| | if hasattr(last_update, 'strftime'): |
| | st.info(f"**Last Updated:** {last_update.strftime('%Y-%m-%d %H:%M')}") |
| |
|
| | |
| | filters_applied = st.session_state.get('filters_applied', False) |
| | global_filters = st.session_state.get('global_filters', {}) |
| |
|
| | if filters_applied and global_filters: |
| | filtered_df = data_loader.apply_filters( |
| | dashboard_df, |
| | platforms=global_filters.get('platforms') or None, |
| | brands=global_filters.get('brands') or None, |
| | sentiments=global_filters.get('sentiments') or None, |
| | date_range=global_filters.get('date_range') or None, |
| | ) |
| | if filtered_df.empty: |
| | st.warning("No data matches the selected filters. Please adjust your filters.") |
| | return |
| | st.info(f"Showing **{len(filtered_df):,}** records after applying filters") |
| | else: |
| | filtered_df = dashboard_df |
| |
|
| | |
| | if page == "π Sentiment Dashboard": |
| | render_dashboard(filtered_df) |
| |
|
| | elif page == "π Custom Sentiment Queries": |
| | |
| | render_sentiment_analysis(data_loader) |
| |
|
| | elif page == "π¬ Reply Required": |
| | |
| | render_reply_required(data_loader) |
| |
|
| | |
| | st.markdown("---") |
| | st.markdown( |
| | """ |
| | <div style='text-align: center; color: gray; padding: 20px;'> |
| | <p>Musora Sentiment Analysis Dashboard v1.0</p> |
| | <p>Powered by Streamlit | Data from Snowflake</p> |
| | </div> |
| | """, |
| | unsafe_allow_html=True |
| | ) |
| |
|
| |
|
| | if __name__ == "__main__": |
| | try: |
| | main() |
| | except Exception as e: |
| | st.error(f"An error occurred: {str(e)}") |
| | st.exception(e) |