LinkedinMonitor / app.py
GuglielmoTor's picture
Update app.py
eef789e verified
#app.py
import gradio as gr
import pandas as pd
import os
import logging
from collections import defaultdict
import matplotlib
matplotlib.use('Agg')
# --- Module Imports ---
from utils.gradio_utils import get_url_user_token
from config import (
PLOT_ID_TO_FORMULA_KEY_MAP,
LINKEDIN_CLIENT_ID_ENV_VAR,
BUBBLE_APP_NAME_ENV_VAR,
BUBBLE_API_KEY_PRIVATE_ENV_VAR,
BUBBLE_API_ENDPOINT_ENV_VAR
)
# REMOVED: from services.analytics_tab_module import AnalyticsTab
from services.state_manager import load_data_from_bubble
from ui.ui_generators import (
# REMOVED: build_analytics_tab_plot_area,
build_dynamic_home_tab_ui,
create_enhanced_report_tab,
BOMB_ICON, EXPLORE_ICON, FORMULA_ICON, ACTIVE_ICON
)
from services.home_tab_module import refresh_home_tab_ui
from ui.config import custom_title_css
from ui.okr_ui_generator import create_enhanced_okr_tab, format_okrs_for_enhanced_display, get_initial_okr_display
# REMOVED: from ui.analytics_plot_generator import update_analytics_plots_figures, create_placeholder_plot
# REMOVED: from formulas import PLOT_FORMULAS
from config import GRAPH_GROUPS, GRAPH_GROUP_TITLES, GRAPH_GROUP_DESCRIPTIONS
from services.sidebar_graphs_module import generate_sidebar_graphs_for_group, _create_empty_plotly_figure
from data_processing.analytics_data_processing import prepare_filtered_analytics_data
# REMOVED: from features.chatbot.chatbot_prompts import get_initial_insight_prompt_and_suggestions
# REMOVED: from features.chatbot.chatbot_handler import generate_llm_response
try:
from run_agentic_pipeline import load_and_display_agentic_results
from services.report_data_handler import fetch_and_reconstruct_data_from_bubble
from ui.insights_ui_generator import format_report_for_display
AGENTIC_MODULES_LOADED = True
except ImportError as e:
logging.error(f"Could not import agentic modules: {e}")
AGENTIC_MODULES_LOADED = False
def load_and_display_agentic_results(*args, **kwargs):
empty_header_html = """
<div class="report-title">πŸ“ Comprehensive Analysis Report</div>
<div class="report-subtitle">AI-Generated Insights</div>
"""
empty_body_markdown = """
<div class="empty-state">
<div class="empty-state-icon">πŸ“„</div>
<div class="empty-state-title">No Report Selected</div>
</div>
"""
return (
gr.update(value="Modules not loaded."),
gr.update(choices=[], value=None),
gr.update(choices=[], value=[]),
gr.update(value="Modules not loaded."),
None,
[],
[],
gr.update(value=empty_header_html),
gr.update(value=empty_body_markdown),
{},
gr.update(value=get_initial_okr_display()),
gr.update(value={})
)
def fetch_and_reconstruct_data_from_bubble(*args, **kwargs):
return None, {}
def format_report_for_display(report_data):
return {'header_html': '<h1>Modules not loaded.</h1>', 'body_markdown': 'Unavailable.'}
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
title="LinkedIn Organization Dashboard") as app:
# --- STATE MANAGEMENT ---
token_state = gr.State(value={
"token": None, "client_id": None, "org_urn": None,
"bubble_posts_df": pd.DataFrame(), "bubble_post_stats_df": pd.DataFrame(),
"bubble_mentions_df": pd.DataFrame(), "bubble_follower_stats_df": pd.DataFrame(),
"bubble_agentic_analysis_data": pd.DataFrame(),
"url_user_token_temp_storage": None,
"config_date_col_posts": "published_at", "config_date_col_mentions": "date",
"config_date_col_followers": "date", "config_media_type_col": "media_type",
"config_eb_labels_col": "li_eb_label"
})
# REMOVED: chat_histories_st = gr.State({})
# REMOVED: current_chat_plot_id_st = gr.State(None)
# REMOVED: plot_data_for_chatbot_st = gr.State({})
orchestration_raw_results_st = gr.State(None)
key_results_for_selection_st = gr.State([])
selected_key_result_ids_st = gr.State([])
reconstruction_cache_st = gr.State({})
actionable_okrs_data_st = gr.State({})
active_graph_group_st = gr.State(None)
# --- UI LAYOUT ---
url_user_token_display = gr.Textbox(label="User Token", interactive=False, visible=False)
org_urn_display = gr.Textbox(label="Org URN", interactive=False, visible=False)
status_box = gr.Textbox(label="Status", interactive=False, value="", visible=False)
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)
app.load(
fn=None,
js="""
const processedButtons = new Set();
function setAllTooltips() {
const bombBtns = document.querySelectorAll('.analytics-bomb-btn button');
bombBtns.forEach(btn => {
if (!processedButtons.has(btn)) {
btn.setAttribute('title', 'Analizza il grafico con l\\'intelligenza artificiale.');
processedButtons.add(btn);
}
});
const formulaBtns = document.querySelectorAll('.analytics-formula-btn button');
formulaBtns.forEach(btn => {
if (!processedButtons.has(btn)) {
btn.setAttribute('title', 'Scopri come viene calcolato il dato.');
processedButtons.add(btn);
}
});
const exploreBtns = document.querySelectorAll('.analytics-explore-btn button');
exploreBtns.forEach(btn => {
if (!processedButtons.has(btn)) {
btn.setAttribute('title', 'Ingrandisci il grafico per un\\'analisi dettagliata.');
processedButtons.add(btn);
}
});
}
setInterval(setAllTooltips, 250);
"""
)
def initial_data_load_sequence(url_token, org_urn_val, current_state):
"""Handles initial data loading from Bubble."""
status_msg, new_state = load_data_from_bubble(url_token, org_urn_val, current_state)
show_status_box = False
if new_state.get("token") is None:
show_status_box = True
elif "Error" in status_msg or "Warning" in status_msg:
show_status_box = True
return gr.update(value=status_msg, visible=show_status_box), new_state
# REMOVED: analytics_icons and analytics_tab_instance initialization
def update_report_display(selected_report_id: str, current_token_state: dict):
"""Updates report display when a new report is selected."""
empty_header_html = """
<div class="report-title">πŸ“ Comprehensive Analysis Report</div>
<div class="report-subtitle">AI-Generated Insights</div>
"""
empty_body_markdown_no_selection = """
<div class="empty-state">
<div class="empty-state-icon">πŸ“‹</div>
<div class="empty-state-title">Select a Report</div>
</div>
"""
if not selected_report_id:
return gr.update(value=empty_header_html), gr.update(value=empty_body_markdown_no_selection)
agentic_df = current_token_state.get("bubble_agentic_analysis_data")
if agentic_df is None or agentic_df.empty:
return gr.update(value=empty_header_html), gr.update(value="<div class='empty-state'>No data</div>")
selected_report_series_df = agentic_df[agentic_df['_id'] == selected_report_id]
if selected_report_series_df.empty:
return gr.update(value=empty_header_html), gr.update(value="<div class='empty-state'>Not found</div>")
selected_report_series = selected_report_series_df.iloc[0]
formatted_content_parts = format_report_for_display(selected_report_series)
return (
gr.update(value=formatted_content_parts['header_html']),
gr.update(value=formatted_content_parts['body_markdown'])
)
def handle_home_filter_change(token_state_value, date_filter_option, custom_start_date, custom_end_date):
"""Combined function to handle home date filter changes."""
if date_filter_option == "Personalizza":
custom_row_update = gr.update(visible=True)
else:
custom_row_update = gr.update(visible=False)
kpi_updates = refresh_home_tab_ui(
token_state_value,
date_filter_option,
custom_start_date,
custom_end_date
)
return (custom_row_update,) + kpi_updates
def prepare_table_for_group(graph_group: str, token_state_value: dict):
"""
Prepara la tabella sidebar con i dati base del gruppo.
"""
try:
logging.info(f"Preparing sidebar table for group: {graph_group}")
(filtered_posts_df,
filtered_mentions_df,
filtered_comments_df,
_, _, _, _) = prepare_filtered_analytics_data(
token_state_value, "Sempre", None, None
)
if graph_group == "posts":
if filtered_posts_df.empty:
return gr.update(
value=pd.DataFrame({"Info": ["Nessun post disponibile"]}),
visible=True,
headers=["Data", "Post", "Likes", "Comments", "Shares", "Impressions"]
)
df = filtered_posts_df.copy()
df['date'] = pd.to_datetime(df['published_at'], errors='coerce')
df = df.dropna(subset=['date']).sort_values('date', ascending=False).head(10)
if 'text' not in df.columns:
df['text'] = "N/A"
output_df = pd.DataFrame({
"Data": df['date'].dt.strftime('%Y-%m-%d'),
"Post link": df['id'].apply(lambda post_id: f'<a href="https://www.linkedin.com/feed/update/{post_id}" target="_blank" style="color: #3b82f6; text-decoration: none;">Post Link</a>'),
"Post": df['text'].astype(str).str[:80] + "...",
"Likes": pd.to_numeric(df.get('likeCount', 0), errors='coerce').fillna(0).astype(int),
"Comments": pd.to_numeric(df.get('commentCount', 0), errors='coerce').fillna(0).astype(int),
"Shares": pd.to_numeric(df.get('shareCount', 0), errors='coerce').fillna(0).astype(int),
"Impressions": pd.to_numeric(df.get('impressionCount', 0), errors='coerce').fillna(0).astype(int)
})
logging.info(f"βœ… Table populated with {len(output_df)} posts")
return gr.update(
value=output_df,
visible=True,
headers=["Data", "Post link", "Post", "Likes", "Comments", "Shares", "Impressions"],
datatype=["date", "markdown", "str", "number", "number", "number", "number"],
column_widths=["12%", "12%", "30%", "7%", "7%", "7%", "7%"]
)
elif graph_group == "sentiment":
mentions_list = []
if not filtered_mentions_df.empty and 'sentiment_label' in filtered_mentions_df.columns:
mentions_df = filtered_mentions_df.copy()
mentions_df['date'] = pd.to_datetime(mentions_df['date'], errors='coerce')
mentions_df = mentions_df.dropna(subset=['date']).sort_values('date', ascending=False).head(25)
mentions_list = [{
"Data": row['date'].strftime('%Y-%m-%d'),
"Contenuto": str(row.get('mention_text', 'N/A'))[:80] + "...",
"Tipo": "Menzione",
"Sentiment": row.get('sentiment_label', 'N/A')
} for _, row in mentions_df.iterrows()]
comments_list = []
if not filtered_comments_df.empty and not filtered_posts_df.empty:
comments_df = filtered_comments_df.copy()
posts_dates_df = filtered_posts_df[['id', 'published_at']].copy()
posts_dates_df['date'] = pd.to_datetime(posts_dates_df['published_at'], errors='coerce')
comments_with_dates = pd.merge(
comments_df,
posts_dates_df[['id', 'date']],
left_on='post_id',
right_on='id',
how='left'
)
comments_df_sorted = comments_with_dates.dropna(subset=['date']).sort_values('date', ascending=False).head(25)
sentiment_col = 'sentiment_label' if 'sentiment_label' in comments_df_sorted.columns else 'sentiment'
for _, row in comments_df_sorted.iterrows():
content_text = str(row.get('comment_text', 'N/A'))[:80] + "..."
comments_list.append({
"Data": row['date'].strftime('%Y-%m-%d'),
"Contenuto": content_text,
"Tipo": "Comment",
"Sentiment": row.get(sentiment_col, 'N/A')
})
combined = mentions_list + comments_list
if not combined:
return gr.update(
value=pd.DataFrame({"Info": ["Nessun dato sentiment disponibile"]}),
visible=True
)
combined_sorted = sorted(combined, key=lambda x: x['Data'], reverse=True)
output_df = pd.DataFrame(combined_sorted).head(50)
logging.info(f"βœ… Table populated with {len(output_df)} sentiment records")
return gr.update(
value=output_df,
visible=True,
headers=["Data", "Contenuto", "Tipo", "Sentiment"],
datatype=["str", "str", "str", "str"],
column_widths=["20%", "40%", "15%", "15%"]
)
else:
logging.info(f"Table hidden for group: {graph_group}")
return gr.update(visible=False)
except Exception as e:
logging.error(f"Error preparing table: {e}", exc_info=True)
return gr.update(
value=pd.DataFrame({"Errore": [f"Errore: {str(e)}"]}),
visible=True
)
def open_analysis_page(graph_group: str, current_token_state):
"""
Apre la pagina dettagliata rendendo visibile il Column overlay
e nascondendo il contenuto principale della Home.
"""
try:
date_filter = "Sempre"
logging.info(f"Opening analysis page for group '{graph_group}'")
(filtered_posts_df, filtered_mentions_df, filtered_comments_df,
filtered_follower_df, raw_follower_df, _, _) = prepare_filtered_analytics_data(
current_token_state, date_filter, None, None
)
figures = generate_sidebar_graphs_for_group(
graph_group,
filtered_posts_df,
filtered_mentions_df,
filtered_comments_df,
filtered_follower_df,
raw_follower_df,
current_token_state
)
logging.info(f"Generated {len(figures)} figures for group '{graph_group}'")
figure_updates = []
for i in range(6):
if i < len(figures):
figure_updates.append(gr.update(value=figures[i], visible=True))
else:
figure_updates.append(gr.update(value=None, visible=False))
is_posts_group = (graph_group == "posts")
col2_visibility_update = gr.update(visible=not is_posts_group)
table_update = prepare_table_for_group(graph_group, current_token_state)
analysis_page_visibility = gr.update(visible=True)
home_content_visibility = gr.update(visible=False)
logging.info(f"βœ… Analysis page opened successfully for '{graph_group}'")
return (
analysis_page_visibility,
*figure_updates,
graph_group,
col2_visibility_update,
table_update,
home_content_visibility
)
except Exception as e:
logging.error(f"Error opening analysis page: {e}", exc_info=True)
empty_updates = [gr.update(value=None, visible=False) for _ in range(6)]
return (
gr.update(visible=True),
*empty_updates,
None,
gr.update(visible=True),
gr.update(visible=False),
gr.update(visible=False)
)
def close_analysis_page():
"""
Chiude la pagina dettagliata nascondendo il Column overlay
e ripristinando la visibilitΓ  del contenuto Home.
"""
return (
gr.update(visible=False),
gr.update(visible=True)
)
with gr.Tabs() as tabs:
with gr.TabItem("🏠 Home", id="tab_home"):
(
home_content_col,
home_date_filter_selector, home_custom_dates_row, home_custom_start_date, home_custom_end_date, home_apply_filter_btn,
home_kpi_new_followers, home_kpi_growth_rate, home_kpi_brand_sentiment,
home_perf_engagement_plot, home_perf_detail_btn,
home_kpi_top_topics, home_kpi_top_formats,
home_kpi_follower_persona,
analysis_page_container,
home_sidebar_graph1, home_sidebar_graph2, home_sidebar_graph3,
home_sidebar_graph4, home_sidebar_graph5, home_sidebar_graph6,
home_sidebar_column_2,
home_sidebar_close_btn,
home_follower_detail_btn, home_sentiment_detail_btn,
home_sidebar_selection_table
) = build_dynamic_home_tab_ui()
home_refresh_outputs_list = [
home_kpi_new_followers, home_kpi_growth_rate, home_kpi_brand_sentiment,
home_perf_engagement_plot, home_kpi_top_topics, home_kpi_top_formats,
home_kpi_follower_persona
]
home_change_handler_outputs = [home_custom_dates_row] + home_refresh_outputs_list
sidebar_outputs = [
analysis_page_container,
home_sidebar_graph1, home_sidebar_graph2,
home_sidebar_graph3, home_sidebar_graph4,
home_sidebar_graph5, home_sidebar_graph6,
active_graph_group_st,
home_sidebar_column_2,
home_sidebar_selection_table,
home_content_col
]
home_perf_detail_btn.click(
fn=lambda ts: open_analysis_page("posts", ts),
inputs=[token_state],
outputs=sidebar_outputs
)
home_follower_detail_btn.click(
fn=lambda ts: open_analysis_page("followers", ts),
inputs=[token_state],
outputs=sidebar_outputs
)
home_sentiment_detail_btn.click(
fn=lambda ts: open_analysis_page("sentiment", ts),
inputs=[token_state],
outputs=sidebar_outputs
)
home_sidebar_close_btn.click(
fn=close_analysis_page,
outputs=[analysis_page_container, home_content_col]
)
home_date_filter_selector.change(
fn=handle_home_filter_change,
inputs=[token_state, home_date_filter_selector, home_custom_start_date, home_custom_end_date],
outputs=home_change_handler_outputs,
show_progress="full"
)
home_apply_filter_btn.click(
fn=refresh_home_tab_ui,
inputs=[token_state, home_date_filter_selector, home_custom_start_date, home_custom_end_date],
outputs=home_refresh_outputs_list,
show_progress="full"
)
# REMOVED: analytics_tab_instance.create_tab_ui()
with gr.TabItem("πŸ“ AI Analysis Reports", id="tab_agentic_report", visible=AGENTIC_MODULES_LOADED):
agentic_pipeline_status_md, report_selector_dd, report_header_html_display, report_body_markdown_display = \
create_enhanced_report_tab(AGENTIC_MODULES_LOADED)
with gr.TabItem("🎯 OKRs & Action Items", id="tab_agentic_okrs", visible=AGENTIC_MODULES_LOADED):
gr.Markdown("## 🎯 AI Generated OKRs")
if not AGENTIC_MODULES_LOADED:
gr.Markdown("πŸ”΄ **Error:** Agentic modules not loaded.")
with gr.Column(visible=False):
key_results_cbg = gr.CheckboxGroup(label="Select Key Results", choices=[], value=[])
okr_detail_display_md = gr.Markdown("Details will appear here.")
enhanced_okr_display_html = create_enhanced_okr_tab()
if AGENTIC_MODULES_LOADED:
report_selector_dd.change(
fn=update_report_display,
inputs=[report_selector_dd, token_state],
outputs=[report_header_html_display, report_body_markdown_display],
show_progress="minimal"
)
agentic_display_outputs = [
agentic_pipeline_status_md,
report_selector_dd,
key_results_cbg,
okr_detail_display_md,
orchestration_raw_results_st,
selected_key_result_ids_st,
key_results_for_selection_st,
report_header_html_display,
report_body_markdown_display,
reconstruction_cache_st,
enhanced_okr_display_html,
actionable_okrs_data_st
]
# REFACTORED: Simplified event chain without analytics_load_event
initial_load_event = org_urn_display.change(
fn=lambda: gr.update(value="Loading data...", visible=True),
inputs=[],
outputs=[status_box],
show_progress="full"
).then(
fn=initial_data_load_sequence,
inputs=[url_user_token_display, org_urn_display, token_state],
outputs=[status_box, token_state],
show_progress="full"
)
# REFACTORED: home_load_event now follows directly after initial_load_event
home_load_event = initial_load_event.then(
fn=refresh_home_tab_ui,
inputs=[token_state, home_date_filter_selector, home_custom_start_date, home_custom_end_date],
outputs=home_refresh_outputs_list,
show_progress="minimal"
)
home_load_event.then(
fn=load_and_display_agentic_results,
inputs=[token_state, reconstruction_cache_st],
outputs=agentic_display_outputs,
show_progress="minimal"
).then(
fn=format_okrs_for_enhanced_display,
inputs=[reconstruction_cache_st],
outputs=[enhanced_okr_display_html],
show_progress="minimal"
)
if __name__ == "__main__":
if not os.environ.get(LINKEDIN_CLIENT_ID_ENV_VAR):
logging.warning(f"WARNING: '{LINKEDIN_CLIENT_ID_ENV_VAR}' not set.")
if not all(os.environ.get(var) for var in [BUBBLE_APP_NAME_ENV_VAR, BUBBLE_API_KEY_PRIVATE_ENV_VAR, BUBBLE_API_ENDPOINT_ENV_VAR]):
logging.warning("WARNING: Bubble environment variables not set.")
if not AGENTIC_MODULES_LOADED:
logging.warning("CRITICAL: Agentic modules failed to load.")
if not os.environ.get("GEMINI_API_KEY"):
logging.warning("WARNING: 'GEMINI_API_KEY' not set.")
app.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)), debug=True)