#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 = """
πŸ“ Comprehensive Analysis Report
AI-Generated Insights
""" empty_body_markdown = """
πŸ“„
No Report Selected
""" 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': '

Modules not loaded.

', '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 = """
πŸ“ Comprehensive Analysis Report
AI-Generated Insights
""" empty_body_markdown_no_selection = """
πŸ“‹
Select a Report
""" 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="
No data
") 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="
Not found
") 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'Post Link'), "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)