Spaces:
Runtime error
Runtime error
# 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("<p style='text-align:center;'>Sync status will appear here.</p>") | |
dashboard_display_html = gr.HTML("<p style='text-align:center;'>Dashboard loading...</p>") | |
# 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) | |