| """ |
| 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) |