GuglielmoTor commited on
Commit
8673558
·
verified ·
1 Parent(s): 3152dad

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +228 -233
app.py CHANGED
@@ -5,6 +5,7 @@ import logging
5
  import matplotlib
6
  matplotlib.use('Agg') # Set backend for Matplotlib to avoid GUI conflicts with Gradio
7
  import matplotlib.pyplot as plt
 
8
 
9
  # --- Module Imports ---
10
  from gradio_utils import get_url_user_token
@@ -34,7 +35,7 @@ from analytics_plot_generator import (
34
  generate_engagement_rate_over_time_plot,
35
  generate_reach_over_time_plot,
36
  generate_impressions_over_time_plot,
37
- create_placeholder_plot,
38
  generate_likes_over_time_plot,
39
  generate_clicks_over_time_plot,
40
  generate_shares_over_time_plot,
@@ -51,7 +52,7 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
51
  # --- Analytics Tab: Plot Figure Generation Function ---
52
  def update_analytics_plots_figures(token_state_value, date_filter_option, custom_start_date, custom_end_date):
53
  logging.info(f"Updating analytics plot figures. Filter: {date_filter_option}, Custom Start: {custom_start_date}, Custom End: {custom_end_date}")
54
- num_expected_plots = 23
55
 
56
  if not token_state_value or not token_state_value.get("token"):
57
  message = "❌ Access denied. No token. Cannot generate analytics."
@@ -60,6 +61,8 @@ def update_analytics_plots_figures(token_state_value, date_filter_option, custom
60
  return [message] + placeholder_figs
61
 
62
  try:
 
 
63
  (filtered_merged_posts_df,
64
  filtered_mentions_df,
65
  date_filtered_follower_stats_df,
@@ -68,6 +71,7 @@ def update_analytics_plots_figures(token_state_value, date_filter_option, custom
68
  prepare_filtered_analytics_data(
69
  token_state_value, date_filter_option, custom_start_date, custom_end_date
70
  )
 
71
  except Exception as e:
72
  error_msg = f"❌ Error preparing analytics data: {e}"
73
  logging.error(error_msg, exc_info=True)
@@ -80,8 +84,17 @@ def update_analytics_plots_figures(token_state_value, date_filter_option, custom
80
  eb_labels_col_name = token_state_value.get("config_eb_labels_col", "eb_labels")
81
 
82
  plot_figs = []
 
 
 
83
  try:
 
 
 
84
  plot_figs.append(generate_posts_activity_plot(filtered_merged_posts_df, date_column=date_column_posts))
 
 
 
85
  plot_figs.append(generate_engagement_type_plot(filtered_merged_posts_df))
86
  fig_mentions_activity_shared = generate_mentions_activity_plot(filtered_mentions_df, date_column=date_column_mentions)
87
  fig_mention_sentiment_shared = generate_mention_sentiment_plot(filtered_mentions_df)
@@ -94,7 +107,7 @@ def update_analytics_plots_figures(token_state_value, date_filter_option, custom
94
  plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_industry', plot_title="Followers by Industry"))
95
  plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_seniority', plot_title="Followers by Seniority"))
96
  plot_figs.append(generate_engagement_rate_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
97
- plot_figs.append(generate_reach_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
98
  plot_figs.append(generate_impressions_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
99
  plot_figs.append(generate_likes_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
100
  plot_figs.append(generate_clicks_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
@@ -104,27 +117,30 @@ def update_analytics_plots_figures(token_state_value, date_filter_option, custom
104
  plot_figs.append(generate_post_frequency_plot(filtered_merged_posts_df, date_column=date_column_posts))
105
  plot_figs.append(generate_content_format_breakdown_plot(filtered_merged_posts_df, format_col=media_type_col_name))
106
  plot_figs.append(generate_content_topic_breakdown_plot(filtered_merged_posts_df, topics_col=eb_labels_col_name))
107
- plot_figs.append(fig_mentions_activity_shared)
108
- plot_figs.append(fig_mention_sentiment_shared)
 
 
 
109
 
110
  message = f"📊 Analytics updated for period: {date_filter_option}"
111
  if date_filter_option == "Custom Range":
112
  s_display = start_dt_for_msg.strftime('%Y-%m-%d') if start_dt_for_msg else "Any"
113
  e_display = end_dt_for_msg.strftime('%Y-%m-%d') if end_dt_for_msg else "Any"
114
  message += f" (From: {s_display} To: {e_display})"
115
-
116
  final_plot_figs = []
117
  for i, p_fig in enumerate(plot_figs):
118
- if p_fig is not None and not isinstance(p_fig, str):
119
  final_plot_figs.append(p_fig)
120
  else:
121
  logging.warning(f"Plot figure generation failed or returned unexpected type for slot {i}, using placeholder. Figure: {p_fig}")
122
  final_plot_figs.append(create_placeholder_plot(title="Plot Error", message="Failed to generate this plot figure."))
123
-
124
  while len(final_plot_figs) < num_expected_plots:
125
  logging.warning(f"Padding missing plot figure. Expected {num_expected_plots}, got {len(final_plot_figs)}.")
126
  final_plot_figs.append(create_placeholder_plot(title="Missing Plot", message="Plot figure could not be generated."))
127
-
128
  return [message] + final_plot_figs[:num_expected_plots]
129
 
130
  except Exception as e:
@@ -173,22 +189,22 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
173
  outputs=[status_box, token_state, sync_data_btn, dashboard_display_html],
174
  show_progress="full"
175
  )
176
-
177
  with gr.TabItem("2️⃣ Analytics", id="tab_analytics"):
178
  gr.Markdown("## 📈 LinkedIn Performance Analytics")
179
  gr.Markdown("Select a date range. Click buttons for actions.")
180
-
181
  analytics_status_md = gr.Markdown("Analytics status...")
182
 
183
- with gr.Row():
184
  date_filter_selector = gr.Radio(
185
  ["All Time", "Last 7 Days", "Last 30 Days", "Custom Range"],
186
  label="Select Date Range", value="Last 30 Days", scale=3
187
  )
188
  with gr.Column(scale=2):
189
- custom_start_date_picker = gr.DateTime(label="Start Date", visible=False, include_time=False, type="datetime")
190
- custom_end_date_picker = gr.DateTime(label="End Date", visible=False, include_time=False, type="datetime")
191
-
192
  apply_filter_btn = gr.Button("🔍 Apply Filter & Refresh Analytics", variant="primary")
193
 
194
  def toggle_custom_date_pickers(selection):
@@ -204,8 +220,8 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
204
  plot_configs = [
205
  {"label": "Posts Activity Over Time", "id": "posts_activity", "section": "Posts & Engagement Overview"},
206
  {"label": "Post Engagement Types", "id": "engagement_type", "section": "Posts & Engagement Overview"},
207
- {"label": "Mentions Activity Over Time", "id": "mentions_activity", "section": "Mentions Overview"},
208
- {"label": "Mention Sentiment Distribution", "id": "mention_sentiment", "section": "Mentions Overview"},
209
  {"label": "Followers Count Over Time", "id": "followers_count", "section": "Follower Dynamics"},
210
  {"label": "Followers Growth Rate", "id": "followers_growth_rate", "section": "Follower Dynamics"},
211
  {"label": "Followers by Location", "id": "followers_by_location", "section": "Follower Demographics"},
@@ -213,7 +229,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
213
  {"label": "Followers by Industry", "id": "followers_by_industry", "section": "Follower Demographics"},
214
  {"label": "Followers by Seniority", "id": "followers_by_seniority", "section": "Follower Demographics"},
215
  {"label": "Engagement Rate Over Time", "id": "engagement_rate", "section": "Post Performance Insights"},
216
- {"label": "Reach Over Time (Clicks)", "id": "reach_over_time", "section": "Post Performance Insights"},
217
  {"label": "Impressions Over Time", "id": "impressions_over_time", "section": "Post Performance Insights"},
218
  {"label": "Reactions (Likes) Over Time", "id": "likes_over_time", "section": "Post Performance Insights"},
219
  {"label": "Clicks Over Time", "id": "clicks_over_time", "section": "Detailed Post Engagement Over Time"},
@@ -223,218 +239,196 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
223
  {"label": "Post Frequency", "id": "post_frequency_cs", "section": "Content Strategy Analysis"},
224
  {"label": "Breakdown of Content by Format", "id": "content_format_breakdown_cs", "section": "Content Strategy Analysis"},
225
  {"label": "Breakdown of Content by Topics", "id": "content_topic_breakdown_cs", "section": "Content Strategy Analysis"},
226
- {"label": "Mentions Volume Over Time (Detailed)", "id": "mention_analysis_volume", "section": "Mention Analysis (Detailed)"},
227
- {"label": "Breakdown of Mentions by Sentiment (Detailed)", "id": "mention_analysis_sentiment", "section": "Mention Analysis (Detailed)"}
228
  ]
229
  assert len(plot_configs) == 23, "Mismatch in plot_configs and expected plots."
230
 
231
- # --- State for Analytics Tab interactivity ---
232
- # Stores {"plot_id": "action_type"} e.g. {"posts_activity": "insights"} or None
233
- active_panel_action_state = gr.State(None)
234
- # Stores plot_id of the currently explored plot, or None
235
- explored_plot_id_state = gr.State(None)
236
 
237
- with gr.Row(equal_height=False):
238
- with gr.Column(scale=8) as plots_area_col:
 
239
  plot_ui_objects = build_analytics_tab_plot_area(plot_configs)
240
-
241
- with gr.Column(scale=4, visible=False) as global_actions_column_ui: # Renamed for clarity
242
- gr.Markdown("### 💡 Generated Content") # More generic title
243
  global_actions_markdown_ui = gr.Markdown("Click a button (💣, ƒ) on a plot to see content here.")
244
 
245
 
246
  # --- Event Handler for Insights and Formula Buttons ---
247
- def handle_panel_action(plot_id_clicked, action_type, current_active_action, token_state_val):
248
- logging.info(f"Action '{action_type}' for plot: {plot_id_clicked}. Current active: {current_active_action}")
249
-
 
 
 
 
 
 
 
 
 
 
250
  clicked_plot_label = plot_ui_objects.get(plot_id_clicked, {}).get("label", "Selected Plot")
251
 
252
- # Determine new state for the action panel
253
- new_active_action_state = {"plot_id": plot_id_clicked, "type": action_type}
254
- is_toggling_off = current_active_action == new_active_action_state
255
 
 
256
  content_text = ""
257
  action_col_visible = False
258
-
259
- # Button icon updates - prepare a list of gr.update for all buttons
260
- button_updates = {} # Dict to hold updates for gr.Button objects
261
 
262
  if is_toggling_off:
263
- new_active_action_state = None
264
- content_text = f"{action_type.capitalize()} for {clicked_plot_label} hidden."
265
  action_col_visible = False
266
  logging.info(f"Closing {action_type} panel for {plot_id_clicked}")
267
- # Reset the clicked button to its original icon
268
- if action_type == "insights":
269
- button_updates[plot_ui_objects[plot_id_clicked]["bomb_button"]] = gr.update(value=BOMB_ICON)
270
- elif action_type == "formula":
271
- button_updates[plot_ui_objects[plot_id_clicked]["formula_button"]] = gr.update(value=FORMULA_ICON)
272
  else: # Activating or switching
 
273
  action_col_visible = True
274
  if action_type == "insights":
275
- # TODO: Implement actual insight generation
276
- content_text = f"**Insights for: {clicked_plot_label}**\n\nPlot ID: `{plot_id_clicked}`.\nDetailed AI insights here."
277
- button_updates[plot_ui_objects[plot_id_clicked]["bomb_button"]] = gr.update(value=ACTIVE_ICON)
278
  elif action_type == "formula":
279
  # TODO: Implement actual formula display
280
- content_text = f"**Formula/Methodology for: {clicked_plot_label}**\n\nPlot ID: `{plot_id_clicked}`.\nMethodology details here."
281
- button_updates[plot_ui_objects[plot_id_clicked]["formula_button"]] = gr.update(value=ACTIVE_ICON)
282
-
283
- logging.info(f"Opening {action_type} panel for {plot_id_clicked}")
284
-
285
- # Reset other action buttons for the *same* plot if a new action is chosen for it
286
- # And reset all buttons if switching plots or toggling off
287
- for pid, ui_obj in plot_ui_objects.items():
288
- is_current_plot_active_bomb = new_active_action_state == {"plot_id": pid, "type": "insights"}
289
- is_current_plot_active_formula = new_active_action_state == {"plot_id": pid, "type": "formula"}
290
-
291
- if ui_obj["bomb_button"] not in button_updates: # Avoid overwriting the primary clicked button's update
292
- button_updates[ui_obj["bomb_button"]] = gr.update(value=BOMB_ICON if not is_current_plot_active_bomb else ACTIVE_ICON)
293
- if ui_obj["formula_button"] not in button_updates:
294
- button_updates[ui_obj["formula_button"]] = gr.update(value=FORMULA_ICON if not is_current_plot_active_formula else ACTIVE_ICON)
 
 
 
 
 
 
295
 
296
- # Construct the list of updates for Gradio
297
- # Order: global_actions_column_ui, global_actions_markdown_ui, active_panel_action_state, then all button updates
298
  final_updates = [
299
- gr.update(visible=action_col_visible),
300
- gr.update(value=content_text),
301
- new_active_action_state
302
- ]
303
- # Add button updates in a consistent order (e.g., order of plot_configs)
304
- for cfg in plot_configs:
305
- p_id = cfg["id"]
306
- if p_id in plot_ui_objects: # Check if plot_id exists in UI objects
307
- if plot_ui_objects[p_id]["bomb_button"] in button_updates:
308
- final_updates.append(button_updates[plot_ui_objects[p_id]["bomb_button"]])
309
- else:
310
- final_updates.append(gr.update(value=BOMB_ICON))
311
-
312
- if plot_ui_objects[p_id]["formula_button"] in button_updates:
313
- final_updates.append(button_updates[plot_ui_objects[p_id]["formula_button"]])
314
- else:
315
- final_updates.append(gr.update(value=FORMULA_ICON))
316
- else: # Should not happen if plot_ui_objects is complete
317
- final_updates.extend([gr.update(), gr.update()]) # Add empty updates to maintain length
318
 
319
  return final_updates
320
 
321
  # --- Event Handler for Explore Button ---
322
- def handle_explore_click(plot_id_clicked, current_explored_plot_id):
323
- logging.info(f"Explore clicked for: {plot_id_clicked}. Currently explored: {current_explored_plot_id}")
324
-
325
- panel_visibility_updates = {}
326
- button_icon_updates = {}
327
- new_explored_id = None
328
 
329
- is_toggling_off = (plot_id_clicked == current_explored_plot_id)
 
 
330
 
331
  if is_toggling_off:
332
- new_explored_id = None
333
- button_icon_updates[plot_ui_objects[plot_id_clicked]["explore_button"]] = gr.update(value=EXPLORE_ICON)
334
  logging.info(f"Un-exploring plot: {plot_id_clicked}")
335
  else:
336
- new_explored_id = plot_id_clicked
337
- button_icon_updates[plot_ui_objects[plot_id_clicked]["explore_button"]] = gr.update(value=ACTIVE_ICON)
338
- if current_explored_plot_id and current_explored_plot_id != plot_id_clicked:
339
- button_icon_updates[plot_ui_objects[current_explored_plot_id]["explore_button"]] = gr.update(value=EXPLORE_ICON)
340
  logging.info(f"Exploring plot: {plot_id_clicked}")
341
 
342
- for i in range(0, len(plot_configs), 2):
343
- config1_id = plot_configs[i]["id"]
344
- if config1_id not in plot_ui_objects: continue # Skip if panel doesn't exist
345
- panel1 = plot_ui_objects[config1_id]["panel_component"]
346
-
347
- show_panel1 = not new_explored_id or (config1_id == new_explored_id)
348
- panel_visibility_updates[panel1] = gr.update(visible=show_panel1)
349
-
350
- if i + 1 < len(plot_configs):
351
- config2_id = plot_configs[i+1]["id"]
352
- if config2_id not in plot_ui_objects: continue # Skip if panel doesn't exist
353
-
354
- if plot_configs[i+1]["section"] == plot_configs[i]["section"]:
355
- panel2 = plot_ui_objects[config2_id]["panel_component"]
356
- show_panel2 = not new_explored_id or (config2_id == new_explored_id)
357
-
358
- if new_explored_id: # If a plot is being explored
359
- if config1_id == new_explored_id: show_panel2 = False # Hide sibling if first is explored
360
- elif config2_id == new_explored_id: show_panel1 = False # Hide sibling if second is explored
361
-
362
- panel_visibility_updates[panel1] = gr.update(visible=show_panel1) # Re-update panel1 if changed
363
- panel_visibility_updates[panel2] = gr.update(visible=show_panel2)
364
-
365
- final_updates = [new_explored_id]
366
  for cfg in plot_configs:
367
  p_id = cfg["id"]
368
  if p_id in plot_ui_objects:
369
- panel = plot_ui_objects[p_id]["panel_component"]
370
- final_updates.append(panel_visibility_updates.get(panel, gr.update(visible=not new_explored_id or (p_id == new_explored_id) )))
 
 
 
 
 
 
371
 
372
- explore_btn = plot_ui_objects[p_id]["explore_button"]
373
- final_updates.append(button_icon_updates.get(explore_btn, gr.update(value=EXPLORE_ICON if p_id != new_explored_id else ACTIVE_ICON)))
 
 
 
374
  else: # Should not happen
375
- final_updates.extend([gr.update(), gr.update()])
376
 
 
377
  return final_updates
378
 
379
- # --- Connect Action Buttons ---
380
- action_buttons_outputs = [
381
- global_actions_column_ui,
382
- global_actions_markdown_ui,
383
- active_panel_action_state
 
 
 
 
384
  ]
385
- for cfg in plot_configs:
386
- pid = cfg["id"]
387
- if pid in plot_ui_objects: # Important check
388
- action_buttons_outputs.append(plot_ui_objects[pid]["bomb_button"])
389
- action_buttons_outputs.append(plot_ui_objects[pid]["formula_button"])
390
- else: # Maintain list length if a plot_ui_object is unexpectedly missing
391
- action_buttons_outputs.extend([None, None]) # This could cause issues if None is not handled by Gradio outputs
392
-
393
- explore_buttons_outputs = [explored_plot_id_state]
394
- for cfg in plot_configs:
395
- pid = cfg["id"]
396
- if pid in plot_ui_objects: # Important check
397
- explore_buttons_outputs.append(plot_ui_objects[pid]["panel_component"])
398
- explore_buttons_outputs.append(plot_ui_objects[pid]["explore_button"])
399
- else: # Maintain list length
400
- explore_buttons_outputs.extend([None, None])
 
401
 
402
 
 
 
 
 
 
403
  for config_item in plot_configs:
404
  plot_id = config_item["id"]
405
- if plot_id in plot_ui_objects: # Ensure component exists before trying to attach event
406
  ui_obj = plot_ui_objects[plot_id]
407
-
408
- current_action_outputs = [
409
- global_actions_column_ui,
410
- global_actions_markdown_ui,
411
- active_panel_action_state
412
- ] + [plot_ui_objects[c["id"]]["bomb_button"] for c in plot_configs if c["id"] in plot_ui_objects] \
413
- + [plot_ui_objects[c["id"]]["formula_button"] for c in plot_configs if c["id"] in plot_ui_objects]
414
-
415
- current_explore_outputs = [explored_plot_id_state] \
416
- + [plot_ui_objects[c["id"]]["panel_component"] for c in plot_configs if c["id"] in plot_ui_objects] \
417
- + [plot_ui_objects[c["id"]]["explore_button"] for c in plot_configs if c["id"] in plot_ui_objects]
418
-
419
 
420
  ui_obj["bomb_button"].click(
421
- fn=lambda p_id=plot_id: handle_panel_action(p_id, "insights", active_panel_action_state.value, token_state.value),
422
- inputs=None,
423
- outputs=current_action_outputs, # Use dynamically scoped list
424
  api_name=f"action_insights_{plot_id}"
425
  )
426
  ui_obj["formula_button"].click(
427
- fn=lambda p_id=plot_id: handle_panel_action(p_id, "formula", active_panel_action_state.value, token_state.value),
428
- inputs=None,
429
- outputs=current_action_outputs, # Use dynamically scoped list
430
  api_name=f"action_formula_{plot_id}"
431
  )
432
  ui_obj["explore_button"].click(
433
- fn=lambda p_id=plot_id: handle_explore_click(p_id, explored_plot_id_state.value),
434
- inputs=None,
435
- outputs=current_explore_outputs, # Use dynamically scoped list
436
  api_name=f"action_explore_{plot_id}"
437
  )
 
 
 
438
 
439
  # --- Function to Refresh All Analytics UI ---
440
  def refresh_all_analytics_ui_elements(current_token_state, date_filter_val, custom_start_val, custom_end_val):
@@ -442,88 +436,90 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
442
  plot_generation_results = update_analytics_plots_figures(
443
  current_token_state, date_filter_val, custom_start_val, custom_end_val
444
  )
445
-
446
- status_message_update = plot_generation_results[0]
447
- generated_plot_figures = plot_generation_results[1:]
448
 
449
- all_updates = [status_message_update]
 
450
 
451
- for i, config in enumerate(plot_configs):
452
- p_id_key = config["id"]
453
- if p_id_key in plot_ui_objects:
454
- if i < len(generated_plot_figures):
455
- all_updates.append(generated_plot_figures[i])
456
- else:
457
- all_updates.append(create_placeholder_plot("Figure Error", f"No figure for {p_id_key}"))
458
- else:
459
- all_updates.append(None)
460
 
461
- all_updates.append(gr.update(visible=False))
462
- all_updates.append(gr.update(value="Click a button (💣, ƒ) on a plot..."))
463
- all_updates.append(None)
 
 
 
464
 
 
 
 
 
 
 
465
  for cfg in plot_configs:
466
  pid = cfg["id"]
467
  if pid in plot_ui_objects:
468
- all_updates.append(gr.update(value=BOMB_ICON))
469
- all_updates.append(gr.update(value=FORMULA_ICON))
470
- all_updates.append(gr.update(value=EXPLORE_ICON))
471
- all_updates.append(gr.update(visible=True))
472
- else:
473
- all_updates.extend([None,None,None,None])
474
-
475
-
476
- all_updates.append(None)
477
 
 
 
478
  logging.info(f"Prepared {len(all_updates)} updates for analytics refresh.")
479
  return all_updates
480
 
481
  # --- Define outputs for the apply_filter_btn and sync.then() ---
482
- apply_filter_and_sync_outputs = [analytics_status_md]
483
- for config in plot_configs:
484
- pid = config["id"]
485
- if pid in plot_ui_objects and "plot_component" in plot_ui_objects[pid]:
486
- apply_filter_and_sync_outputs.append(plot_ui_objects[pid]["plot_component"])
487
- else:
488
- apply_filter_and_sync_outputs.append(None) # Must add placeholder if component might be missing
489
 
490
- apply_filter_and_sync_outputs.extend([
491
- global_actions_column_ui,
492
- global_actions_markdown_ui,
493
- active_panel_action_state
 
 
 
 
 
 
 
 
 
494
  ])
495
 
496
- for cfg in plot_configs:
497
- pid = cfg["id"]
498
- if pid in plot_ui_objects:
499
- apply_filter_and_sync_outputs.append(plot_ui_objects[pid]["bomb_button"])
500
- apply_filter_and_sync_outputs.append(plot_ui_objects[pid]["formula_button"])
501
- apply_filter_and_sync_outputs.append(plot_ui_objects[pid]["explore_button"])
502
- apply_filter_and_sync_outputs.append(plot_ui_objects[pid]["panel_component"])
503
- else:
504
- # This case indicates a mismatch and will likely cause errors if not handled.
505
- # Forcing list length for now, but root cause would be plot_ui_objects incompleteness.
506
- apply_filter_and_sync_outputs.extend([None, None, None, None])
507
-
508
-
509
- apply_filter_and_sync_outputs.append(explored_plot_id_state)
510
 
511
- logging.info(f"Total outputs for apply_filter/sync: {len(apply_filter_and_sync_outputs)}")
 
512
 
513
  apply_filter_btn.click(
514
  fn=refresh_all_analytics_ui_elements,
515
  inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker],
516
- outputs=apply_filter_and_sync_outputs,
517
  show_progress="full"
518
  )
519
 
520
  with gr.TabItem("3️⃣ Mentions", id="tab_mentions"):
521
  refresh_mentions_display_btn = gr.Button("🔄 Refresh Mentions Display", variant="secondary")
522
  mentions_html = gr.HTML("Mentions data...")
523
- mentions_sentiment_dist_plot = gr.Plot(label="Mention Sentiment Distribution")
524
  refresh_mentions_display_btn.click(
525
  fn=run_mentions_tab_display, inputs=[token_state],
526
- outputs=[mentions_html, mentions_sentiment_dist_plot],
527
  show_progress="full"
528
  )
529
 
@@ -541,24 +537,24 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
541
  outputs=[follower_stats_html, fs_plot_monthly_gains, fs_plot_seniority, fs_plot_industry],
542
  show_progress="full"
543
  )
544
-
545
  sync_event_part1 = sync_data_btn.click(
546
  fn=sync_all_linkedin_data_orchestrator,
547
  inputs=[token_state], outputs=[sync_status_html_output, token_state], show_progress="full"
548
  )
549
  sync_event_part2 = sync_event_part1.then(
550
- fn=process_and_store_bubble_token,
551
- inputs=[url_user_token_display, org_urn_display, token_state],
552
- outputs=[status_box, token_state, sync_data_btn], show_progress=False
553
  )
554
  sync_event_part3 = sync_event_part2.then(
555
- fn=display_main_dashboard,
556
  inputs=[token_state], outputs=[dashboard_display_html], show_progress=False
557
  )
558
  sync_event_final = sync_event_part3.then(
559
- fn=refresh_all_analytics_ui_elements,
560
- inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker],
561
- outputs=apply_filter_and_sync_outputs, show_progress="full"
562
  )
563
 
564
  if __name__ == "__main__":
@@ -575,4 +571,3 @@ if __name__ == "__main__":
575
  logging.error("Matplotlib is not installed. Plots will not be generated.")
576
 
577
  app.launch(server_name="0.0.0.0", server_port=7860, debug=True)
578
-
 
5
  import matplotlib
6
  matplotlib.use('Agg') # Set backend for Matplotlib to avoid GUI conflicts with Gradio
7
  import matplotlib.pyplot as plt
8
+ import time # For profiling if needed
9
 
10
  # --- Module Imports ---
11
  from gradio_utils import get_url_user_token
 
35
  generate_engagement_rate_over_time_plot,
36
  generate_reach_over_time_plot,
37
  generate_impressions_over_time_plot,
38
+ create_placeholder_plot,
39
  generate_likes_over_time_plot,
40
  generate_clicks_over_time_plot,
41
  generate_shares_over_time_plot,
 
52
  # --- Analytics Tab: Plot Figure Generation Function ---
53
  def update_analytics_plots_figures(token_state_value, date_filter_option, custom_start_date, custom_end_date):
54
  logging.info(f"Updating analytics plot figures. Filter: {date_filter_option}, Custom Start: {custom_start_date}, Custom End: {custom_end_date}")
55
+ num_expected_plots = 23 # This should match len(plot_configs)
56
 
57
  if not token_state_value or not token_state_value.get("token"):
58
  message = "❌ Access denied. No token. Cannot generate analytics."
 
61
  return [message] + placeholder_figs
62
 
63
  try:
64
+ # Optional: Start profiling data preparation
65
+ # data_prep_start_time = time.time()
66
  (filtered_merged_posts_df,
67
  filtered_mentions_df,
68
  date_filtered_follower_stats_df,
 
71
  prepare_filtered_analytics_data(
72
  token_state_value, date_filter_option, custom_start_date, custom_end_date
73
  )
74
+ # logging.info(f"Data preparation took {time.time() - data_prep_start_time:.2f}s")
75
  except Exception as e:
76
  error_msg = f"❌ Error preparing analytics data: {e}"
77
  logging.error(error_msg, exc_info=True)
 
84
  eb_labels_col_name = token_state_value.get("config_eb_labels_col", "eb_labels")
85
 
86
  plot_figs = []
87
+ # Optional: For detailed profiling of each plot
88
+ # individual_plot_times = {}
89
+ # total_plot_gen_start_time = time.time()
90
  try:
91
+ # Example for profiling one plot:
92
+ # plot_name = "posts_activity"
93
+ # plot_start_time = time.time()
94
  plot_figs.append(generate_posts_activity_plot(filtered_merged_posts_df, date_column=date_column_posts))
95
+ # individual_plot_times[plot_name] = time.time() - plot_start_time
96
+ # logging.info(f"Generated {plot_name} in {individual_plot_times[plot_name]:.2f}s")
97
+
98
  plot_figs.append(generate_engagement_type_plot(filtered_merged_posts_df))
99
  fig_mentions_activity_shared = generate_mentions_activity_plot(filtered_mentions_df, date_column=date_column_mentions)
100
  fig_mention_sentiment_shared = generate_mention_sentiment_plot(filtered_mentions_df)
 
107
  plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_industry', plot_title="Followers by Industry"))
108
  plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_seniority', plot_title="Followers by Seniority"))
109
  plot_figs.append(generate_engagement_rate_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
110
+ plot_figs.append(generate_reach_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts)) # Assuming this is intended, though label says "Clicks"
111
  plot_figs.append(generate_impressions_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
112
  plot_figs.append(generate_likes_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
113
  plot_figs.append(generate_clicks_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
 
117
  plot_figs.append(generate_post_frequency_plot(filtered_merged_posts_df, date_column=date_column_posts))
118
  plot_figs.append(generate_content_format_breakdown_plot(filtered_merged_posts_df, format_col=media_type_col_name))
119
  plot_figs.append(generate_content_topic_breakdown_plot(filtered_merged_posts_df, topics_col=eb_labels_col_name))
120
+ plot_figs.append(fig_mentions_activity_shared) # Re-adding shared plot for "Mention Analysis (Detailed)"
121
+ plot_figs.append(fig_mention_sentiment_shared) # Re-adding shared plot for "Mention Analysis (Detailed)"
122
+
123
+ # logging.info(f"All plots generated in {time.time() - total_plot_gen_start_time:.2f}s")
124
+ # logging.info(f"Individual plot generation times: {individual_plot_times}")
125
 
126
  message = f"📊 Analytics updated for period: {date_filter_option}"
127
  if date_filter_option == "Custom Range":
128
  s_display = start_dt_for_msg.strftime('%Y-%m-%d') if start_dt_for_msg else "Any"
129
  e_display = end_dt_for_msg.strftime('%Y-%m-%d') if end_dt_for_msg else "Any"
130
  message += f" (From: {s_display} To: {e_display})"
131
+
132
  final_plot_figs = []
133
  for i, p_fig in enumerate(plot_figs):
134
+ if p_fig is not None and not isinstance(p_fig, str): # Check if it's a Matplotlib figure
135
  final_plot_figs.append(p_fig)
136
  else:
137
  logging.warning(f"Plot figure generation failed or returned unexpected type for slot {i}, using placeholder. Figure: {p_fig}")
138
  final_plot_figs.append(create_placeholder_plot(title="Plot Error", message="Failed to generate this plot figure."))
139
+
140
  while len(final_plot_figs) < num_expected_plots:
141
  logging.warning(f"Padding missing plot figure. Expected {num_expected_plots}, got {len(final_plot_figs)}.")
142
  final_plot_figs.append(create_placeholder_plot(title="Missing Plot", message="Plot figure could not be generated."))
143
+
144
  return [message] + final_plot_figs[:num_expected_plots]
145
 
146
  except Exception as e:
 
189
  outputs=[status_box, token_state, sync_data_btn, dashboard_display_html],
190
  show_progress="full"
191
  )
192
+
193
  with gr.TabItem("2️⃣ Analytics", id="tab_analytics"):
194
  gr.Markdown("## 📈 LinkedIn Performance Analytics")
195
  gr.Markdown("Select a date range. Click buttons for actions.")
196
+
197
  analytics_status_md = gr.Markdown("Analytics status...")
198
 
199
+ with gr.Row():
200
  date_filter_selector = gr.Radio(
201
  ["All Time", "Last 7 Days", "Last 30 Days", "Custom Range"],
202
  label="Select Date Range", value="Last 30 Days", scale=3
203
  )
204
  with gr.Column(scale=2):
205
+ custom_start_date_picker = gr.DateTime(label="Start Date", visible=False, include_time=False, type="datetime") # type="date" might be better if time not used
206
+ custom_end_date_picker = gr.DateTime(label="End Date", visible=False, include_time=False, type="datetime") # type="date"
207
+
208
  apply_filter_btn = gr.Button("🔍 Apply Filter & Refresh Analytics", variant="primary")
209
 
210
  def toggle_custom_date_pickers(selection):
 
220
  plot_configs = [
221
  {"label": "Posts Activity Over Time", "id": "posts_activity", "section": "Posts & Engagement Overview"},
222
  {"label": "Post Engagement Types", "id": "engagement_type", "section": "Posts & Engagement Overview"},
223
+ {"label": "Mentions Activity Over Time", "id": "mentions_activity", "section": "Mentions Overview"},
224
+ {"label": "Mention Sentiment Distribution", "id": "mention_sentiment", "section": "Mentions Overview"},
225
  {"label": "Followers Count Over Time", "id": "followers_count", "section": "Follower Dynamics"},
226
  {"label": "Followers Growth Rate", "id": "followers_growth_rate", "section": "Follower Dynamics"},
227
  {"label": "Followers by Location", "id": "followers_by_location", "section": "Follower Demographics"},
 
229
  {"label": "Followers by Industry", "id": "followers_by_industry", "section": "Follower Demographics"},
230
  {"label": "Followers by Seniority", "id": "followers_by_seniority", "section": "Follower Demographics"},
231
  {"label": "Engagement Rate Over Time", "id": "engagement_rate", "section": "Post Performance Insights"},
232
+ {"label": "Reach Over Time", "id": "reach_over_time", "section": "Post Performance Insights"}, # Corrected label based on function
233
  {"label": "Impressions Over Time", "id": "impressions_over_time", "section": "Post Performance Insights"},
234
  {"label": "Reactions (Likes) Over Time", "id": "likes_over_time", "section": "Post Performance Insights"},
235
  {"label": "Clicks Over Time", "id": "clicks_over_time", "section": "Detailed Post Engagement Over Time"},
 
239
  {"label": "Post Frequency", "id": "post_frequency_cs", "section": "Content Strategy Analysis"},
240
  {"label": "Breakdown of Content by Format", "id": "content_format_breakdown_cs", "section": "Content Strategy Analysis"},
241
  {"label": "Breakdown of Content by Topics", "id": "content_topic_breakdown_cs", "section": "Content Strategy Analysis"},
242
+ {"label": "Mentions Volume Over Time (Detailed)", "id": "mention_analysis_volume", "section": "Mention Analysis (Detailed)"},
243
+ {"label": "Breakdown of Mentions by Sentiment (Detailed)", "id": "mention_analysis_sentiment", "section": "Mention Analysis (Detailed)"}
244
  ]
245
  assert len(plot_configs) == 23, "Mismatch in plot_configs and expected plots."
246
 
247
+ active_panel_action_state = gr.State(None) # Stores {"plot_id": "action_type"} e.g. {"posts_activity": "insights"} or None
248
+ explored_plot_id_state = gr.State(None) # Stores plot_id of the currently explored plot, or None
249
+
250
+ plot_ui_objects = {} # Will be populated by build_analytics_tab_plot_area
 
251
 
252
+ with gr.Row(equal_height=False):
253
+ with gr.Column(scale=8) as plots_area_col:
254
+ # Call the UI builder function and store its results
255
  plot_ui_objects = build_analytics_tab_plot_area(plot_configs)
256
+
257
+ with gr.Column(scale=4, visible=False) as global_actions_column_ui:
258
+ gr.Markdown("### 💡 Generated Content")
259
  global_actions_markdown_ui = gr.Markdown("Click a button (💣, ƒ) on a plot to see content here.")
260
 
261
 
262
  # --- Event Handler for Insights and Formula Buttons ---
263
+ def handle_panel_action(plot_id_clicked, action_type, current_active_action_from_state, current_token_state_val):
264
+ logging.info(f"Action '{action_type}' for plot: {plot_id_clicked}. Current active from state: {current_active_action_from_state}")
265
+
266
+ # This check is crucial: plot_ui_objects must be populated before this handler is effectively used.
267
+ # It should be, as this handler is tied to buttons created by build_analytics_tab_plot_area.
268
+ if not plot_ui_objects or plot_id_clicked not in plot_ui_objects:
269
+ logging.error(f"plot_ui_objects not populated or plot_id {plot_id_clicked} not found during handle_panel_action.")
270
+ # Return updates that don't crash, perhaps just an error message or no-ops.
271
+ # Number of outputs for this handler: 3 (global_col_vis, global_md_val, active_state_val) + 2 * len(plot_configs) (buttons)
272
+ error_updates = [gr.update(visible=False), "Error: UI components not ready.", None] + [gr.update() for _ in range(2 * len(plot_configs))]
273
+ return error_updates
274
+
275
+
276
  clicked_plot_label = plot_ui_objects.get(plot_id_clicked, {}).get("label", "Selected Plot")
277
 
278
+ hypothetical_new_active_state = {"plot_id": plot_id_clicked, "type": action_type}
279
+ is_toggling_off = current_active_action_from_state == hypothetical_new_active_state
 
280
 
281
+ new_active_action_state_to_set = None
282
  content_text = ""
283
  action_col_visible = False
 
 
 
284
 
285
  if is_toggling_off:
286
+ new_active_action_state_to_set = None # Closing the panel
287
+ content_text = f"{action_type.capitalize()} panel for '{clicked_plot_label}' closed."
288
  action_col_visible = False
289
  logging.info(f"Closing {action_type} panel for {plot_id_clicked}")
 
 
 
 
 
290
  else: # Activating or switching
291
+ new_active_action_state_to_set = hypothetical_new_active_state
292
  action_col_visible = True
293
  if action_type == "insights":
294
+ # TODO: Implement actual insight generation using current_token_state_val if needed
295
+ content_text = f"**Insights for: {clicked_plot_label}**\n\nPlot ID: `{plot_id_clicked}`.\n(AI insights generation placeholder)"
 
296
  elif action_type == "formula":
297
  # TODO: Implement actual formula display
298
+ content_text = f"**Formula/Methodology for: {clicked_plot_label}**\n\nPlot ID: `{plot_id_clicked}`.\n(Methodology details placeholder)"
299
+ logging.info(f"Opening/switching to {action_type} panel for {plot_id_clicked}")
300
+
301
+ # Prepare updates for ALL buttons based on new_active_action_state_to_set
302
+ all_button_updates = []
303
+ for cfg_item in plot_configs:
304
+ p_id_iter = cfg_item["id"]
305
+ if p_id_iter in plot_ui_objects:
306
+ # Bomb button state for this p_id_iter
307
+ if new_active_action_state_to_set == {"plot_id": p_id_iter, "type": "insights"}:
308
+ all_button_updates.append(gr.update(value=ACTIVE_ICON))
309
+ else:
310
+ all_button_updates.append(gr.update(value=BOMB_ICON))
311
+
312
+ # Formula button state for this p_id_iter
313
+ if new_active_action_state_to_set == {"plot_id": p_id_iter, "type": "formula"}:
314
+ all_button_updates.append(gr.update(value=ACTIVE_ICON))
315
+ else:
316
+ all_button_updates.append(gr.update(value=FORMULA_ICON))
317
+ else: # Should not happen if plot_ui_objects is complete
318
+ all_button_updates.extend([gr.update(), gr.update()])
319
 
 
 
320
  final_updates = [
321
+ gr.update(visible=action_col_visible),
322
+ gr.update(value=content_text),
323
+ new_active_action_state_to_set # This is the new value for active_panel_action_state
324
+ ] + all_button_updates
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
  return final_updates
327
 
328
  # --- Event Handler for Explore Button ---
329
+ def handle_explore_click(plot_id_clicked, current_explored_plot_id_from_state):
330
+ logging.info(f"Explore clicked for: {plot_id_clicked}. Currently explored from state: {current_explored_plot_id_from_state}")
331
+
332
+ if not plot_ui_objects: # Safeguard
333
+ logging.error("plot_ui_objects not populated during handle_explore_click.")
334
+ return [current_explored_plot_id_from_state] + [gr.update() for _ in range(2 * len(plot_configs))]
335
 
336
+
337
+ new_explored_id_to_set = None
338
+ is_toggling_off = (plot_id_clicked == current_explored_plot_id_from_state)
339
 
340
  if is_toggling_off:
341
+ new_explored_id_to_set = None # Un-exploring
 
342
  logging.info(f"Un-exploring plot: {plot_id_clicked}")
343
  else:
344
+ new_explored_id_to_set = plot_id_clicked # Exploring this plot
 
 
 
345
  logging.info(f"Exploring plot: {plot_id_clicked}")
346
 
347
+ # Prepare updates for panel visibility and explore button icons
348
+ panel_and_button_updates = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  for cfg in plot_configs:
350
  p_id = cfg["id"]
351
  if p_id in plot_ui_objects:
352
+ panel_visible = not new_explored_id_to_set or (p_id == new_explored_id_to_set)
353
+
354
+ # Special handling for paired plots in the same section when one is explored
355
+ # This logic might need refinement based on how build_analytics_tab_plot_area structures rows
356
+ # For now, the basic logic is: if new_explored_id_to_set is active, only that panel is visible.
357
+ # If new_explored_id_to_set is None, all panels are visible.
358
+
359
+ panel_and_button_updates.append(gr.update(visible=panel_visible)) # Panel visibility update
360
 
361
+ # Explore button icon update
362
+ if p_id == new_explored_id_to_set: # If this plot is the one being explored (or just became explored)
363
+ panel_and_button_updates.append(gr.update(value=ACTIVE_ICON))
364
+ else: # All other plots, or if un-exploring
365
+ panel_and_button_updates.append(gr.update(value=EXPLORE_ICON))
366
  else: # Should not happen
367
+ panel_and_button_updates.extend([gr.update(), gr.update()])
368
 
369
+ final_updates = [new_explored_id_to_set] + panel_and_button_updates
370
  return final_updates
371
 
372
+ # --- Define outputs for event handlers ---
373
+ # These lists define which Gradio components will receive updates from the handlers.
374
+ # The order and number of components must match the list of updates returned by the handler.
375
+
376
+ # Outputs for handle_panel_action (Insights/Formula buttons)
377
+ action_buttons_outputs_list = [
378
+ global_actions_column_ui,
379
+ global_actions_markdown_ui,
380
+ active_panel_action_state # The gr.State object itself
381
  ]
382
+ for cfg_item_action in plot_configs: # Iterate in defined order
383
+ pid_action = cfg_item_action["id"]
384
+ if pid_action in plot_ui_objects:
385
+ action_buttons_outputs_list.append(plot_ui_objects[pid_action]["bomb_button"])
386
+ action_buttons_outputs_list.append(plot_ui_objects[pid_action]["formula_button"])
387
+ else: # Should not happen if plot_ui_objects is correctly populated
388
+ action_buttons_outputs_list.extend([None, None]) # Add placeholders to maintain length
389
+
390
+ # Outputs for handle_explore_click
391
+ explore_buttons_outputs_list = [explored_plot_id_state] # The gr.State object
392
+ for cfg_item_explore in plot_configs: # Iterate in defined order
393
+ pid_explore = cfg_item_explore["id"]
394
+ if pid_explore in plot_ui_objects:
395
+ explore_buttons_outputs_list.append(plot_ui_objects[pid_explore]["panel_component"]) # For visibility
396
+ explore_buttons_outputs_list.append(plot_ui_objects[pid_explore]["explore_button"]) # For icon
397
+ else: # Should not happen
398
+ explore_buttons_outputs_list.extend([None, None])
399
 
400
 
401
+ # --- Connect Action Buttons ---
402
+ # Inputs for the click handlers that need current state values
403
+ action_click_inputs = [active_panel_action_state, token_state]
404
+ explore_click_inputs = [explored_plot_id_state] # Only needs explored_plot_id_state
405
+
406
  for config_item in plot_configs:
407
  plot_id = config_item["id"]
408
+ if plot_id in plot_ui_objects: # Ensure component exists
409
  ui_obj = plot_ui_objects[plot_id]
 
 
 
 
 
 
 
 
 
 
 
 
410
 
411
  ui_obj["bomb_button"].click(
412
+ fn=lambda current_active_val, current_token_val, p_id=plot_id: handle_panel_action(p_id, "insights", current_active_val, current_token_val),
413
+ inputs=action_click_inputs,
414
+ outputs=action_buttons_outputs_list,
415
  api_name=f"action_insights_{plot_id}"
416
  )
417
  ui_obj["formula_button"].click(
418
+ fn=lambda current_active_val, current_token_val, p_id=plot_id: handle_panel_action(p_id, "formula", current_active_val, current_token_val),
419
+ inputs=action_click_inputs,
420
+ outputs=action_buttons_outputs_list,
421
  api_name=f"action_formula_{plot_id}"
422
  )
423
  ui_obj["explore_button"].click(
424
+ fn=lambda current_explored_val, p_id=plot_id: handle_explore_click(p_id, current_explored_val),
425
+ inputs=explore_click_inputs,
426
+ outputs=explore_buttons_outputs_list,
427
  api_name=f"action_explore_{plot_id}"
428
  )
429
+ else:
430
+ logging.warning(f"UI object for plot_id '{plot_id}' not found when trying to attach click handlers. This might lead to issues.")
431
+
432
 
433
  # --- Function to Refresh All Analytics UI ---
434
  def refresh_all_analytics_ui_elements(current_token_state, date_filter_val, custom_start_val, custom_end_val):
 
436
  plot_generation_results = update_analytics_plots_figures(
437
  current_token_state, date_filter_val, custom_start_val, custom_end_val
438
  )
 
 
 
439
 
440
+ status_message_update = plot_generation_results[0]
441
+ generated_plot_figures = plot_generation_results[1:] # Should be list of 23 figures or placeholders
442
 
443
+ all_updates = [status_message_update] # For analytics_status_md
 
 
 
 
 
 
 
 
444
 
445
+ # Updates for plot components
446
+ for i in range(len(plot_configs)):
447
+ if i < len(generated_plot_figures):
448
+ all_updates.append(generated_plot_figures[i])
449
+ else: # Should not happen if update_analytics_plots_figures pads correctly
450
+ all_updates.append(create_placeholder_plot("Figure Error", f"Missing figure for plot {i}"))
451
 
452
+ # Updates for global action column and its content, and active_panel_action_state
453
+ all_updates.append(gr.update(visible=False)) # global_actions_column_ui
454
+ all_updates.append(gr.update(value="Click a button (💣, ƒ) on a plot...")) # global_actions_markdown_ui
455
+ all_updates.append(None) # Reset active_panel_action_state
456
+
457
+ # Updates for all action buttons (bomb, formula), explore buttons, and panel visibility
458
  for cfg in plot_configs:
459
  pid = cfg["id"]
460
  if pid in plot_ui_objects:
461
+ all_updates.append(gr.update(value=BOMB_ICON)) # Reset bomb_button
462
+ all_updates.append(gr.update(value=FORMULA_ICON)) # Reset formula_button
463
+ all_updates.append(gr.update(value=EXPLORE_ICON)) # Reset explore_button
464
+ all_updates.append(gr.update(visible=True)) # Reset panel_component visibility (all visible initially)
465
+ else: # Should not happen
466
+ all_updates.extend([None, None, None, None])
 
 
 
467
 
468
+ all_updates.append(None) # Reset explored_plot_id_state
469
+
470
  logging.info(f"Prepared {len(all_updates)} updates for analytics refresh.")
471
  return all_updates
472
 
473
  # --- Define outputs for the apply_filter_btn and sync.then() ---
474
+ # This list must exactly match the structure of updates returned by refresh_all_analytics_ui_elements
475
+ apply_filter_and_sync_outputs_list = [analytics_status_md] # 1. Status MD
 
 
 
 
 
476
 
477
+ # 2. Plot components (23 of them)
478
+ for config_item_filter_sync in plot_configs:
479
+ pid_filter_sync = config_item_filter_sync["id"]
480
+ if pid_filter_sync in plot_ui_objects and "plot_component" in plot_ui_objects[pid_filter_sync]:
481
+ apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync]["plot_component"])
482
+ else: # Should not happen
483
+ apply_filter_and_sync_outputs_list.append(None)
484
+
485
+ # 3. Global action column, its markdown, and its state (3 items)
486
+ apply_filter_and_sync_outputs_list.extend([
487
+ global_actions_column_ui,
488
+ global_actions_markdown_ui,
489
+ active_panel_action_state # The State object itself
490
  ])
491
 
492
+ # 4. Action buttons (bomb, formula), explore buttons, and panel visibility for each plot (23 * 4 = 92 items)
493
+ for cfg_filter_sync_btns in plot_configs:
494
+ pid_filter_sync_btns = cfg_filter_sync_btns["id"]
495
+ if pid_filter_sync_btns in plot_ui_objects:
496
+ apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync_btns]["bomb_button"])
497
+ apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync_btns]["formula_button"])
498
+ apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync_btns]["explore_button"])
499
+ apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync_btns]["panel_component"]) # For panel visibility
500
+ else: # Should not happen
501
+ apply_filter_and_sync_outputs_list.extend([None, None, None, None])
502
+
503
+ # 5. Explored plot ID state (1 item)
504
+ apply_filter_and_sync_outputs_list.append(explored_plot_id_state) # The State object itself
 
505
 
506
+ logging.info(f"Total outputs for apply_filter/sync: {len(apply_filter_and_sync_outputs_list)}")
507
+
508
 
509
  apply_filter_btn.click(
510
  fn=refresh_all_analytics_ui_elements,
511
  inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker],
512
+ outputs=apply_filter_and_sync_outputs_list, # Use the carefully constructed list
513
  show_progress="full"
514
  )
515
 
516
  with gr.TabItem("3️⃣ Mentions", id="tab_mentions"):
517
  refresh_mentions_display_btn = gr.Button("🔄 Refresh Mentions Display", variant="secondary")
518
  mentions_html = gr.HTML("Mentions data...")
519
+ mentions_sentiment_dist_plot = gr.Plot(label="Mention Sentiment Distribution") # This is a gr.Plot component
520
  refresh_mentions_display_btn.click(
521
  fn=run_mentions_tab_display, inputs=[token_state],
522
+ outputs=[mentions_html, mentions_sentiment_dist_plot], # run_mentions_tab_display returns (html_str, fig_object)
523
  show_progress="full"
524
  )
525
 
 
537
  outputs=[follower_stats_html, fs_plot_monthly_gains, fs_plot_seniority, fs_plot_industry],
538
  show_progress="full"
539
  )
540
+
541
  sync_event_part1 = sync_data_btn.click(
542
  fn=sync_all_linkedin_data_orchestrator,
543
  inputs=[token_state], outputs=[sync_status_html_output, token_state], show_progress="full"
544
  )
545
  sync_event_part2 = sync_event_part1.then(
546
+ fn=process_and_store_bubble_token, # This fn returns (status_msg, new_state, btn_update)
547
+ inputs=[url_user_token_display, org_urn_display, token_state],
548
+ outputs=[status_box, token_state, sync_data_btn], show_progress=False # btn_update is for sync_data_btn
549
  )
550
  sync_event_part3 = sync_event_part2.then(
551
+ fn=display_main_dashboard, # This fn returns html_string
552
  inputs=[token_state], outputs=[dashboard_display_html], show_progress=False
553
  )
554
  sync_event_final = sync_event_part3.then(
555
+ fn=refresh_all_analytics_ui_elements, # This fn returns a list of updates
556
+ inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker],
557
+ outputs=apply_filter_and_sync_outputs_list, show_progress="full" # Use the carefully constructed list
558
  )
559
 
560
  if __name__ == "__main__":
 
571
  logging.error("Matplotlib is not installed. Plots will not be generated.")
572
 
573
  app.launch(server_name="0.0.0.0", server_port=7860, debug=True)