GuglielmoTor commited on
Commit
6a8e128
Β·
verified Β·
1 Parent(s): c47a4ee

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +120 -127
app.py CHANGED
@@ -5,7 +5,6 @@ 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
- # from functools import partial # No longer needed if gr.State(value=plot_id) is used
9
 
10
  # --- Module Imports ---
11
  from gradio_utils import get_url_user_token
@@ -21,7 +20,7 @@ from ui_generators import (
21
  display_main_dashboard,
22
  run_mentions_tab_display,
23
  run_follower_stats_tab_display,
24
- build_analytics_tab_ui_components # Import the new UI builder function
25
  )
26
  # Corrected import for analytics_data_processing
27
  from analytics_data_processing import prepare_filtered_analytics_data
@@ -34,7 +33,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, # For initializing plots
38
  generate_likes_over_time_plot,
39
  generate_clicks_over_time_plot,
40
  generate_shares_over_time_plot,
@@ -48,14 +47,10 @@ from analytics_plot_generator import (
48
  # Configure logging
49
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(module)s - %(message)s')
50
 
51
- # --- Analytics Tab: Plot Update Function (Original, generates figures) ---
52
  def update_analytics_plots_figures(token_state_value, date_filter_option, custom_start_date, custom_end_date):
53
- """
54
- Prepares analytics data using external processing function and then generates plot figures.
55
- This function is primarily responsible for returning the Matplotlib figure objects.
56
- """
57
  logging.info(f"Updating analytics plot figures. Filter: {date_filter_option}, Custom Start: {custom_start_date}, Custom End: {custom_end_date}")
58
- num_expected_plots = 23 # This should match the number of plots defined in plot_configs
59
 
60
  if not token_state_value or not token_state_value.get("token"):
61
  message = "❌ Access denied. No token. Cannot generate analytics."
@@ -83,20 +78,14 @@ def update_analytics_plots_figures(token_state_value, date_filter_option, custom
83
  media_type_col_name = token_state_value.get("config_media_type_col", "media_type")
84
  eb_labels_col_name = token_state_value.get("config_eb_labels_col", "eb_labels")
85
 
86
- logging.info(f"Data for plotting - Filtered Merged Posts: {len(filtered_merged_posts_df)} rows, Filtered Mentions: {len(filtered_mentions_df)} rows.")
87
- logging.info(f"Date-Filtered Follower Stats: {len(date_filtered_follower_stats_df)} rows, Raw Follower Stats: {len(raw_follower_stats_df)} rows.")
88
-
89
  try:
90
- plot_figs = []
91
  plot_figs.append(generate_posts_activity_plot(filtered_merged_posts_df, date_column=date_column_posts))
92
  plot_figs.append(generate_engagement_type_plot(filtered_merged_posts_df))
93
-
94
  fig_mentions_activity_shared = generate_mentions_activity_plot(filtered_mentions_df, date_column=date_column_mentions)
95
  fig_mention_sentiment_shared = generate_mention_sentiment_plot(filtered_mentions_df)
96
-
97
- plot_figs.append(fig_mentions_activity_shared) # Original mention plot slot 1
98
- plot_figs.append(fig_mention_sentiment_shared) # Original mention plot slot 2
99
-
100
  plot_figs.append(generate_followers_count_over_time_plot(date_filtered_follower_stats_df, type_value='follower_gains_monthly'))
101
  plot_figs.append(generate_followers_growth_rate_plot(date_filtered_follower_stats_df, type_value='follower_gains_monthly'))
102
  plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_geo', plot_title="Followers by Location"))
@@ -114,10 +103,8 @@ def update_analytics_plots_figures(token_state_value, date_filter_option, custom
114
  plot_figs.append(generate_post_frequency_plot(filtered_merged_posts_df, date_column=date_column_posts))
115
  plot_figs.append(generate_content_format_breakdown_plot(filtered_merged_posts_df, format_col=media_type_col_name))
116
  plot_figs.append(generate_content_topic_breakdown_plot(filtered_merged_posts_df, topics_col=eb_labels_col_name))
117
-
118
- # For the "Mention Analysis" section, we reuse the figures generated earlier
119
- plot_figs.append(fig_mentions_activity_shared) # New UI slot for mention volume, reuses figure
120
- plot_figs.append(fig_mention_sentiment_shared) # New UI slot for mention sentiment, reuses figure
121
 
122
  message = f"πŸ“Š Analytics updated for period: {date_filter_option}"
123
  if date_filter_option == "Custom Range":
@@ -127,17 +114,16 @@ def update_analytics_plots_figures(token_state_value, date_filter_option, custom
127
 
128
  final_plot_figs = []
129
  for i, p_fig in enumerate(plot_figs):
130
- if p_fig is not None and not isinstance(p_fig, str):
131
  final_plot_figs.append(p_fig)
132
  else:
133
  logging.warning(f"Plot figure generation failed or returned unexpected type for slot {i}, using placeholder. Figure: {p_fig}")
134
  final_plot_figs.append(create_placeholder_plot(title="Plot Error", message="Failed to generate this plot figure."))
135
 
136
  while len(final_plot_figs) < num_expected_plots:
137
- logging.warning(f"Padding missing plot figure with placeholder. Expected {num_expected_plots}, got {len(final_plot_figs)}.")
138
  final_plot_figs.append(create_placeholder_plot(title="Missing Plot", message="Plot figure could not be generated."))
139
 
140
- logging.info(f"Successfully generated {len(final_plot_figs)} plot figures for {num_expected_plots} UI slots.")
141
  return [message] + final_plot_figs[:num_expected_plots]
142
 
143
  except Exception as e:
@@ -153,37 +139,31 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
153
 
154
  token_state = gr.State(value={
155
  "token": None, "client_id": None, "org_urn": None,
156
- "bubble_posts_df": pd.DataFrame(),
157
- "bubble_post_stats_df": pd.DataFrame(),
158
- "bubble_mentions_df": pd.DataFrame(),
159
- "bubble_follower_stats_df": pd.DataFrame(),
160
- "fetch_count_for_api": 0,
161
- "url_user_token_temp_storage": None,
162
- "config_date_col_posts": "published_at",
163
- "config_date_col_mentions": "date",
164
- "config_date_col_followers": "date",
165
- "config_media_type_col": "media_type",
166
  "config_eb_labels_col": "eb_labels"
167
  })
168
 
169
  gr.Markdown("# πŸš€ LinkedIn Organization Dashboard")
170
- url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False)
171
  status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...")
172
- org_urn_display = gr.Textbox(label="Organization URN (from URL - Hidden)", interactive=False, visible=False)
173
 
174
  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)
175
 
176
  def initial_load_sequence(url_token, org_urn_val, current_state):
177
- logging.info(f"Initial load sequence triggered. Org URN: {org_urn_val}, URL Token: {'Present' if url_token else 'Absent'}")
178
  status_msg, new_state, btn_update = process_and_store_bubble_token(url_token, org_urn_val, current_state)
179
  dashboard_content = display_main_dashboard(new_state)
180
  return status_msg, new_state, btn_update, dashboard_content
181
 
182
  with gr.Tabs() as tabs:
183
  with gr.TabItem("1️⃣ Dashboard & Sync", id="tab_dashboard_sync"):
184
- gr.Markdown("System checks for existing data from Bubble. The 'Sync' button activates if new data needs to be fetched from LinkedIn based on the last sync times and data availability.")
185
  sync_data_btn = gr.Button("πŸ”„ Sync LinkedIn Data", variant="primary", visible=False, interactive=False)
186
- sync_status_html_output = gr.HTML("<p style='text-align:center;'>Sync status will appear here.</p>")
187
  dashboard_display_html = gr.HTML("<p style='text-align:center;'>Dashboard loading...</p>")
188
 
189
  org_urn_display.change(
@@ -195,17 +175,18 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
195
 
196
  with gr.TabItem("2️⃣ Analytics", id="tab_analytics"):
197
  gr.Markdown("## πŸ“ˆ LinkedIn Performance Analytics")
198
- gr.Markdown("Select a date range to filter analytics. Click πŸ’£ for insights.")
199
 
200
- analytics_status_md = gr.Markdown("Analytics status will appear here...")
201
 
202
- with gr.Row():
203
  date_filter_selector = gr.Radio(
204
  ["All Time", "Last 7 Days", "Last 30 Days", "Custom Range"],
205
- label="Select Date Range", value="Last 30 Days"
206
  )
207
- custom_start_date_picker = gr.DateTime(label="Start Date", visible=False, include_time=False, type="datetime")
208
- custom_end_date_picker = gr.DateTime(label="End Date", visible=False, include_time=False, type="datetime")
 
209
 
210
  apply_filter_btn = gr.Button("πŸ” Apply Filter & Refresh Analytics", variant="primary")
211
 
@@ -219,8 +200,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
219
  outputs=[custom_start_date_picker, custom_end_date_picker]
220
  )
221
 
222
- # --- Define plot configurations ---
223
- # (Order must match the order of figures returned by update_analytics_plots_figures)
224
  plot_configs = [
225
  {"label": "Posts Activity Over Time", "id": "posts_activity", "section": "Posts & Engagement Overview"},
226
  {"label": "Post Engagement Types", "id": "engagement_type", "section": "Posts & Engagement Overview"},
@@ -246,71 +226,67 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
246
  {"label": "Mentions Volume Over Time (Detailed)", "id": "mention_analysis_volume", "section": "Mention Analysis (Detailed)"},
247
  {"label": "Breakdown of Mentions by Sentiment (Detailed)", "id": "mention_analysis_sentiment", "section": "Mention Analysis (Detailed)"}
248
  ]
249
- assert len(plot_configs) == 23, "Mismatch in number of plot configurations and expected plots."
250
-
251
- # --- Build Analytics Tab UI using the function from ui_generators ---
252
- # This function will create the gr.Markdown for sections and rows for plots.
253
- # It needs to be called within this gr.Blocks() context.
254
- plot_ui_objects = build_analytics_tab_ui_components(plot_configs)
255
-
256
- active_insight_plot_id_state = gr.State(None) # Stores the plot_id of the currently open insight panel
 
 
 
 
 
 
 
 
257
 
258
  # --- Bomb Button Click Handler ---
259
- def handle_bomb_click(plot_id_clicked, current_active_plot_id, current_token_state):
260
  logging.info(f"Bomb clicked for: {plot_id_clicked}. Currently active: {current_active_plot_id}")
261
- updates = []
262
- new_active_id = None
263
-
264
- if plot_id_clicked == current_active_plot_id:
265
- new_active_id = None # Toggle off
 
 
 
 
 
266
  logging.info(f"Closing insights for {plot_id_clicked}")
267
- else:
268
- new_active_id = plot_id_clicked # Activate new one
269
- logging.info(f"Opening insights for {plot_id_clicked}, closing others.")
270
-
271
- for p_id_iter, ui_obj_dict in plot_ui_objects.items():
272
- is_target_one = (p_id_iter == new_active_id)
273
- updates.append(gr.update(visible=is_target_one)) # For insights_col visibility
274
-
275
- if is_target_one:
276
- # TODO: Implement actual insight generation logic here
277
- insight_text = f"**Insights for {ui_obj_dict['label']}**\n\n"
278
- insight_text += f"Plot ID: `{p_id_iter}`.\n"
279
- insight_text += "Detailed analysis would involve examining trends, anomalies, and correlations related to this specific chart.\n"
280
- insight_text += "For example, for 'Posts Activity', we might look for days with unusually high or low activity and correlate with external events or content types."
281
- updates.append(gr.update(value=insight_text))
282
- else:
283
- updates.append(gr.update(value=f"Click πŸ’£ for insights on {ui_obj_dict['label']}...")) # Reset placeholder
284
 
285
- updates.append(new_active_id) # New value for active_insight_plot_id_state
286
- logging.info(f"Returning {len(updates)-1} UI updates. New active ID: {new_active_id}")
287
- return updates
288
 
289
  # --- Connect Bomb Buttons ---
290
- bomb_click_dynamic_outputs = []
291
- # The order of items in bomb_click_dynamic_outputs must match the order of iteration
292
- # in handle_bomb_click when it creates its `updates` list.
293
- # plot_ui_objects is a dictionary, so .keys() gives an arbitrary order if not Python 3.7+
294
- # To be safe, iterate based on plot_configs order for constructing outputs.
295
- for config in plot_configs:
296
- p_id_key = config["id"]
297
- bomb_click_dynamic_outputs.append(plot_ui_objects[p_id_key]["insights_col"])
298
- bomb_click_dynamic_outputs.append(plot_ui_objects[p_id_key]["insights_md"])
299
- bomb_click_dynamic_outputs.append(active_insight_plot_id_state)
300
 
301
  for config in plot_configs:
302
  plot_id = config["id"]
303
- components_dict = plot_ui_objects[plot_id]
304
- components_dict["bomb"].click(
305
- fn=handle_bomb_click,
306
- inputs=[gr.State(value=plot_id), active_insight_plot_id_state, token_state],
307
- outputs=bomb_click_dynamic_outputs,
308
- api_name=f"show_insights_{plot_id}" # Gradio handles None api_name if plot_id is None (though it shouldn't be)
309
- )
 
310
 
311
- # --- Function to Refresh All Analytics UI (Plots + Reset Insights) ---
312
  def refresh_all_analytics_ui_elements(current_token_state, date_filter_val, custom_start_val, custom_end_val):
313
- logging.info("Refreshing all analytics UI elements.")
314
  plot_generation_results = update_analytics_plots_figures(
315
  current_token_state, date_filter_val, custom_start_val, custom_end_val
316
  )
@@ -318,37 +294,53 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
318
  status_message_update = plot_generation_results[0]
319
  generated_plot_figures = plot_generation_results[1:]
320
 
321
- all_updates = [status_message_update]
322
 
323
  # Plot figure updates - iterate based on plot_configs to ensure order
324
  for i, config in enumerate(plot_configs):
325
  p_id_key = config["id"]
326
- if i < len(generated_plot_figures):
327
- all_updates.append(generated_plot_figures[i])
 
 
 
 
328
  else:
329
- logging.error(f"Mismatch: Expected figure for {p_id_key} but not enough figures generated.")
330
- all_updates.append(create_placeholder_plot("Figure Error", f"No figure for {p_id_key}"))
331
-
332
- # Insight column visibility and markdown content reset - iterate based on plot_configs
333
- for config in plot_configs:
334
- p_id_key = config["id"]
335
- ui_obj_dict_val = plot_ui_objects[p_id_key]
336
- all_updates.append(gr.update(visible=False)) # Hide insights_col
337
- all_updates.append(gr.update(value=f"Click πŸ’£ for insights on {ui_obj_dict_val['label']}...")) # Reset insights_md
338
 
 
 
 
339
  all_updates.append(None) # Reset active_insight_plot_id_state
 
 
340
  return all_updates
341
 
342
  # --- Define outputs for the apply_filter_btn and sync.then() ---
343
  apply_filter_and_sync_outputs = [analytics_status_md]
344
- # Iterate based on plot_configs to ensure order
345
- for config in plot_configs: # Plot components
346
- apply_filter_and_sync_outputs.append(plot_ui_objects[config["id"]]["plot"])
347
- for config in plot_configs: # Insight column components
348
- apply_filter_and_sync_outputs.append(plot_ui_objects[config["id"]]["insights_col"])
349
- for config in plot_configs: # Insight markdown components
350
- apply_filter_and_sync_outputs.append(plot_ui_objects[config["id"]]["insights_md"])
351
- apply_filter_and_sync_outputs.append(active_insight_plot_id_state) # State component
 
 
 
 
 
 
 
 
 
 
 
 
 
352
 
353
  # --- Connect Apply Filter Button ---
354
  apply_filter_btn.click(
@@ -359,6 +351,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
359
  )
360
 
361
  with gr.TabItem("3️⃣ Mentions", id="tab_mentions"):
 
362
  refresh_mentions_display_btn = gr.Button("πŸ”„ Refresh Mentions Display (from local data)", variant="secondary")
363
  mentions_html = gr.HTML("Mentions data loads from Bubble after sync. Click refresh to view current local data.")
364
  mentions_sentiment_dist_plot = gr.Plot(label="Mention Sentiment Distribution")
@@ -369,6 +362,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
369
  )
370
 
371
  with gr.TabItem("4️⃣ Follower Stats", id="tab_follower_stats"):
 
372
  refresh_follower_stats_btn = gr.Button("πŸ”„ Refresh Follower Stats Display (from local data)", variant="secondary")
373
  follower_stats_html = gr.HTML("Follower statistics load from Bubble after sync. Click refresh to view current local data.")
374
  with gr.Row():
@@ -383,7 +377,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
383
  show_progress="full"
384
  )
385
 
386
- # --- Define the full sync_click_event chain HERE, now that analytics outputs are known ---
387
  sync_event_part1 = sync_data_btn.click(
388
  fn=sync_all_linkedin_data_orchestrator,
389
  inputs=[token_state],
@@ -402,6 +396,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
402
  outputs=[dashboard_display_html],
403
  show_progress=False
404
  )
 
405
  sync_event_final = sync_event_part3.then(
406
  fn=refresh_all_analytics_ui_elements,
407
  inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker],
@@ -409,19 +404,17 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
409
  show_progress="full"
410
  )
411
 
412
-
413
  if __name__ == "__main__":
414
  if not os.environ.get(LINKEDIN_CLIENT_ID_ENV_VAR):
415
- logging.warning(f"WARNING: '{LINKEDIN_CLIENT_ID_ENV_VAR}' environment variable not set.")
416
  if not os.environ.get(BUBBLE_APP_NAME_ENV_VAR) or \
417
  not os.environ.get(BUBBLE_API_KEY_PRIVATE_ENV_VAR) or \
418
  not os.environ.get(BUBBLE_API_ENDPOINT_ENV_VAR):
419
- logging.warning("WARNING: Bubble environment variables not fully set.")
420
 
421
  try:
422
- logging.info(f"Matplotlib version: {matplotlib.__version__} found. Backend: {matplotlib.get_backend()}")
423
  except ImportError:
424
  logging.error("Matplotlib is not installed. Plots will not be generated.")
425
 
426
  app.launch(server_name="0.0.0.0", server_port=7860, debug=True)
427
-
 
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
 
20
  display_main_dashboard,
21
  run_mentions_tab_display,
22
  run_follower_stats_tab_display,
23
+ build_analytics_tab_plot_area # Import the updated UI builder
24
  )
25
  # Corrected import for analytics_data_processing
26
  from analytics_data_processing import prepare_filtered_analytics_data
 
33
  generate_engagement_rate_over_time_plot,
34
  generate_reach_over_time_plot,
35
  generate_impressions_over_time_plot,
36
+ create_placeholder_plot,
37
  generate_likes_over_time_plot,
38
  generate_clicks_over_time_plot,
39
  generate_shares_over_time_plot,
 
47
  # Configure logging
48
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(module)s - %(message)s')
49
 
50
+ # --- Analytics Tab: Plot Figure Generation Function ---
51
  def update_analytics_plots_figures(token_state_value, date_filter_option, custom_start_date, custom_end_date):
 
 
 
 
52
  logging.info(f"Updating analytics plot figures. Filter: {date_filter_option}, Custom Start: {custom_start_date}, Custom End: {custom_end_date}")
53
+ num_expected_plots = 23
54
 
55
  if not token_state_value or not token_state_value.get("token"):
56
  message = "❌ Access denied. No token. Cannot generate analytics."
 
78
  media_type_col_name = token_state_value.get("config_media_type_col", "media_type")
79
  eb_labels_col_name = token_state_value.get("config_eb_labels_col", "eb_labels")
80
 
81
+ plot_figs = []
 
 
82
  try:
 
83
  plot_figs.append(generate_posts_activity_plot(filtered_merged_posts_df, date_column=date_column_posts))
84
  plot_figs.append(generate_engagement_type_plot(filtered_merged_posts_df))
 
85
  fig_mentions_activity_shared = generate_mentions_activity_plot(filtered_mentions_df, date_column=date_column_mentions)
86
  fig_mention_sentiment_shared = generate_mention_sentiment_plot(filtered_mentions_df)
87
+ plot_figs.append(fig_mentions_activity_shared)
88
+ plot_figs.append(fig_mention_sentiment_shared)
 
 
89
  plot_figs.append(generate_followers_count_over_time_plot(date_filtered_follower_stats_df, type_value='follower_gains_monthly'))
90
  plot_figs.append(generate_followers_growth_rate_plot(date_filtered_follower_stats_df, type_value='follower_gains_monthly'))
91
  plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_geo', plot_title="Followers by Location"))
 
103
  plot_figs.append(generate_post_frequency_plot(filtered_merged_posts_df, date_column=date_column_posts))
104
  plot_figs.append(generate_content_format_breakdown_plot(filtered_merged_posts_df, format_col=media_type_col_name))
105
  plot_figs.append(generate_content_topic_breakdown_plot(filtered_merged_posts_df, topics_col=eb_labels_col_name))
106
+ plot_figs.append(fig_mentions_activity_shared)
107
+ plot_figs.append(fig_mention_sentiment_shared)
 
 
108
 
109
  message = f"πŸ“Š Analytics updated for period: {date_filter_option}"
110
  if date_filter_option == "Custom Range":
 
114
 
115
  final_plot_figs = []
116
  for i, p_fig in enumerate(plot_figs):
117
+ if p_fig is not None and not isinstance(p_fig, str): # Check if it's a Matplotlib figure
118
  final_plot_figs.append(p_fig)
119
  else:
120
  logging.warning(f"Plot figure generation failed or returned unexpected type for slot {i}, using placeholder. Figure: {p_fig}")
121
  final_plot_figs.append(create_placeholder_plot(title="Plot Error", message="Failed to generate this plot figure."))
122
 
123
  while len(final_plot_figs) < num_expected_plots:
124
+ logging.warning(f"Padding missing plot figure. Expected {num_expected_plots}, got {len(final_plot_figs)}.")
125
  final_plot_figs.append(create_placeholder_plot(title="Missing Plot", message="Plot figure could not be generated."))
126
 
 
127
  return [message] + final_plot_figs[:num_expected_plots]
128
 
129
  except Exception as e:
 
139
 
140
  token_state = gr.State(value={
141
  "token": None, "client_id": None, "org_urn": None,
142
+ "bubble_posts_df": pd.DataFrame(), "bubble_post_stats_df": pd.DataFrame(),
143
+ "bubble_mentions_df": pd.DataFrame(), "bubble_follower_stats_df": pd.DataFrame(),
144
+ "fetch_count_for_api": 0, "url_user_token_temp_storage": None,
145
+ "config_date_col_posts": "published_at", "config_date_col_mentions": "date",
146
+ "config_date_col_followers": "date", "config_media_type_col": "media_type",
 
 
 
 
 
147
  "config_eb_labels_col": "eb_labels"
148
  })
149
 
150
  gr.Markdown("# πŸš€ LinkedIn Organization Dashboard")
151
+ url_user_token_display = gr.Textbox(label="User Token (Hidden)", interactive=False, visible=False)
152
  status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...")
153
+ org_urn_display = gr.Textbox(label="Organization URN (Hidden)", interactive=False, visible=False)
154
 
155
  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)
156
 
157
  def initial_load_sequence(url_token, org_urn_val, current_state):
 
158
  status_msg, new_state, btn_update = process_and_store_bubble_token(url_token, org_urn_val, current_state)
159
  dashboard_content = display_main_dashboard(new_state)
160
  return status_msg, new_state, btn_update, dashboard_content
161
 
162
  with gr.Tabs() as tabs:
163
  with gr.TabItem("1️⃣ Dashboard & Sync", id="tab_dashboard_sync"):
164
+ gr.Markdown("System checks for existing data from Bubble. 'Sync' activates if new data is needed.")
165
  sync_data_btn = gr.Button("πŸ”„ Sync LinkedIn Data", variant="primary", visible=False, interactive=False)
166
+ sync_status_html_output = gr.HTML("<p style='text-align:center;'>Sync status...</p>")
167
  dashboard_display_html = gr.HTML("<p style='text-align:center;'>Dashboard loading...</p>")
168
 
169
  org_urn_display.change(
 
175
 
176
  with gr.TabItem("2️⃣ Analytics", id="tab_analytics"):
177
  gr.Markdown("## πŸ“ˆ LinkedIn Performance Analytics")
178
+ gr.Markdown("Select a date range. Click πŸ’£ for insights.")
179
 
180
+ analytics_status_md = gr.Markdown("Analytics status...")
181
 
182
+ with gr.Row(): # Filters row
183
  date_filter_selector = gr.Radio(
184
  ["All Time", "Last 7 Days", "Last 30 Days", "Custom Range"],
185
+ label="Select Date Range", value="Last 30 Days", scale=3
186
  )
187
+ with gr.Column(scale=2):
188
+ custom_start_date_picker = gr.DateTime(label="Start Date", visible=False, include_time=False, type="datetime")
189
+ custom_end_date_picker = gr.DateTime(label="End Date", visible=False, include_time=False, type="datetime")
190
 
191
  apply_filter_btn = gr.Button("πŸ” Apply Filter & Refresh Analytics", variant="primary")
192
 
 
200
  outputs=[custom_start_date_picker, custom_end_date_picker]
201
  )
202
 
203
+ # --- Define plot configurations (Order must match figure generation) ---
 
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"},
 
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
+ # --- Main layout for Analytics Tab: Plots Area and Global Insights Column ---
232
+ with gr.Row(equal_height=False): # Main row for plots area and insights column
233
+ with gr.Column(scale=8): # Column to hold all plot rows and section headers
234
+ # Build the plot area (section headers and rows of plot panels)
235
+ # This function is defined in ui_generators.py
236
+ # It will create gr.Markdown for sections and gr.Row for plot pairs
237
+ plot_ui_objects = build_analytics_tab_plot_area(plot_configs)
238
+
239
+ # Global Insights Column (initially hidden)
240
+ with gr.Column(scale=4, visible=False) as global_insights_column_ui:
241
+ gr.Markdown("### πŸ’‘ Generated Insights")
242
+ global_insights_markdown_ui = gr.Markdown("Click πŸ’£ on a plot to see insights here.")
243
+
244
+ active_insight_plot_id_state = gr.State(None)
245
 
246
  # --- Bomb Button Click Handler ---
247
+ def handle_bomb_click(plot_id_clicked, current_active_plot_id, token_state_val): # Added token_state_val
248
  logging.info(f"Bomb clicked for: {plot_id_clicked}. Currently active: {current_active_plot_id}")
249
+
250
+ # Retrieve the label for the clicked plot
251
+ clicked_plot_label = "Selected Plot" # Default
252
+ if plot_id_clicked and plot_id_clicked in plot_ui_objects:
253
+ clicked_plot_label = plot_ui_objects[plot_id_clicked]["label"]
254
+
255
+ if plot_id_clicked == current_active_plot_id: # Toggle off
256
+ new_active_id = None
257
+ insight_text_update = f"Insights for {clicked_plot_label} hidden. Click πŸ’£ to show."
258
+ insights_col_visible = False
259
  logging.info(f"Closing insights for {plot_id_clicked}")
260
+ else: # Activate new one or switch
261
+ new_active_id = plot_id_clicked
262
+ # TODO: Implement actual insight generation logic here using plot_id_clicked and token_state_val
263
+ insight_text_update = f"**Insights for: {clicked_plot_label}**\n\n"
264
+ insight_text_update += f"Plot ID: `{plot_id_clicked}`.\n"
265
+ insight_text_update += "This is where detailed, AI-generated insights for this specific chart would appear, based on its data and trends.\n"
266
+ insight_text_update += "For instance, if this were 'Post Engagement Types', we might analyze which type is dominant and suggest content strategies."
267
+ insights_col_visible = True
268
+ logging.info(f"Opening insights for {plot_id_clicked}")
 
 
 
 
 
 
 
 
269
 
270
+ return gr.update(visible=insights_col_visible), gr.update(value=insight_text_update), new_active_id
 
 
271
 
272
  # --- Connect Bomb Buttons ---
273
+ # Outputs for each bomb click: global insights column visibility, its markdown content, and the state
274
+ bomb_click_outputs = [global_insights_column_ui, global_insights_markdown_ui, active_insight_plot_id_state]
 
 
 
 
 
 
 
 
275
 
276
  for config in plot_configs:
277
  plot_id = config["id"]
278
+ if plot_id in plot_ui_objects: # Ensure the UI object was created
279
+ components_dict = plot_ui_objects[plot_id]
280
+ components_dict["bomb_button"].click(
281
+ fn=handle_bomb_click,
282
+ inputs=[gr.State(value=plot_id), active_insight_plot_id_state, token_state], # Pass token_state
283
+ outputs=bomb_click_outputs,
284
+ api_name=f"show_insights_{plot_id}"
285
+ )
286
 
287
+ # --- Function to Refresh All Analytics UI (Plots + Reset Global Insights) ---
288
  def refresh_all_analytics_ui_elements(current_token_state, date_filter_val, custom_start_val, custom_end_val):
289
+ logging.info("Refreshing all analytics UI elements and resetting insights.")
290
  plot_generation_results = update_analytics_plots_figures(
291
  current_token_state, date_filter_val, custom_start_val, custom_end_val
292
  )
 
294
  status_message_update = plot_generation_results[0]
295
  generated_plot_figures = plot_generation_results[1:]
296
 
297
+ all_updates = [status_message_update] # For analytics_status_md
298
 
299
  # Plot figure updates - iterate based on plot_configs to ensure order
300
  for i, config in enumerate(plot_configs):
301
  p_id_key = config["id"]
302
+ if p_id_key in plot_ui_objects: # Check if plot UI exists
303
+ if i < len(generated_plot_figures):
304
+ all_updates.append(generated_plot_figures[i])
305
+ else:
306
+ logging.error(f"Mismatch: Expected figure for {p_id_key} but not enough figures generated.")
307
+ all_updates.append(create_placeholder_plot("Figure Error", f"No figure for {p_id_key}"))
308
  else:
309
+ # This case should ideally not happen if plot_configs and plot_ui_objects are in sync
310
+ logging.warning(f"Plot UI object for id {p_id_key} not found during refresh. Skipping its figure update.")
311
+
 
 
 
 
 
 
312
 
313
+ # Reset Global Insights Column
314
+ all_updates.append(gr.update(visible=False)) # Hide global_insights_column_ui
315
+ all_updates.append(gr.update(value="Click πŸ’£ on a plot to see insights here.")) # Reset global_insights_markdown_ui
316
  all_updates.append(None) # Reset active_insight_plot_id_state
317
+
318
+ logging.info(f"Prepared {len(all_updates)} updates for analytics refresh.")
319
  return all_updates
320
 
321
  # --- Define outputs for the apply_filter_btn and sync.then() ---
322
  apply_filter_and_sync_outputs = [analytics_status_md]
323
+ # Add plot components (must be in the order of plot_configs)
324
+ for config in plot_configs:
325
+ p_id_key = config["id"]
326
+ if p_id_key in plot_ui_objects:
327
+ apply_filter_and_sync_outputs.append(plot_ui_objects[p_id_key]["plot_component"])
328
+ else:
329
+ # Add a placeholder None if a plot component wasn't created, to maintain output list length.
330
+ # This helps prevent errors if plot_ui_objects somehow doesn't contain an expected key.
331
+ apply_filter_and_sync_outputs.append(None)
332
+ logging.error(f"Plot component for {p_id_key} missing in plot_ui_objects for apply_filter_outputs.")
333
+
334
+
335
+ # Add global insights components and state
336
+ apply_filter_and_sync_outputs.extend([
337
+ global_insights_column_ui,
338
+ global_insights_markdown_ui,
339
+ active_insight_plot_id_state
340
+ ])
341
+
342
+ logging.info(f"Total outputs for apply_filter/sync: {len(apply_filter_and_sync_outputs)}")
343
+
344
 
345
  # --- Connect Apply Filter Button ---
346
  apply_filter_btn.click(
 
351
  )
352
 
353
  with gr.TabItem("3️⃣ Mentions", id="tab_mentions"):
354
+ # ... (Mentions tab content remains the same) ...
355
  refresh_mentions_display_btn = gr.Button("πŸ”„ Refresh Mentions Display (from local data)", variant="secondary")
356
  mentions_html = gr.HTML("Mentions data loads from Bubble after sync. Click refresh to view current local data.")
357
  mentions_sentiment_dist_plot = gr.Plot(label="Mention Sentiment Distribution")
 
362
  )
363
 
364
  with gr.TabItem("4️⃣ Follower Stats", id="tab_follower_stats"):
365
+ # ... (Follower Stats tab content remains the same) ...
366
  refresh_follower_stats_btn = gr.Button("πŸ”„ Refresh Follower Stats Display (from local data)", variant="secondary")
367
  follower_stats_html = gr.HTML("Follower statistics load from Bubble after sync. Click refresh to view current local data.")
368
  with gr.Row():
 
377
  show_progress="full"
378
  )
379
 
380
+ # --- Define the full sync_click_event chain HERE ---
381
  sync_event_part1 = sync_data_btn.click(
382
  fn=sync_all_linkedin_data_orchestrator,
383
  inputs=[token_state],
 
396
  outputs=[dashboard_display_html],
397
  show_progress=False
398
  )
399
+ # Connect to refresh analytics UI after sync
400
  sync_event_final = sync_event_part3.then(
401
  fn=refresh_all_analytics_ui_elements,
402
  inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker],
 
404
  show_progress="full"
405
  )
406
 
 
407
  if __name__ == "__main__":
408
  if not os.environ.get(LINKEDIN_CLIENT_ID_ENV_VAR):
409
+ logging.warning(f"WARNING: '{LINKEDIN_CLIENT_ID_ENV_VAR}' env var not set.")
410
  if not os.environ.get(BUBBLE_APP_NAME_ENV_VAR) or \
411
  not os.environ.get(BUBBLE_API_KEY_PRIVATE_ENV_VAR) or \
412
  not os.environ.get(BUBBLE_API_ENDPOINT_ENV_VAR):
413
+ logging.warning("WARNING: Bubble env vars not fully set.")
414
 
415
  try:
416
+ logging.info(f"Matplotlib version: {matplotlib.__version__}, Backend: {matplotlib.get_backend()}")
417
  except ImportError:
418
  logging.error("Matplotlib is not installed. Plots will not be generated.")
419
 
420
  app.launch(server_name="0.0.0.0", server_port=7860, debug=True)