Spaces:
Sleeping
Sleeping
| #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) |