Spaces:
Sleeping
Sleeping
# app.py | |
# -- coding: utf-8 -- | |
import gradio as gr | |
import pandas as pd | |
import os | |
import logging | |
import time # Added for simulating delay | |
# --- 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') | |
# --- Helper function to load HTML animation --- | |
def get_sync_animation_html(): | |
"""Loads the HTML content for the sync animation.""" | |
try: | |
# Ensure this path is correct relative to where app.py is run | |
# Make sure 'sync_animation.html' is in the same directory as app.py | |
with open("sync_animation.html", "r", encoding="utf-8") as f: | |
return f.read() | |
except FileNotFoundError: | |
logging.error("sync_animation.html not found. Please ensure it's in the same directory as app.py.") | |
return "<p style='text-align:center; color: red;'>Animation file not found. Syncing...</p>" | |
except Exception as e: | |
logging.error(f"Error loading sync_animation.html: {e}") | |
return f"<p style='text-align:center; color: red;'>Error loading animation: {e}. Syncing...</p>" | |
# --- 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.") | |
return ("β Access denied. No token.", None, None, None, None, None, None, None) | |
# Ensure dataframes are at least empty DataFrames if not found in state | |
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: | |
# Call the actual or placeholder function | |
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) | |
# --- Animation Test Function (Generator) --- | |
def show_animation_then_simulate_processing(): | |
""" | |
Yields the animation HTML, then simulates a delay, | |
and finally yields a completion message. | |
""" | |
logging.info("TEST BUTTON: Yielding animation HTML.") | |
yield get_sync_animation_html() # First update: Display the animation | |
logging.info("TEST BUTTON: Simulating processing (server-side delay of 8 seconds).") | |
time.sleep(8) # Server-side delay | |
logging.info("TEST BUTTON: Simulation complete. Yielding completion message.") | |
yield "<p style='text-align:center; color: green; font-size: 1.2em;'>β Animation Test Complete!</p>" # Second update | |
# --- Gradio UI Blocks --- | |
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"), | |
title="LinkedIn Organization Dashboard") as app: | |
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(), | |
"bubble_operations_log_df": pd.DataFrame(), | |
"mentions_should_sync_now": False, | |
"fs_should_sync_now": False, | |
"url_user_token_temp_storage": None # Not used in current logic, but kept from original | |
}) | |
gr.Markdown("# π LinkedIn Organization Dashboard") | |
# These textboxes are used to capture URL parameters via app.load() | |
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) | |
# This function runs when the Gradio app loads in the browser. | |
# It calls `get_url_user_token` which should be JavaScript or a Gradio request handler. | |
# For it to work with JavaScript `fn=None, inputs=None, ..., js="() => { ... return [token, urn]; }" | |
# For Python function, it needs a gr.Request input. | |
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) | |
def initial_load_sequence(url_token, org_urn_val, current_state, request: gr.Request): # Added request for get_url_user_token if it needs it | |
"""Handles the initial processing after URL parameters are potentially loaded.""" | |
# If get_url_user_token is pure Python and needs request, it's passed here. | |
# If get_url_user_token was JS, url_token and org_urn_val would be populated from its output. | |
# If url_token and org_urn_val are NOT populated by app.load (e.g. if get_url_user_token is a JS snippet that didn't run or returned null) | |
# you might need to call get_url_user_token here again if it's a Python function that needs the request object. | |
# However, app.load is designed to populate its outputs. | |
# For this example, we assume url_token and org_urn_val are correctly passed from org_urn_display.change | |
# which is triggered after app.load populates org_urn_display. | |
logging.info(f"Initial load sequence triggered. Org URN via change: {org_urn_val}, URL Token via change: {'Present' if url_token else 'Absent'}") | |
# Call state_manager.process_and_store_bubble_token | |
status_msg, new_state, btn_update = process_and_store_bubble_token(url_token, org_urn_val, current_state) | |
# Call ui_generators.display_main_dashboard | |
dashboard_content_html = display_main_dashboard(new_state) | |
return status_msg, new_state, btn_update, dashboard_content_html | |
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.") | |
with gr.Row(): | |
sync_data_btn = gr.Button("π Sync LinkedIn Data", variant="primary", visible=False, interactive=False) | |
test_animation_btn = gr.Button("π§ͺ Test Animation Display", variant="secondary") | |
# HTML component to display sync status or animation | |
sync_status_html_output = gr.HTML("<p style='text-align:center;'>Sync status will appear here.</p>") | |
# HTML component to display the main dashboard content | |
dashboard_display_html = gr.HTML("<p style='text-align:center;'>Dashboard loading...</p>") | |
# When org_urn_display changes (after app.load), trigger initial_load_sequence | |
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" | |
) | |
# Original Sync Button Logic (multi-step process) | |
sync_data_btn.click( | |
fn=lambda: get_sync_animation_html(), # Show animation first | |
inputs=None, | |
outputs=[sync_status_html_output], | |
show_progress=False | |
).then( | |
fn=sync_all_linkedin_data_orchestrator, # Perform actual sync | |
inputs=[token_state], | |
outputs=[sync_status_html_output, token_state], | |
show_progress=False # Animation is the progress | |
).then( | |
fn=process_and_store_bubble_token, # Re-process token/state after sync | |
inputs=[url_user_token_display, org_urn_display, token_state], | |
outputs=[status_box, token_state, sync_data_btn], | |
show_progress=False | |
).then( | |
fn=display_main_dashboard, # Refresh dashboard display | |
inputs=[token_state], | |
outputs=[dashboard_display_html], | |
show_progress=False | |
) | |
# New Test Animation Button Logic (uses the generator function) | |
test_animation_btn.click( | |
fn=show_animation_then_simulate_processing, | |
inputs=None, | |
outputs=[sync_status_html_output], | |
show_progress=False # Animation itself is the progress indicator | |
) | |
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...") # For HTML content like follower count | |
with gr.Row(): | |
follower_plot = gr.Plot(label="Follower Demographics") | |
growth_plot = 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 = gr.Plot(label="Mentions Volume") | |
mentions_sentiment_plot = 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 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.") | |
# Check for Matplotlib (optional, but good for plots) | |
try: | |
import matplotlib | |
logging.info(f"Matplotlib version: {matplotlib.__version__} found. Backend: {matplotlib.get_backend()}") | |
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, share=False) |