# app.py # -- coding: utf-8 -- import gradio as gr import pandas as pd import os import logging # --- Module Imports --- # Functions from your existing/provided custom modules from analytics_fetch_and_rendering import fetch_and_render_analytics # Assuming this exists from gradio_utils import get_url_user_token # For fetching URL parameters # Functions from newly created/refactored modules from config import ( LINKEDIN_CLIENT_ID_ENV_VAR, BUBBLE_APP_NAME_ENV_VAR, BUBBLE_API_KEY_PRIVATE_ENV_VAR, BUBBLE_API_ENDPOINT_ENV_VAR ) from state_manager import process_and_store_bubble_token from sync_logic import sync_all_linkedin_data_orchestrator from ui_generators import ( display_main_dashboard, run_mentions_tab_display, run_follower_stats_tab_display ) # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') # --- Guarded Analytics Fetch --- def guarded_fetch_analytics(token_state): """Guarded call to fetch_and_render_analytics, ensuring token and basic data structures.""" if not token_state or not token_state.get("token"): logging.warning("Analytics fetch: Access denied. No token.") # Ensure the number of returned Nones matches the expected number of outputs for the plots return ("❌ Access denied. No token.", None, None, None, None, None, None, None) # Ensure DataFrames are passed, even if empty, to avoid errors in the analytics function posts_df_analytics = token_state.get("bubble_posts_df", pd.DataFrame()) mentions_df_analytics = token_state.get("bubble_mentions_df", pd.DataFrame()) follower_stats_df_analytics = token_state.get("bubble_follower_stats_df", pd.DataFrame()) logging.info("Calling fetch_and_render_analytics with current token_state data.") try: return fetch_and_render_analytics( token_state.get("client_id"), token_state.get("token"), token_state.get("org_urn"), posts_df_analytics, mentions_df_analytics, follower_stats_df_analytics ) except Exception as e: logging.error(f"Error in guarded_fetch_analytics calling fetch_and_render_analytics: {e}", exc_info=True) return (f"❌ Error fetching analytics: {e}", None, None, None, None, None, None, None) # --- Gradio UI Blocks --- with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"), title="LinkedIn Organization Dashboard") as app: # Central state for holding token, client_id, org_urn, and fetched dataframes token_state = gr.State(value={ "token": None, "client_id": None, "org_urn": None, "bubble_posts_df": pd.DataFrame(), "fetch_count_for_api": 0, "bubble_mentions_df": pd.DataFrame(), "bubble_follower_stats_df": pd.DataFrame(), "url_user_token_temp_storage": None }) gr.Markdown("# 🚀 LinkedIn Organization Dashboard") # Hidden textboxes to capture URL parameters url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False) status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...") org_urn_display = gr.Textbox(label="Organization URN (from URL - Hidden)", interactive=False, visible=False) # Load URL parameters when the Gradio app loads app.load(fn=get_url_user_token, inputs=None, outputs=[url_user_token_display, org_urn_display], api_name="get_url_params", show_progress=False) # This function will run after URL params are loaded and org_urn_display changes def initial_load_sequence(url_token, org_urn_val, current_state): logging.info(f"Initial load sequence triggered. Org URN: {org_urn_val}, URL Token: {'Present' if url_token else 'Absent'}") # Process token, fetch Bubble data, determine sync needs status_msg, new_state, btn_update = process_and_store_bubble_token(url_token, org_urn_val, current_state) # Display initial dashboard content based on (potentially empty) Bubble data dashboard_content = display_main_dashboard(new_state) return status_msg, new_state, btn_update, dashboard_content with gr.Tabs(): with gr.TabItem("1️⃣ Dashboard & Sync"): gr.Markdown("System checks for existing data from Bubble. The 'Sync' button activates if new data needs to be fetched from LinkedIn based on the last sync times and data availability.") sync_data_btn = gr.Button("🔄 Sync LinkedIn Data", variant="primary", visible=False, interactive=False) sync_status_html_output = gr.HTML("

Sync status will appear here.

") dashboard_display_html = gr.HTML("

Dashboard loading...

") # Chain of events for initial load: org_urn_display.change( fn=initial_load_sequence, inputs=[url_user_token_display, org_urn_display, token_state], outputs=[status_box, token_state, sync_data_btn, dashboard_display_html], show_progress="full" ) # Also trigger initial_load_sequence if url_user_token_display changes (e.g. if it loads after org_urn) # This helps ensure it runs once both are potentially available. # Note: `org_urn_display.change` might be sufficient if `get_url_user_token` updates both nearly simultaneously. # Adding this for robustness, but ensure it doesn't cause unwanted multiple runs if state isn't managed carefully. # Consider using a flag in token_state if multiple triggers become an issue. # For now, relying on org_urn_display.change as the primary trigger post-load. # When Sync button is clicked: sync_data_btn.click( fn=sync_all_linkedin_data_orchestrator, inputs=[token_state], outputs=[sync_status_html_output, token_state], # token_state is updated here show_progress="full" ).then( fn=process_and_store_bubble_token, # Re-check sync status and update button inputs=[url_user_token_display, org_urn_display, token_state], # Pass current token_state outputs=[status_box, token_state, sync_data_btn], # token_state updated again show_progress=False # Typically "full" for user-initiated actions, "minimal" or False for quick updates ).then( fn=display_main_dashboard, # Refresh dashboard display inputs=[token_state], outputs=[dashboard_display_html], show_progress=False ) with gr.TabItem("2️⃣ Analytics"): fetch_analytics_btn = gr.Button("📈 Fetch/Refresh Full Analytics", variant="primary") follower_count_md = gr.Markdown("Analytics data will load here...") with gr.Row(): follower_plot, growth_plot = gr.Plot(label="Follower Demographics"), gr.Plot(label="Follower Growth") with gr.Row(): eng_rate_plot = gr.Plot(label="Engagement Rate") with gr.Row(): interaction_plot = gr.Plot(label="Post Interactions") with gr.Row(): eb_plot = gr.Plot(label="Engagement Benchmark") with gr.Row(): mentions_vol_plot, mentions_sentiment_plot = gr.Plot(label="Mentions Volume"), gr.Plot(label="Mentions Sentiment") fetch_analytics_btn.click( fn=guarded_fetch_analytics, inputs=[token_state], outputs=[follower_count_md, follower_plot, growth_plot, eng_rate_plot, interaction_plot, eb_plot, mentions_vol_plot, mentions_sentiment_plot], show_progress="full" ) with gr.TabItem("3️⃣ Mentions"): refresh_mentions_display_btn = gr.Button("🔄 Refresh Mentions Display (from local data)", variant="secondary") mentions_html = gr.HTML("Mentions data loads from Bubble after sync. Click refresh to view current local data.") mentions_sentiment_dist_plot = gr.Plot(label="Mention Sentiment Distribution") refresh_mentions_display_btn.click( fn=run_mentions_tab_display, inputs=[token_state], outputs=[mentions_html, mentions_sentiment_dist_plot], show_progress="full" ) with gr.TabItem("4️⃣ Follower Stats"): refresh_follower_stats_btn = gr.Button("🔄 Refresh Follower Stats Display (from local data)", variant="secondary") follower_stats_html = gr.HTML("Follower statistics load from Bubble after sync. Click refresh to view current local data.") with gr.Row(): fs_plot_monthly_gains = gr.Plot(label="Monthly Follower Gains") with gr.Row(): fs_plot_seniority = gr.Plot(label="Followers by Seniority (Top 10 Organic)") fs_plot_industry = gr.Plot(label="Followers by Industry (Top 10 Organic)") refresh_follower_stats_btn.click( fn=run_follower_stats_tab_display, inputs=[token_state], outputs=[follower_stats_html, fs_plot_monthly_gains, fs_plot_seniority, fs_plot_industry], show_progress="full" ) if __name__ == "__main__": # Check for essential environment variables if not os.environ.get(LINKEDIN_CLIENT_ID_ENV_VAR): logging.warning(f"WARNING: '{LINKEDIN_CLIENT_ID_ENV_VAR}' environment variable not set. The app may not function correctly for LinkedIn API calls.") if not os.environ.get(BUBBLE_APP_NAME_ENV_VAR) or \ not os.environ.get(BUBBLE_API_KEY_PRIVATE_ENV_VAR) or \ not os.environ.get(BUBBLE_API_ENDPOINT_ENV_VAR): logging.warning("WARNING: One or more Bubble environment variables (BUBBLE_APP_NAME, BUBBLE_API_KEY_PRIVATE, BUBBLE_API_ENDPOINT) are not set. Bubble integration will fail.") try: import matplotlib logging.info(f"Matplotlib version: {matplotlib.__version__} found. Backend: {matplotlib.get_backend()}") # The backend is now set in ui_generators.py, which is good practice. except ImportError: logging.error("Matplotlib is not installed. Plots will not be generated. Please install it: pip install matplotlib") # Launch the Gradio app app.launch(server_name="0.0.0.0", server_port=7860, debug=True)