GuglielmoTor commited on
Commit
d33040c
·
verified ·
1 Parent(s): eb46c40

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +114 -129
app.py CHANGED
@@ -27,7 +27,7 @@ from ui_generators import (
27
  # Corrected import for analytics_data_processing
28
  from analytics_data_processing import prepare_filtered_analytics_data
29
  from analytics_plot_generator import (
30
- generate_posts_activity_plot, # Make sure this is available if posts_activity is in map
31
  generate_mentions_activity_plot, generate_mention_sentiment_plot,
32
  generate_followers_count_over_time_plot,
33
  generate_followers_growth_rate_plot,
@@ -52,9 +52,9 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
52
 
53
  # Mapping from plot_configs IDs to PLOT_FORMULAS keys
54
  PLOT_ID_TO_FORMULA_KEY_MAP = {
55
- "posts_activity": "posts_activity", # Example, if you add it back to plot_configs
56
- "mentions_activity": "mentions_activity", # Example, if you add it back
57
- "mention_sentiment": "mention_sentiment", # Example, if you add it back
58
  "followers_count": "followers_count_over_time",
59
  "followers_growth_rate": "followers_growth_rate",
60
  "followers_by_location": "followers_by_demographics",
@@ -72,25 +72,20 @@ PLOT_ID_TO_FORMULA_KEY_MAP = {
72
  "post_frequency_cs": "post_frequency",
73
  "content_format_breakdown_cs": "content_format_breakdown",
74
  "content_topic_breakdown_cs": "content_topic_breakdown",
75
- "mention_analysis_volume": "mentions_activity",
76
- "mention_analysis_sentiment": "mention_sentiment"
77
  }
78
 
79
 
80
  # --- Analytics Tab: Plot Figure Generation Function ---
81
  def update_analytics_plots_figures(token_state_value, date_filter_option, custom_start_date, custom_end_date):
82
  logging.info(f"Updating analytics plot figures. Filter: {date_filter_option}, Custom Start: {custom_start_date}, Custom End: {custom_end_date}")
83
- # This num_expected_plots should align with the number of gr.Plot components expected as output.
84
- # The plot_configs list has 19 items.
85
- # If generate_posts_activity_plot, generate_mentions_activity_plot, generate_mention_sentiment_plot
86
- # are added back to the main list of plots directly, this number might change.
87
- # For now, it seems the detailed mention plots are re-using figures.
88
- num_expected_plots = 19 # Adjusted to match plot_configs length for clarity in this function's direct output list construction
89
 
90
  if not token_state_value or not token_state_value.get("token"):
91
- message = "❌ Access denied. No token. Cannot generate analytics."
92
  logging.warning(message)
93
- placeholder_figs = [create_placeholder_plot(title="Access Denied", message="No token.") for _ in range(num_expected_plots)]
94
  return [message] + placeholder_figs
95
 
96
  try:
@@ -103,31 +98,27 @@ def update_analytics_plots_figures(token_state_value, date_filter_option, custom
103
  token_state_value, date_filter_option, custom_start_date, custom_end_date
104
  )
105
  except Exception as e:
106
- error_msg = f"❌ Error preparing analytics data: {e}"
107
  logging.error(error_msg, exc_info=True)
108
- placeholder_figs = [create_placeholder_plot(title="Data Preparation Error", message=str(e)) for _ in range(num_expected_plots)]
109
  return [error_msg] + placeholder_figs
110
 
111
  date_column_posts = token_state_value.get("config_date_col_posts", "published_at")
112
  date_column_mentions = token_state_value.get("config_date_col_mentions", "date")
113
  media_type_col_name = token_state_value.get("config_media_type_col", "media_type")
114
- eb_labels_col_name = token_state_value.get("config_eb_labels_col", "li_eb_label") # Corrected from li_eb_label to li_eb_labels if that's the actual key in formulas
115
 
116
  plot_figs = []
117
  try:
118
- # These plots are generated once and potentially re-used if their IDs match later plot_configs
119
- # However, the current plot_configs doesn't list these explicitly at the start,
120
- # but "mention_analysis_volume" and "mention_analysis_sentiment" will use them.
121
  fig_mentions_activity_shared = generate_mentions_activity_plot(filtered_mentions_df, date_column=date_column_mentions)
122
  fig_mention_sentiment_shared = generate_mention_sentiment_plot(filtered_mentions_df)
123
 
124
- # Order matters here, must match plot_configs
125
  plot_figs.append(generate_followers_count_over_time_plot(date_filtered_follower_stats_df, type_value='follower_gains_monthly'))
126
  plot_figs.append(generate_followers_growth_rate_plot(date_filtered_follower_stats_df, type_value='follower_gains_monthly'))
127
- plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_geo', plot_title="Followers by Location"))
128
- plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_function', plot_title="Followers by Role"))
129
- plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_industry', plot_title="Followers by Industry"))
130
- plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_seniority', plot_title="Followers by Seniority"))
131
  plot_figs.append(generate_engagement_rate_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
132
  plot_figs.append(generate_reach_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
133
  plot_figs.append(generate_impressions_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
@@ -138,36 +129,34 @@ def update_analytics_plots_figures(token_state_value, date_filter_option, custom
138
  plot_figs.append(generate_comments_sentiment_breakdown_plot(filtered_merged_posts_df, sentiment_column='comment_sentiment'))
139
  plot_figs.append(generate_post_frequency_plot(filtered_merged_posts_df, date_column=date_column_posts))
140
  plot_figs.append(generate_content_format_breakdown_plot(filtered_merged_posts_df, format_col=media_type_col_name))
141
- plot_figs.append(generate_content_topic_breakdown_plot(filtered_merged_posts_df, topics_col=eb_labels_col_name)) # Ensure eb_labels_col_name matches key in formulas if different from 'li_eb_labels'
142
- plot_figs.append(fig_mentions_activity_shared) # For "mention_analysis_volume"
143
- plot_figs.append(fig_mention_sentiment_shared) # For "mention_analysis_sentiment"
144
 
145
- message = f"📊 Analytics updated for period: {date_filter_option}"
146
  if date_filter_option == "Custom Range":
147
- s_display = start_dt_for_msg.strftime('%Y-%m-%d') if start_dt_for_msg else "Any"
148
- e_display = end_dt_for_msg.strftime('%Y-%m-%d') if end_dt_for_msg else "Any"
149
- message += f" (From: {s_display} To: {e_display})"
150
 
151
  final_plot_figs = []
152
  for i, p_fig in enumerate(plot_figs):
153
  if p_fig is not None and not isinstance(p_fig, str):
154
  final_plot_figs.append(p_fig)
155
  else:
156
- logging.warning(f"Plot figure generation failed or returned unexpected type for slot {i}, using placeholder. Figure: {p_fig}")
157
- final_plot_figs.append(create_placeholder_plot(title="Plot Error", message="Failed to generate this plot figure."))
158
 
159
- # Ensure the list has exactly num_expected_plots items.
160
- # This padding should ideally not be needed if plot_figs is constructed correctly to match num_expected_plots.
161
  while len(final_plot_figs) < num_expected_plots:
162
- logging.warning(f"Padding missing plot figure. Expected {num_expected_plots}, got {len(final_plot_figs)}.")
163
- final_plot_figs.append(create_placeholder_plot(title="Missing Plot", message="Plot figure could not be generated."))
164
 
165
- return [message] + final_plot_figs[:num_expected_plots] # Ensure correct number of plot figures are returned
166
 
167
  except Exception as e:
168
- error_msg = f"❌ Error generating analytics plot figures: {e}"
169
  logging.error(error_msg, exc_info=True)
170
- placeholder_figs = [create_placeholder_plot(title="Plot Generation Error", message=str(e)) for _ in range(num_expected_plots)]
171
  return [error_msg] + placeholder_figs
172
 
173
 
@@ -182,13 +171,13 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
182
  "fetch_count_for_api": 0, "url_user_token_temp_storage": None,
183
  "config_date_col_posts": "published_at", "config_date_col_mentions": "date",
184
  "config_date_col_followers": "date", "config_media_type_col": "media_type",
185
- "config_eb_labels_col": "li_eb_labels" # Ensure this matches the column name used in plot generator and formulas.py
186
  })
187
 
188
  gr.Markdown("# 🚀 LinkedIn Organization Dashboard")
189
- url_user_token_display = gr.Textbox(label="User Token (Hidden)", interactive=False, visible=False)
190
- status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...")
191
- org_urn_display = gr.Textbox(label="Organization URN (Hidden)", interactive=False, visible=False)
192
 
193
  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)
194
 
@@ -199,10 +188,10 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
199
 
200
  with gr.Tabs() as tabs:
201
  with gr.TabItem("1️⃣ Dashboard & Sync", id="tab_dashboard_sync"):
202
- gr.Markdown("System checks for existing data from Bubble. 'Sync' activates if new data is needed.")
203
- sync_data_btn = gr.Button("🔄 Sync LinkedIn Data", variant="primary", visible=False, interactive=False)
204
- sync_status_html_output = gr.HTML("<p style='text-align:center;'>Sync status...</p>")
205
- dashboard_display_html = gr.HTML("<p style='text-align:center;'>Dashboard loading...</p>")
206
 
207
  org_urn_display.change(
208
  fn=initial_load_sequence,
@@ -211,25 +200,25 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
211
  show_progress="full"
212
  )
213
 
214
- with gr.TabItem("2️⃣ Analytics", id="tab_analytics"):
215
- gr.Markdown("## 📈 LinkedIn Performance Analytics")
216
- gr.Markdown("Select a date range. Click buttons for actions.")
217
 
218
- analytics_status_md = gr.Markdown("Analytics status...")
219
 
220
  with gr.Row():
221
  date_filter_selector = gr.Radio(
222
- ["All Time", "Last 7 Days", "Last 30 Days", "Custom Range"],
223
- label="Select Date Range", value="All Time", scale=3
224
  )
225
  with gr.Column(scale=2):
226
- custom_start_date_picker = gr.DateTime(label="Start Date", visible=False, include_time=False, type="datetime")
227
- custom_end_date_picker = gr.DateTime(label="End Date", visible=False, include_time=False, type="datetime")
228
 
229
- apply_filter_btn = gr.Button("🔍 Apply Filter & Refresh Analytics", variant="primary")
230
 
231
  def toggle_custom_date_pickers(selection):
232
- is_custom = selection == "Custom Range"
233
  return gr.update(visible=is_custom), gr.update(visible=is_custom)
234
 
235
  date_filter_selector.change(
@@ -239,27 +228,27 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
239
  )
240
 
241
  plot_configs = [
242
- {"label": "Followers Count Over Time", "id": "followers_count", "section": "Follower Dynamics"},
243
- {"label": "Followers Growth Rate", "id": "followers_growth_rate", "section": "Follower Dynamics"},
244
- {"label": "Followers by Location", "id": "followers_by_location", "section": "Follower Demographics"},
245
- {"label": "Followers by Role (Function)", "id": "followers_by_role", "section": "Follower Demographics"},
246
- {"label": "Followers by Industry", "id": "followers_by_industry", "section": "Follower Demographics"},
247
- {"label": "Followers by Seniority", "id": "followers_by_seniority", "section": "Follower Demographics"},
248
- {"label": "Engagement Rate Over Time", "id": "engagement_rate", "section": "Post Performance Insights"},
249
- {"label": "Reach Over Time", "id": "reach_over_time", "section": "Post Performance Insights"},
250
- {"label": "Impressions Over Time", "id": "impressions_over_time", "section": "Post Performance Insights"},
251
- {"label": "Reactions (Likes) Over Time", "id": "likes_over_time", "section": "Post Performance Insights"},
252
- {"label": "Clicks Over Time", "id": "clicks_over_time", "section": "Detailed Post Engagement Over Time"},
253
- {"label": "Shares Over Time", "id": "shares_over_time", "section": "Detailed Post Engagement Over Time"},
254
- {"label": "Comments Over Time", "id": "comments_over_time", "section": "Detailed Post Engagement Over Time"},
255
- {"label": "Breakdown of Comments by Sentiment", "id": "comments_sentiment", "section": "Detailed Post Engagement Over Time"},
256
- {"label": "Post Frequency", "id": "post_frequency_cs", "section": "Content Strategy Analysis"},
257
- {"label": "Breakdown of Content by Format", "id": "content_format_breakdown_cs", "section": "Content Strategy Analysis"},
258
- {"label": "Breakdown of Content by Topics", "id": "content_topic_breakdown_cs", "section": "Content Strategy Analysis"},
259
- {"label": "Mentions Volume Over Time (Detailed)", "id": "mention_analysis_volume", "section": "Mention Analysis (Detailed)"},
260
- {"label": "Breakdown of Mentions by Sentiment (Detailed)", "id": "mention_analysis_sentiment", "section": "Mention Analysis (Detailed)"}
261
  ]
262
- assert len(plot_configs) == 19, "Mismatch in plot_configs and expected plots."
263
 
264
  active_panel_action_state = gr.State(None)
265
  explored_plot_id_state = gr.State(None)
@@ -271,19 +260,19 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
271
  plot_ui_objects = build_analytics_tab_plot_area(plot_configs)
272
 
273
  with gr.Column(scale=4, visible=False) as global_actions_column_ui:
274
- gr.Markdown("### 💡 Generated Content")
275
- global_actions_markdown_ui = gr.Markdown("Click a button (💣, ƒ) on a plot to see content here.")
276
 
277
  # --- Event Handler for Insights and Formula Buttons ---
278
  def handle_panel_action(plot_id_clicked, action_type, current_active_action_from_state, current_token_state_val):
279
- logging.info(f"Action '{action_type}' for plot: {plot_id_clicked}. Current active from state: {current_active_action_from_state}")
280
 
281
  if not plot_ui_objects or plot_id_clicked not in plot_ui_objects:
282
- logging.error(f"plot_ui_objects not populated or plot_id {plot_id_clicked} not found during handle_panel_action.")
283
- error_updates = [gr.update(visible=False), "Error: UI components not ready.", None] + [gr.update() for _ in range(2 * len(plot_configs))]
284
  return error_updates
285
 
286
- clicked_plot_label = plot_ui_objects.get(plot_id_clicked, {}).get("label", "Selected Plot")
287
 
288
  hypothetical_new_active_state = {"plot_id": plot_id_clicked, "type": action_type}
289
  is_toggling_off = current_active_action_from_state == hypothetical_new_active_state
@@ -294,28 +283,28 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
294
 
295
  if is_toggling_off:
296
  new_active_action_state_to_set = None
297
- content_text = f"{action_type.capitalize()} panel for '{clicked_plot_label}' closed."
298
  action_col_visible = False
299
- logging.info(f"Closing {action_type} panel for {plot_id_clicked}")
300
  else:
301
  new_active_action_state_to_set = hypothetical_new_active_state
302
  action_col_visible = True
303
  if action_type == "insights":
304
- # TODO: Implement actual insight generation
305
- content_text = f"**Insights for: {clicked_plot_label}**\n\nPlot ID: `{plot_id_clicked}`.\n(AI insights generation placeholder)"
306
  elif action_type == "formula":
307
  formula_key = PLOT_ID_TO_FORMULA_KEY_MAP.get(plot_id_clicked)
308
  if formula_key and formula_key in PLOT_FORMULAS:
309
  formula_data = PLOT_FORMULAS[formula_key]
310
  content_text = f"### {formula_data['title']}\n\n"
311
- content_text += f"**Description:**\n{formula_data['description']}\n\n"
312
- content_text += "**Calculation Steps:**\n"
313
  for step in formula_data['calculation_steps']:
314
- content_text += f"- {step}\n"
315
  else:
316
- content_text = f"**Formula/Methodology for: {clicked_plot_label}**\n\nPlot ID: `{plot_id_clicked}`.\n(No detailed formula information found for this plot ID in `formulas.py`)"
317
- logging.info(f"Displaying formula for {plot_id_clicked} (mapped to {formula_key})")
318
- logging.info(f"Opening/switching to {action_type} panel for {plot_id_clicked}")
319
 
320
  all_button_updates = []
321
  for cfg_item in plot_configs:
@@ -341,23 +330,22 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
341
 
342
  return final_updates
343
 
344
- # --- Event Handler for Explore Button ---
345
  def handle_explore_click(plot_id_clicked, current_explored_plot_id_from_state):
346
- logging.info(f"Explore clicked for: {plot_id_clicked}. Currently explored from state: {current_explored_plot_id_from_state}")
347
 
348
  if not plot_ui_objects:
349
- logging.error("plot_ui_objects not populated during handle_explore_click.")
350
- return [current_explored_plot_id_from_state] + [gr.update() for _ in range(2 * len(plot_configs))] # 2 updates per plot_config (panel, explore_button)
351
 
352
  new_explored_id_to_set = None
353
  is_toggling_off = (plot_id_clicked == current_explored_plot_id_from_state)
354
 
355
  if is_toggling_off:
356
  new_explored_id_to_set = None
357
- logging.info(f"Un-exploring plot: {plot_id_clicked}")
358
  else:
359
  new_explored_id_to_set = plot_id_clicked
360
- logging.info(f"Exploring plot: {plot_id_clicked}")
361
 
362
  panel_and_button_updates = []
363
  for cfg in plot_configs:
@@ -425,33 +413,30 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
425
  api_name=f"action_explore_{plot_id}"
426
  )
427
  else:
428
- logging.warning(f"UI object for plot_id '{plot_id}' not found when trying to attach click handlers.")
429
 
430
  def refresh_all_analytics_ui_elements(current_token_state, date_filter_val, custom_start_val, custom_end_val):
431
- logging.info("Refreshing all analytics UI elements and resetting actions.")
432
- # The number of plots expected by update_analytics_plots_figures should match plot_configs length
433
- # For this example, it's 19.
434
  plot_generation_results = update_analytics_plots_figures(
435
  current_token_state, date_filter_val, custom_start_val, custom_end_val
436
  )
437
 
438
  status_message_update = plot_generation_results[0]
439
- generated_plot_figures = plot_generation_results[1:] # Should be list of 19 figures
440
 
441
  all_updates = [status_message_update]
442
 
443
- # Updates for plot components (19 of them)
444
- for i in range(len(plot_configs)): # Iterate 19 times
445
  if i < len(generated_plot_figures):
446
  all_updates.append(generated_plot_figures[i])
447
  else:
448
- all_updates.append(create_placeholder_plot("Figure Error", f"Missing figure for plot {plot_configs[i]['id']}"))
449
 
450
  all_updates.append(gr.update(visible=False))
451
- all_updates.append(gr.update(value="Click a button (💣, ƒ) on a plot..."))
452
  all_updates.append(None)
453
 
454
- for cfg in plot_configs: # 19 plots
455
  pid = cfg["id"]
456
  if pid in plot_ui_objects:
457
  all_updates.append(gr.update(value=BOMB_ICON))
@@ -462,12 +447,12 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
462
  all_updates.extend([None, None, None, None])
463
 
464
  all_updates.append(None)
465
- logging.info(f"Prepared {len(all_updates)} updates for analytics refresh.")
466
  return all_updates
467
 
468
  apply_filter_and_sync_outputs_list = [analytics_status_md]
469
 
470
- for config_item_filter_sync in plot_configs: # 19 plots
471
  pid_filter_sync = config_item_filter_sync["id"]
472
  if pid_filter_sync in plot_ui_objects and "plot_component" in plot_ui_objects[pid_filter_sync]:
473
  apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync]["plot_component"])
@@ -480,7 +465,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
480
  active_panel_action_state
481
  ])
482
 
483
- for cfg_filter_sync_btns in plot_configs: # 19 plots
484
  pid_filter_sync_btns = cfg_filter_sync_btns["id"]
485
  if pid_filter_sync_btns in plot_ui_objects:
486
  apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync_btns]["bomb_button"])
@@ -492,7 +477,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
492
 
493
  apply_filter_and_sync_outputs_list.append(explored_plot_id_state)
494
 
495
- logging.info(f"Total outputs for apply_filter/sync: {len(apply_filter_and_sync_outputs_list)}") # Expected: 1 + 19 + 3 + (19*4) + 1 = 1 + 19 + 3 + 76 + 1 = 100
496
 
497
  apply_filter_btn.click(
498
  fn=refresh_all_analytics_ui_elements,
@@ -501,24 +486,24 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
501
  show_progress="full"
502
  )
503
 
504
- with gr.TabItem("3️⃣ Mentions", id="tab_mentions"):
505
- refresh_mentions_display_btn = gr.Button("🔄 Refresh Mentions Display", variant="secondary")
506
- mentions_html = gr.HTML("Mentions data...")
507
- mentions_sentiment_dist_plot = gr.Plot(label="Mention Sentiment Distribution")
508
  refresh_mentions_display_btn.click(
509
  fn=run_mentions_tab_display, inputs=[token_state],
510
  outputs=[mentions_html, mentions_sentiment_dist_plot],
511
  show_progress="full"
512
  )
513
 
514
- with gr.TabItem("4️⃣ Follower Stats", id="tab_follower_stats"):
515
- refresh_follower_stats_btn = gr.Button("🔄 Refresh Follower Stats Display", variant="secondary")
516
- follower_stats_html = gr.HTML("Follower statistics...")
517
  with gr.Row():
518
- fs_plot_monthly_gains = gr.Plot(label="Monthly Follower Gains")
519
  with gr.Row():
520
- fs_plot_seniority = gr.Plot(label="Followers by Seniority (Top 10 Organic)")
521
- fs_plot_industry = gr.Plot(label="Followers by Industry (Top 10 Organic)")
522
 
523
  refresh_follower_stats_btn.click(
524
  fn=run_follower_stats_tab_display, inputs=[token_state],
@@ -547,16 +532,16 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
547
 
548
  if __name__ == "__main__":
549
  if not os.environ.get(LINKEDIN_CLIENT_ID_ENV_VAR):
550
- logging.warning(f"WARNING: '{LINKEDIN_CLIENT_ID_ENV_VAR}' env var not set.")
551
  if not os.environ.get(BUBBLE_APP_NAME_ENV_VAR) or \
552
  not os.environ.get(BUBBLE_API_KEY_PRIVATE_ENV_VAR) or \
553
  not os.environ.get(BUBBLE_API_ENDPOINT_ENV_VAR):
554
- logging.warning("WARNING: Bubble env vars not fully set.")
555
 
556
  try:
557
- logging.info(f"Matplotlib version: {matplotlib.__version__}, Backend: {matplotlib.get_backend()}")
558
  except ImportError:
559
- logging.error("Matplotlib is not installed. Plots will not be generated.")
560
 
561
  app.launch(server_name="0.0.0.0", server_port=7860, debug=True)
562
 
 
27
  # Corrected import for analytics_data_processing
28
  from analytics_data_processing import prepare_filtered_analytics_data
29
  from analytics_plot_generator import (
30
+ generate_posts_activity_plot,
31
  generate_mentions_activity_plot, generate_mention_sentiment_plot,
32
  generate_followers_count_over_time_plot,
33
  generate_followers_growth_rate_plot,
 
52
 
53
  # Mapping from plot_configs IDs to PLOT_FORMULAS keys
54
  PLOT_ID_TO_FORMULA_KEY_MAP = {
55
+ "posts_activity": "posts_activity",
56
+ "mentions_activity": "mentions_activity",
57
+ "mention_sentiment": "mention_sentiment",
58
  "followers_count": "followers_count_over_time",
59
  "followers_growth_rate": "followers_growth_rate",
60
  "followers_by_location": "followers_by_demographics",
 
72
  "post_frequency_cs": "post_frequency",
73
  "content_format_breakdown_cs": "content_format_breakdown",
74
  "content_topic_breakdown_cs": "content_topic_breakdown",
75
+ "mention_analysis_volume": "mentions_activity", # Mapped to the general mentions_activity
76
+ "mention_analysis_sentiment": "mention_sentiment" # Mapped to the general mention_sentiment
77
  }
78
 
79
 
80
  # --- Analytics Tab: Plot Figure Generation Function ---
81
  def update_analytics_plots_figures(token_state_value, date_filter_option, custom_start_date, custom_end_date):
82
  logging.info(f"Updating analytics plot figures. Filter: {date_filter_option}, Custom Start: {custom_start_date}, Custom End: {custom_end_date}")
83
+ num_expected_plots = 19
 
 
 
 
 
84
 
85
  if not token_state_value or not token_state_value.get("token"):
86
+ message = "❌ Accesso negato. Nessun token. Impossibile generare le analisi."
87
  logging.warning(message)
88
+ placeholder_figs = [create_placeholder_plot(title="Accesso Negato", message="Nessun token.") for _ in range(num_expected_plots)]
89
  return [message] + placeholder_figs
90
 
91
  try:
 
98
  token_state_value, date_filter_option, custom_start_date, custom_end_date
99
  )
100
  except Exception as e:
101
+ error_msg = f"❌ Errore durante la preparazione dei dati per le analisi: {e}"
102
  logging.error(error_msg, exc_info=True)
103
+ placeholder_figs = [create_placeholder_plot(title="Errore Preparazione Dati", message=str(e)) for _ in range(num_expected_plots)]
104
  return [error_msg] + placeholder_figs
105
 
106
  date_column_posts = token_state_value.get("config_date_col_posts", "published_at")
107
  date_column_mentions = token_state_value.get("config_date_col_mentions", "date")
108
  media_type_col_name = token_state_value.get("config_media_type_col", "media_type")
109
+ eb_labels_col_name = token_state_value.get("config_eb_labels_col", "li_eb_labels")
110
 
111
  plot_figs = []
112
  try:
 
 
 
113
  fig_mentions_activity_shared = generate_mentions_activity_plot(filtered_mentions_df, date_column=date_column_mentions)
114
  fig_mention_sentiment_shared = generate_mention_sentiment_plot(filtered_mentions_df)
115
 
 
116
  plot_figs.append(generate_followers_count_over_time_plot(date_filtered_follower_stats_df, type_value='follower_gains_monthly'))
117
  plot_figs.append(generate_followers_growth_rate_plot(date_filtered_follower_stats_df, type_value='follower_gains_monthly'))
118
+ plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_geo', plot_title="Follower per Località"))
119
+ plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_function', plot_title="Follower per Ruolo"))
120
+ plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_industry', plot_title="Follower per Settore"))
121
+ plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_seniority', plot_title="Follower per Anzianità"))
122
  plot_figs.append(generate_engagement_rate_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
123
  plot_figs.append(generate_reach_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
124
  plot_figs.append(generate_impressions_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
 
129
  plot_figs.append(generate_comments_sentiment_breakdown_plot(filtered_merged_posts_df, sentiment_column='comment_sentiment'))
130
  plot_figs.append(generate_post_frequency_plot(filtered_merged_posts_df, date_column=date_column_posts))
131
  plot_figs.append(generate_content_format_breakdown_plot(filtered_merged_posts_df, format_col=media_type_col_name))
132
+ plot_figs.append(generate_content_topic_breakdown_plot(filtered_merged_posts_df, topics_col=eb_labels_col_name))
133
+ plot_figs.append(fig_mentions_activity_shared)
134
+ plot_figs.append(fig_mention_sentiment_shared)
135
 
136
+ message = f"📊 Analisi aggiornate per il periodo: {date_filter_option}"
137
  if date_filter_option == "Custom Range":
138
+ s_display = start_dt_for_msg.strftime('%Y-%m-%d') if start_dt_for_msg else "Qualsiasi"
139
+ e_display = end_dt_for_msg.strftime('%Y-%m-%d') if end_dt_for_msg else "Qualsiasi"
140
+ message += f" (Da: {s_display} A: {e_display})"
141
 
142
  final_plot_figs = []
143
  for i, p_fig in enumerate(plot_figs):
144
  if p_fig is not None and not isinstance(p_fig, str):
145
  final_plot_figs.append(p_fig)
146
  else:
147
+ logging.warning(f"Generazione figura grafico fallita o tipo inatteso per slot {i}, uso placeholder. Figura: {p_fig}")
148
+ final_plot_figs.append(create_placeholder_plot(title="Errore Grafico", message="Impossibile generare questa figura."))
149
 
 
 
150
  while len(final_plot_figs) < num_expected_plots:
151
+ logging.warning(f"Aggiungo figura mancante. Previste {num_expected_plots}, ottenute {len(final_plot_figs)}.")
152
+ final_plot_figs.append(create_placeholder_plot(title="Grafico Mancante", message="Impossibile generare la figura del grafico."))
153
 
154
+ return [message] + final_plot_figs[:num_expected_plots]
155
 
156
  except Exception as e:
157
+ error_msg = f"❌ Errore durante la generazione delle figure dei grafici analitici: {e}"
158
  logging.error(error_msg, exc_info=True)
159
+ placeholder_figs = [create_placeholder_plot(title="Errore Generazione Grafici", message=str(e)) for _ in range(num_expected_plots)]
160
  return [error_msg] + placeholder_figs
161
 
162
 
 
171
  "fetch_count_for_api": 0, "url_user_token_temp_storage": None,
172
  "config_date_col_posts": "published_at", "config_date_col_mentions": "date",
173
  "config_date_col_followers": "date", "config_media_type_col": "media_type",
174
+ "config_eb_labels_col": "li_eb_labels"
175
  })
176
 
177
  gr.Markdown("# 🚀 LinkedIn Organization Dashboard")
178
+ url_user_token_display = gr.Textbox(label="User Token (Nascosto)", interactive=False, visible=False)
179
+ status_box = gr.Textbox(label="Stato Generale Token LinkedIn", interactive=False, value="Inizializzazione...")
180
+ org_urn_display = gr.Textbox(label="URN Organizzazione (Nascosto)", interactive=False, visible=False)
181
 
182
  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)
183
 
 
188
 
189
  with gr.Tabs() as tabs:
190
  with gr.TabItem("1️⃣ Dashboard & Sync", id="tab_dashboard_sync"):
191
+ gr.Markdown("Il sistema controlla i dati esistenti da Bubble. 'Sincronizza' si attiva se sono necessari nuovi dati.")
192
+ sync_data_btn = gr.Button("🔄 Sincronizza Dati LinkedIn", variant="primary", visible=False, interactive=False)
193
+ sync_status_html_output = gr.HTML("<p style='text-align:center;'>Stato sincronizzazione...</p>")
194
+ dashboard_display_html = gr.HTML("<p style='text-align:center;'>Caricamento dashboard...</p>")
195
 
196
  org_urn_display.change(
197
  fn=initial_load_sequence,
 
200
  show_progress="full"
201
  )
202
 
203
+ with gr.TabItem("2️⃣ Analisi", id="tab_analytics"):
204
+ gr.Markdown("## 📈 Analisi Performance LinkedIn")
205
+ gr.Markdown("Seleziona un intervallo di date. Clicca i pulsanti per le azioni.")
206
 
207
+ analytics_status_md = gr.Markdown("Stato analisi...")
208
 
209
  with gr.Row():
210
  date_filter_selector = gr.Radio(
211
+ ["Sempre", "Ultimi 7 Giorni", "Ultimi 30 Giorni", "Intervallo Personalizzato"],
212
+ label="Seleziona Intervallo Date", value="Sempre", scale=3
213
  )
214
  with gr.Column(scale=2):
215
+ custom_start_date_picker = gr.DateTime(label="Data Inizio", visible=False, include_time=False, type="datetime")
216
+ custom_end_date_picker = gr.DateTime(label="Data Fine", visible=False, include_time=False, type="datetime")
217
 
218
+ apply_filter_btn = gr.Button("🔍 Applica Filtro & Aggiorna Analisi", variant="primary")
219
 
220
  def toggle_custom_date_pickers(selection):
221
+ is_custom = selection == "Intervallo Personalizzato"
222
  return gr.update(visible=is_custom), gr.update(visible=is_custom)
223
 
224
  date_filter_selector.change(
 
228
  )
229
 
230
  plot_configs = [
231
+ {"label": "Numero di Follower nel Tempo", "id": "followers_count", "section": "Dinamiche dei Follower"},
232
+ {"label": "Tasso di Crescita Follower", "id": "followers_growth_rate", "section": "Dinamiche dei Follower"},
233
+ {"label": "Follower per Località", "id": "followers_by_location", "section": "Demografia Follower"},
234
+ {"label": "Follower per Ruolo (Funzione)", "id": "followers_by_role", "section": "Demografia Follower"},
235
+ {"label": "Follower per Settore", "id": "followers_by_industry", "section": "Demografia Follower"},
236
+ {"label": "Follower per Anzianità", "id": "followers_by_seniority", "section": "Demografia Follower"},
237
+ {"label": "Tasso di Engagement nel Tempo", "id": "engagement_rate", "section": "Approfondimenti Performance Post"},
238
+ {"label": "Copertura nel Tempo", "id": "reach_over_time", "section": "Approfondimenti Performance Post"},
239
+ {"label": "Visualizzazioni nel Tempo", "id": "impressions_over_time", "section": "Approfondimenti Performance Post"},
240
+ {"label": "Reazioni (Like) nel Tempo", "id": "likes_over_time", "section": "Approfondimenti Performance Post"},
241
+ {"label": "Click nel Tempo", "id": "clicks_over_time", "section": "Engagement Dettagliato Post nel Tempo"},
242
+ {"label": "Condivisioni nel Tempo", "id": "shares_over_time", "section": "Engagement Dettagliato Post nel Tempo"},
243
+ {"label": "Commenti nel Tempo", "id": "comments_over_time", "section": "Engagement Dettagliato Post nel Tempo"},
244
+ {"label": "Ripartizione Commenti per Sentiment", "id": "comments_sentiment", "section": "Engagement Dettagliato Post nel Tempo"},
245
+ {"label": "Frequenza Post", "id": "post_frequency_cs", "section": "Analisi Strategia Contenuti"},
246
+ {"label": "Ripartizione Contenuti per Formato", "id": "content_format_breakdown_cs", "section": "Analisi Strategia Contenuti"},
247
+ {"label": "Ripartizione Contenuti per Argomenti", "id": "content_topic_breakdown_cs", "section": "Analisi Strategia Contenuti"},
248
+ {"label": "Volume Menzioni nel Tempo (Dettaglio)", "id": "mention_analysis_volume", "section": "Analisi Menzioni (Dettaglio)"},
249
+ {"label": "Ripartizione Menzioni per Sentiment (Dettaglio)", "id": "mention_analysis_sentiment", "section": "Analisi Menzioni (Dettaglio)"}
250
  ]
251
+ assert len(plot_configs) == 19, "Mancata corrispondenza in plot_configs e grafici attesi."
252
 
253
  active_panel_action_state = gr.State(None)
254
  explored_plot_id_state = gr.State(None)
 
260
  plot_ui_objects = build_analytics_tab_plot_area(plot_configs)
261
 
262
  with gr.Column(scale=4, visible=False) as global_actions_column_ui:
263
+ gr.Markdown("### 💡 Contenuto Generato")
264
+ global_actions_markdown_ui = gr.Markdown("Clicca un pulsante (💣, ƒ) su un grafico per vedere il contenuto qui.")
265
 
266
  # --- Event Handler for Insights and Formula Buttons ---
267
  def handle_panel_action(plot_id_clicked, action_type, current_active_action_from_state, current_token_state_val):
268
+ logging.info(f"Azione '{action_type}' per grafico: {plot_id_clicked}. Attualmente attivo da stato: {current_active_action_from_state}")
269
 
270
  if not plot_ui_objects or plot_id_clicked not in plot_ui_objects:
271
+ logging.error(f"plot_ui_objects non popolato o plot_id {plot_id_clicked} non trovato durante handle_panel_action.")
272
+ error_updates = [gr.update(visible=False), "Errore: Componenti UI non pronti.", None] + [gr.update() for _ in range(2 * len(plot_configs))]
273
  return error_updates
274
 
275
+ clicked_plot_label = plot_ui_objects.get(plot_id_clicked, {}).get("label", "Grafico Selezionato")
276
 
277
  hypothetical_new_active_state = {"plot_id": plot_id_clicked, "type": action_type}
278
  is_toggling_off = current_active_action_from_state == hypothetical_new_active_state
 
283
 
284
  if is_toggling_off:
285
  new_active_action_state_to_set = None
286
+ content_text = f"Pannello {action_type.capitalize()} per '{clicked_plot_label}' chiuso."
287
  action_col_visible = False
288
+ logging.info(f"Chiusura pannello {action_type} per {plot_id_clicked}")
289
  else:
290
  new_active_action_state_to_set = hypothetical_new_active_state
291
  action_col_visible = True
292
  if action_type == "insights":
293
+ # TODO: Implementare generazione insight reali
294
+ content_text = f"**Approfondimenti per: {clicked_plot_label}**\n\nID Grafico: `{plot_id_clicked}`.\n(Placeholder generazione insight AI)"
295
  elif action_type == "formula":
296
  formula_key = PLOT_ID_TO_FORMULA_KEY_MAP.get(plot_id_clicked)
297
  if formula_key and formula_key in PLOT_FORMULAS:
298
  formula_data = PLOT_FORMULAS[formula_key]
299
  content_text = f"### {formula_data['title']}\n\n"
300
+ content_text += f"**Descrizione:**\n{formula_data['description']}\n\n"
301
+ content_text += "**Come viene calcolato:**\n" # Titolo sezione in italiano
302
  for step in formula_data['calculation_steps']:
303
+ content_text += f"{step}\n" # Rimosso il "-" per evitare doppio bullet point
304
  else:
305
+ content_text = f"**Formula/Metodologia per: {clicked_plot_label}**\n\nID Grafico: `{plot_id_clicked}`.\n(Nessuna informazione dettagliata sulla formula trovata per questo ID grafico in `formulas.py`)"
306
+ logging.info(f"Visualizzazione formula per {plot_id_clicked} (mappato a {formula_key})")
307
+ logging.info(f"Apertura/passaggio a pannello {action_type} per {plot_id_clicked}")
308
 
309
  all_button_updates = []
310
  for cfg_item in plot_configs:
 
330
 
331
  return final_updates
332
 
 
333
  def handle_explore_click(plot_id_clicked, current_explored_plot_id_from_state):
334
+ logging.info(f"Click su Esplora per: {plot_id_clicked}. Attualmente esplorato da stato: {current_explored_plot_id_from_state}")
335
 
336
  if not plot_ui_objects:
337
+ logging.error("plot_ui_objects non popolato durante handle_explore_click.")
338
+ return [current_explored_plot_id_from_state] + [gr.update() for _ in range(2 * len(plot_configs))]
339
 
340
  new_explored_id_to_set = None
341
  is_toggling_off = (plot_id_clicked == current_explored_plot_id_from_state)
342
 
343
  if is_toggling_off:
344
  new_explored_id_to_set = None
345
+ logging.info(f"Interruzione esplorazione grafico: {plot_id_clicked}")
346
  else:
347
  new_explored_id_to_set = plot_id_clicked
348
+ logging.info(f"Esplorazione grafico: {plot_id_clicked}")
349
 
350
  panel_and_button_updates = []
351
  for cfg in plot_configs:
 
413
  api_name=f"action_explore_{plot_id}"
414
  )
415
  else:
416
+ logging.warning(f"Oggetto UI per plot_id '{plot_id}' non trovato durante il tentativo di associare i gestori di click.")
417
 
418
  def refresh_all_analytics_ui_elements(current_token_state, date_filter_val, custom_start_val, custom_end_val):
419
+ logging.info("Aggiornamento di tutti gli elementi UI delle analisi e reset delle azioni.")
 
 
420
  plot_generation_results = update_analytics_plots_figures(
421
  current_token_state, date_filter_val, custom_start_val, custom_end_val
422
  )
423
 
424
  status_message_update = plot_generation_results[0]
425
+ generated_plot_figures = plot_generation_results[1:]
426
 
427
  all_updates = [status_message_update]
428
 
429
+ for i in range(len(plot_configs)):
 
430
  if i < len(generated_plot_figures):
431
  all_updates.append(generated_plot_figures[i])
432
  else:
433
+ all_updates.append(create_placeholder_plot("Errore Figura", f"Figura mancante per grafico {plot_configs[i]['id']}"))
434
 
435
  all_updates.append(gr.update(visible=False))
436
+ all_updates.append(gr.update(value="Clicca un pulsante (💣, ƒ) su un grafico..."))
437
  all_updates.append(None)
438
 
439
+ for cfg in plot_configs:
440
  pid = cfg["id"]
441
  if pid in plot_ui_objects:
442
  all_updates.append(gr.update(value=BOMB_ICON))
 
447
  all_updates.extend([None, None, None, None])
448
 
449
  all_updates.append(None)
450
+ logging.info(f"Preparati {len(all_updates)} aggiornamenti per il refresh delle analisi.")
451
  return all_updates
452
 
453
  apply_filter_and_sync_outputs_list = [analytics_status_md]
454
 
455
+ for config_item_filter_sync in plot_configs:
456
  pid_filter_sync = config_item_filter_sync["id"]
457
  if pid_filter_sync in plot_ui_objects and "plot_component" in plot_ui_objects[pid_filter_sync]:
458
  apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync]["plot_component"])
 
465
  active_panel_action_state
466
  ])
467
 
468
+ for cfg_filter_sync_btns in plot_configs:
469
  pid_filter_sync_btns = cfg_filter_sync_btns["id"]
470
  if pid_filter_sync_btns in plot_ui_objects:
471
  apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync_btns]["bomb_button"])
 
477
 
478
  apply_filter_and_sync_outputs_list.append(explored_plot_id_state)
479
 
480
+ logging.info(f"Output totali per apply_filter/sync: {len(apply_filter_and_sync_outputs_list)}")
481
 
482
  apply_filter_btn.click(
483
  fn=refresh_all_analytics_ui_elements,
 
486
  show_progress="full"
487
  )
488
 
489
+ with gr.TabItem("3️⃣ Menzioni", id="tab_mentions"):
490
+ refresh_mentions_display_btn = gr.Button("🔄 Aggiorna Visualizzazione Menzioni", variant="secondary")
491
+ mentions_html = gr.HTML("Dati menzioni...")
492
+ mentions_sentiment_dist_plot = gr.Plot(label="Distribuzione Sentiment Menzioni")
493
  refresh_mentions_display_btn.click(
494
  fn=run_mentions_tab_display, inputs=[token_state],
495
  outputs=[mentions_html, mentions_sentiment_dist_plot],
496
  show_progress="full"
497
  )
498
 
499
+ with gr.TabItem("4️⃣ Statistiche Follower", id="tab_follower_stats"):
500
+ refresh_follower_stats_btn = gr.Button("🔄 Aggiorna Visualizzazione Statistiche Follower", variant="secondary")
501
+ follower_stats_html = gr.HTML("Statistiche follower...")
502
  with gr.Row():
503
+ fs_plot_monthly_gains = gr.Plot(label="Guadagni Mensili Follower")
504
  with gr.Row():
505
+ fs_plot_seniority = gr.Plot(label="Follower per Anzianità (Top 10 Organici)")
506
+ fs_plot_industry = gr.Plot(label="Follower per Settore (Top 10 Organici)")
507
 
508
  refresh_follower_stats_btn.click(
509
  fn=run_follower_stats_tab_display, inputs=[token_state],
 
532
 
533
  if __name__ == "__main__":
534
  if not os.environ.get(LINKEDIN_CLIENT_ID_ENV_VAR):
535
+ logging.warning(f"ATTENZIONE: Variabile d'ambiente '{LINKEDIN_CLIENT_ID_ENV_VAR}' non impostata.")
536
  if not os.environ.get(BUBBLE_APP_NAME_ENV_VAR) or \
537
  not os.environ.get(BUBBLE_API_KEY_PRIVATE_ENV_VAR) or \
538
  not os.environ.get(BUBBLE_API_ENDPOINT_ENV_VAR):
539
+ logging.warning("ATTENZIONE: Variabili d'ambiente Bubble non completamente impostate.")
540
 
541
  try:
542
+ logging.info(f"Versione Matplotlib: {matplotlib.__version__}, Backend: {matplotlib.get_backend()}")
543
  except ImportError:
544
+ logging.error("Matplotlib non è installato. I grafici non verranno generati.")
545
 
546
  app.launch(server_name="0.0.0.0", server_port=7860, debug=True)
547