LinkedinMonitor / app.py
GuglielmoTor's picture
Update app.py
6f117a4 verified
raw
history blame
13.5 kB
# 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)