File size: 25,852 Bytes
b560569
575b933
b0464a9
87a87e7
791c130
 
266ae82
f7fc39b
575b933
266ae82
4ad44b9
575b933
 
 
 
2a3b22e
575b933
 
 
 
 
266ae82
6a8e128
9d99925
3b4dccb
266ae82
3b4dccb
 
 
 
 
 
 
 
 
6a8e128
deb2291
266ae82
deb2291
 
c6716b6
 
 
 
3b4dccb
b0464a9
2a3b22e
3b4dccb
2a3b22e
6a8e128
266ae82
 
6a8e128
deb2291
791c130
 
 
3b4dccb
 
a342a6b
575b933
266ae82
 
 
 
3b4dccb
348bc84
791c130
 
 
 
 
3b4dccb
 
791c130
 
 
266ae82
 
348bc84
6a8e128
791c130
266ae82
 
c6716b6
266ae82
6a8e128
 
266ae82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6a8e128
 
deb2291
791c130
 
 
266ae82
791c130
 
266ae82
 
6a8e128
266ae82
c6716b6
266ae82
 
 
 
6a8e128
266ae82
 
 
791c130
575b933
266ae82
791c130
3b4dccb
 
a342a6b
b0464a9
2a3b22e
adb3bbe
266ae82
179ea1f
67742c4
a342a6b
6a8e128
 
 
 
 
266ae82
67742c4
adb3bbe
a342a6b
6a8e128
575b933
6a8e128
179ea1f
a342a6b
575b933
0612e1d
4ad44b9
266ae82
0612e1d
adb3bbe
791c130
 
6a8e128
0612e1d
6a8e128
575b933
a342a6b
2a3b22e
4ad44b9
2a3b22e
a342a6b
 
2a3b22e
791c130
 
 
6a8e128
791c130
6a8e128
791c130
6a8e128
791c130
 
6a8e128
791c130
6a8e128
 
 
791c130
 
 
 
3b902c0
 
 
791c130
 
 
 
 
 
6a8e128
266ae82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6a8e128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266ae82
 
6a8e128
266ae82
6a8e128
 
 
 
 
 
 
 
 
 
266ae82
6a8e128
 
 
 
 
 
 
 
 
266ae82
6a8e128
266ae82
 
6a8e128
 
266ae82
 
 
6a8e128
 
 
 
 
 
 
 
c6716b6
6a8e128
266ae82
6a8e128
266ae82
 
 
 
 
 
 
6a8e128
266ae82
 
 
 
6a8e128
 
 
 
 
 
266ae82
6a8e128
 
 
266ae82
6a8e128
 
 
266ae82
6a8e128
 
266ae82
 
 
 
6a8e128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266ae82
 
791c130
266ae82
791c130
266ae82
a342a6b
adb3bbe
06d22e5
791c130
6a8e128
a342a6b
 
266ae82
4ad44b9
 
266ae82
a342a6b
 
575b933
791c130
6a8e128
a342a6b
 
 
266ae82
a342a6b
 
575b933
a342a6b
 
 
266ae82
a342a6b
538b42b
791c130
6a8e128
266ae82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6a8e128
266ae82
 
 
 
 
 
 
adb3bbe
575b933
6a8e128
575b933
 
 
6a8e128
a342a6b
 
6a8e128
a342a6b
791c130
a342a6b
791c130
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
import gradio as gr
import pandas as pd
import os
import logging
import matplotlib
matplotlib.use('Agg') # Set backend for Matplotlib to avoid GUI conflicts with Gradio
import matplotlib.pyplot as plt

# --- Module Imports ---
from gradio_utils import get_url_user_token

# Functions from newly created/refactored modules
from config import (
    LINKEDIN_CLIENT_ID_ENV_VAR, BUBBLE_APP_NAME_ENV_VAR,
    BUBBLE_API_KEY_PRIVATE_ENV_VAR, BUBBLE_API_ENDPOINT_ENV_VAR
)
from state_manager import process_and_store_bubble_token
from sync_logic import sync_all_linkedin_data_orchestrator
from ui_generators import (
    display_main_dashboard,
    run_mentions_tab_display,
    run_follower_stats_tab_display,
    build_analytics_tab_plot_area # Import the updated UI builder
)
# Corrected import for analytics_data_processing
from analytics_data_processing import prepare_filtered_analytics_data
from analytics_plot_generator import (
    generate_posts_activity_plot, generate_engagement_type_plot,
    generate_mentions_activity_plot, generate_mention_sentiment_plot,
    generate_followers_count_over_time_plot,
    generate_followers_growth_rate_plot,
    generate_followers_by_demographics_plot,
    generate_engagement_rate_over_time_plot,
    generate_reach_over_time_plot,
    generate_impressions_over_time_plot,
    create_placeholder_plot, 
    generate_likes_over_time_plot,
    generate_clicks_over_time_plot,
    generate_shares_over_time_plot,
    generate_comments_over_time_plot,
    generate_comments_sentiment_breakdown_plot,
    generate_post_frequency_plot,
    generate_content_format_breakdown_plot,
    generate_content_topic_breakdown_plot
)

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(module)s - %(message)s')

# --- Analytics Tab: Plot Figure Generation Function ---
def update_analytics_plots_figures(token_state_value, date_filter_option, custom_start_date, custom_end_date):
    logging.info(f"Updating analytics plot figures. Filter: {date_filter_option}, Custom Start: {custom_start_date}, Custom End: {custom_end_date}")
    num_expected_plots = 23 

    if not token_state_value or not token_state_value.get("token"):
        message = "❌ Access denied. No token. Cannot generate analytics."
        logging.warning(message)
        placeholder_figs = [create_placeholder_plot(title="Access Denied", message="No token.") for _ in range(num_expected_plots)]
        return [message] + placeholder_figs

    try:
        (filtered_merged_posts_df,
         filtered_mentions_df,
         date_filtered_follower_stats_df,
         raw_follower_stats_df,
         start_dt_for_msg, end_dt_for_msg) = \
            prepare_filtered_analytics_data(
                token_state_value, date_filter_option, custom_start_date, custom_end_date
            )
    except Exception as e:
        error_msg = f"❌ Error preparing analytics data: {e}"
        logging.error(error_msg, exc_info=True)
        placeholder_figs = [create_placeholder_plot(title="Data Preparation Error", message=str(e)) for _ in range(num_expected_plots)]
        return [error_msg] + placeholder_figs

    date_column_posts = token_state_value.get("config_date_col_posts", "published_at")
    date_column_mentions = token_state_value.get("config_date_col_mentions", "date")
    media_type_col_name = token_state_value.get("config_media_type_col", "media_type")
    eb_labels_col_name = token_state_value.get("config_eb_labels_col", "eb_labels")

    plot_figs = []
    try:
        plot_figs.append(generate_posts_activity_plot(filtered_merged_posts_df, date_column=date_column_posts))
        plot_figs.append(generate_engagement_type_plot(filtered_merged_posts_df))
        fig_mentions_activity_shared = generate_mentions_activity_plot(filtered_mentions_df, date_column=date_column_mentions)
        fig_mention_sentiment_shared = generate_mention_sentiment_plot(filtered_mentions_df)
        plot_figs.append(fig_mentions_activity_shared)
        plot_figs.append(fig_mention_sentiment_shared)
        plot_figs.append(generate_followers_count_over_time_plot(date_filtered_follower_stats_df, type_value='follower_gains_monthly'))
        plot_figs.append(generate_followers_growth_rate_plot(date_filtered_follower_stats_df, type_value='follower_gains_monthly'))
        plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_geo', plot_title="Followers by Location"))
        plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_function', plot_title="Followers by Role"))
        plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_industry', plot_title="Followers by Industry"))
        plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_seniority', plot_title="Followers by Seniority"))
        plot_figs.append(generate_engagement_rate_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
        plot_figs.append(generate_reach_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
        plot_figs.append(generate_impressions_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
        plot_figs.append(generate_likes_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
        plot_figs.append(generate_clicks_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
        plot_figs.append(generate_shares_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
        plot_figs.append(generate_comments_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
        plot_figs.append(generate_comments_sentiment_breakdown_plot(filtered_merged_posts_df, sentiment_column='comment_sentiment'))
        plot_figs.append(generate_post_frequency_plot(filtered_merged_posts_df, date_column=date_column_posts))
        plot_figs.append(generate_content_format_breakdown_plot(filtered_merged_posts_df, format_col=media_type_col_name))
        plot_figs.append(generate_content_topic_breakdown_plot(filtered_merged_posts_df, topics_col=eb_labels_col_name))
        plot_figs.append(fig_mentions_activity_shared) 
        plot_figs.append(fig_mention_sentiment_shared)

        message = f"πŸ“Š Analytics updated for period: {date_filter_option}"
        if date_filter_option == "Custom Range":
            s_display = start_dt_for_msg.strftime('%Y-%m-%d') if start_dt_for_msg else "Any"
            e_display = end_dt_for_msg.strftime('%Y-%m-%d') if end_dt_for_msg else "Any"
            message += f" (From: {s_display} To: {e_display})"
        
        final_plot_figs = []
        for i, p_fig in enumerate(plot_figs):
            if p_fig is not None and not isinstance(p_fig, str): # Check if it's a Matplotlib figure
                final_plot_figs.append(p_fig)
            else:
                logging.warning(f"Plot figure generation failed or returned unexpected type for slot {i}, using placeholder. Figure: {p_fig}")
                final_plot_figs.append(create_placeholder_plot(title="Plot Error", message="Failed to generate this plot figure."))
        
        while len(final_plot_figs) < num_expected_plots:
            logging.warning(f"Padding missing plot figure. Expected {num_expected_plots}, got {len(final_plot_figs)}.")
            final_plot_figs.append(create_placeholder_plot(title="Missing Plot", message="Plot figure could not be generated."))
        
        return [message] + final_plot_figs[:num_expected_plots]

    except Exception as e:
        error_msg = f"❌ Error generating analytics plot figures: {e}"
        logging.error(error_msg, exc_info=True)
        placeholder_figs = [create_placeholder_plot(title="Plot Generation Error", message=str(e)) for _ in range(num_expected_plots)]
        return [error_msg] + placeholder_figs


# --- Gradio UI Blocks ---
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
               title="LinkedIn Organization Dashboard") as app:

    token_state = gr.State(value={
        "token": None, "client_id": None, "org_urn": None,
        "bubble_posts_df": pd.DataFrame(), "bubble_post_stats_df": pd.DataFrame(),
        "bubble_mentions_df": pd.DataFrame(), "bubble_follower_stats_df": pd.DataFrame(),
        "fetch_count_for_api": 0, "url_user_token_temp_storage": None,
        "config_date_col_posts": "published_at", "config_date_col_mentions": "date",
        "config_date_col_followers": "date", "config_media_type_col": "media_type",
        "config_eb_labels_col": "eb_labels"
    })

    gr.Markdown("# πŸš€ LinkedIn Organization Dashboard")
    url_user_token_display = gr.Textbox(label="User Token (Hidden)", interactive=False, visible=False)
    status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...")
    org_urn_display = gr.Textbox(label="Organization URN (Hidden)", interactive=False, visible=False)

    app.load(fn=get_url_user_token, inputs=None, outputs=[url_user_token_display, org_urn_display], api_name="get_url_params", show_progress=False)

    def initial_load_sequence(url_token, org_urn_val, current_state):
        status_msg, new_state, btn_update = process_and_store_bubble_token(url_token, org_urn_val, current_state)
        dashboard_content = display_main_dashboard(new_state)
        return status_msg, new_state, btn_update, dashboard_content

    with gr.Tabs() as tabs:
        with gr.TabItem("1️⃣ Dashboard & Sync", id="tab_dashboard_sync"):
            gr.Markdown("System checks for existing data from Bubble. 'Sync' activates if new data is needed.")
            sync_data_btn = gr.Button("πŸ”„ Sync LinkedIn Data", variant="primary", visible=False, interactive=False)
            sync_status_html_output = gr.HTML("<p style='text-align:center;'>Sync status...</p>")
            dashboard_display_html = gr.HTML("<p style='text-align:center;'>Dashboard loading...</p>")

            org_urn_display.change(
                fn=initial_load_sequence,
                inputs=[url_user_token_display, org_urn_display, token_state],
                outputs=[status_box, token_state, sync_data_btn, dashboard_display_html],
                show_progress="full"
            )
            
        with gr.TabItem("2️⃣ Analytics", id="tab_analytics"):
            gr.Markdown("## πŸ“ˆ LinkedIn Performance Analytics")
            gr.Markdown("Select a date range. Click πŸ’£ for insights.")
            
            analytics_status_md = gr.Markdown("Analytics status...")

            with gr.Row(): # Filters row
                date_filter_selector = gr.Radio(
                    ["All Time", "Last 7 Days", "Last 30 Days", "Custom Range"],
                    label="Select Date Range", value="Last 30 Days", scale=3
                )
                with gr.Column(scale=2):
                    custom_start_date_picker = gr.DateTime(label="Start Date", visible=False, include_time=False, type="datetime")
                    custom_end_date_picker = gr.DateTime(label="End Date", visible=False, include_time=False, type="datetime")
            
            apply_filter_btn = gr.Button("πŸ” Apply Filter & Refresh Analytics", variant="primary")

            def toggle_custom_date_pickers(selection):
                is_custom = selection == "Custom Range"
                return gr.update(visible=is_custom), gr.update(visible=is_custom)

            date_filter_selector.change(
                fn=toggle_custom_date_pickers,
                inputs=[date_filter_selector],
                outputs=[custom_start_date_picker, custom_end_date_picker]
            )

            # --- Define plot configurations (Order must match figure generation) ---
            plot_configs = [
                {"label": "Posts Activity Over Time", "id": "posts_activity", "section": "Posts & Engagement Overview"},
                {"label": "Post Engagement Types", "id": "engagement_type", "section": "Posts & Engagement Overview"},
                {"label": "Mentions Activity Over Time", "id": "mentions_activity", "section": "Mentions Overview"}, 
                {"label": "Mention Sentiment Distribution", "id": "mention_sentiment", "section": "Mentions Overview"}, 
                {"label": "Followers Count Over Time", "id": "followers_count", "section": "Follower Dynamics"},
                {"label": "Followers Growth Rate", "id": "followers_growth_rate", "section": "Follower Dynamics"},
                {"label": "Followers by Location", "id": "followers_by_location", "section": "Follower Demographics"},
                {"label": "Followers by Role (Function)", "id": "followers_by_role", "section": "Follower Demographics"},
                {"label": "Followers by Industry", "id": "followers_by_industry", "section": "Follower Demographics"},
                {"label": "Followers by Seniority", "id": "followers_by_seniority", "section": "Follower Demographics"},
                {"label": "Engagement Rate Over Time", "id": "engagement_rate", "section": "Post Performance Insights"},
                {"label": "Reach Over Time (Clicks)", "id": "reach_over_time", "section": "Post Performance Insights"},
                {"label": "Impressions Over Time", "id": "impressions_over_time", "section": "Post Performance Insights"},
                {"label": "Reactions (Likes) Over Time", "id": "likes_over_time", "section": "Post Performance Insights"},
                {"label": "Clicks Over Time", "id": "clicks_over_time", "section": "Detailed Post Engagement Over Time"},
                {"label": "Shares Over Time", "id": "shares_over_time", "section": "Detailed Post Engagement Over Time"},
                {"label": "Comments Over Time", "id": "comments_over_time", "section": "Detailed Post Engagement Over Time"},
                {"label": "Breakdown of Comments by Sentiment", "id": "comments_sentiment", "section": "Detailed Post Engagement Over Time"},
                {"label": "Post Frequency", "id": "post_frequency_cs", "section": "Content Strategy Analysis"},
                {"label": "Breakdown of Content by Format", "id": "content_format_breakdown_cs", "section": "Content Strategy Analysis"},
                {"label": "Breakdown of Content by Topics", "id": "content_topic_breakdown_cs", "section": "Content Strategy Analysis"},
                {"label": "Mentions Volume Over Time (Detailed)", "id": "mention_analysis_volume", "section": "Mention Analysis (Detailed)"}, 
                {"label": "Breakdown of Mentions by Sentiment (Detailed)", "id": "mention_analysis_sentiment", "section": "Mention Analysis (Detailed)"} 
            ]
            assert len(plot_configs) == 23, "Mismatch in plot_configs and expected plots."

            # --- Main layout for Analytics Tab: Plots Area and Global Insights Column ---
            with gr.Row(equal_height=False): # Main row for plots area and insights column
                with gr.Column(scale=8): # Column to hold all plot rows and section headers
                    # Build the plot area (section headers and rows of plot panels)
                    # This function is defined in ui_generators.py
                    # It will create gr.Markdown for sections and gr.Row for plot pairs
                    plot_ui_objects = build_analytics_tab_plot_area(plot_configs)
                
                # Global Insights Column (initially hidden)
                with gr.Column(scale=4, visible=False) as global_insights_column_ui:
                    gr.Markdown("### πŸ’‘ Generated Insights")
                    global_insights_markdown_ui = gr.Markdown("Click πŸ’£ on a plot to see insights here.")

            active_insight_plot_id_state = gr.State(None) 

            # --- Bomb Button Click Handler ---
            def handle_bomb_click(plot_id_clicked, current_active_plot_id, token_state_val): # Added token_state_val
                logging.info(f"Bomb clicked for: {plot_id_clicked}. Currently active: {current_active_plot_id}")
                
                # Retrieve the label for the clicked plot
                clicked_plot_label = "Selected Plot" # Default
                if plot_id_clicked and plot_id_clicked in plot_ui_objects:
                    clicked_plot_label = plot_ui_objects[plot_id_clicked]["label"]

                if plot_id_clicked == current_active_plot_id: # Toggle off
                    new_active_id = None
                    insight_text_update = f"Insights for {clicked_plot_label} hidden. Click πŸ’£ to show."
                    insights_col_visible = False
                    logging.info(f"Closing insights for {plot_id_clicked}")
                else: # Activate new one or switch
                    new_active_id = plot_id_clicked
                    # TODO: Implement actual insight generation logic here using plot_id_clicked and token_state_val
                    insight_text_update = f"**Insights for: {clicked_plot_label}**\n\n"
                    insight_text_update += f"Plot ID: `{plot_id_clicked}`.\n"
                    insight_text_update += "This is where detailed, AI-generated insights for this specific chart would appear, based on its data and trends.\n"
                    insight_text_update += "For instance, if this were 'Post Engagement Types', we might analyze which type is dominant and suggest content strategies."
                    insights_col_visible = True
                    logging.info(f"Opening insights for {plot_id_clicked}")
                
                return gr.update(visible=insights_col_visible), gr.update(value=insight_text_update), new_active_id

            # --- Connect Bomb Buttons ---
            # Outputs for each bomb click: global insights column visibility, its markdown content, and the state
            bomb_click_outputs = [global_insights_column_ui, global_insights_markdown_ui, active_insight_plot_id_state]

            for config in plot_configs:
                plot_id = config["id"]
                if plot_id in plot_ui_objects: # Ensure the UI object was created
                    components_dict = plot_ui_objects[plot_id]
                    components_dict["bomb_button"].click(
                        fn=handle_bomb_click,
                        inputs=[gr.State(value=plot_id), active_insight_plot_id_state, token_state], # Pass token_state
                        outputs=bomb_click_outputs,
                        api_name=f"show_insights_{plot_id}" 
                    )
            
            # --- Function to Refresh All Analytics UI (Plots + Reset Global Insights) ---
            def refresh_all_analytics_ui_elements(current_token_state, date_filter_val, custom_start_val, custom_end_val):
                logging.info("Refreshing all analytics UI elements and resetting insights.")
                plot_generation_results = update_analytics_plots_figures(
                    current_token_state, date_filter_val, custom_start_val, custom_end_val
                )
                
                status_message_update = plot_generation_results[0]
                generated_plot_figures = plot_generation_results[1:]

                all_updates = [status_message_update] # For analytics_status_md

                # Plot figure updates - iterate based on plot_configs to ensure order
                for i, config in enumerate(plot_configs):
                    p_id_key = config["id"]
                    if p_id_key in plot_ui_objects: # Check if plot UI exists
                        if i < len(generated_plot_figures):
                            all_updates.append(generated_plot_figures[i])
                        else:
                            logging.error(f"Mismatch: Expected figure for {p_id_key} but not enough figures generated.")
                            all_updates.append(create_placeholder_plot("Figure Error", f"No figure for {p_id_key}"))
                    else:
                        # This case should ideally not happen if plot_configs and plot_ui_objects are in sync
                        logging.warning(f"Plot UI object for id {p_id_key} not found during refresh. Skipping its figure update.")


                # Reset Global Insights Column
                all_updates.append(gr.update(visible=False)) # Hide global_insights_column_ui
                all_updates.append(gr.update(value="Click πŸ’£ on a plot to see insights here.")) # Reset global_insights_markdown_ui
                all_updates.append(None) # Reset active_insight_plot_id_state
                
                logging.info(f"Prepared {len(all_updates)} updates for analytics refresh.")
                return all_updates

            # --- Define outputs for the apply_filter_btn and sync.then() ---
            apply_filter_and_sync_outputs = [analytics_status_md]
            # Add plot components (must be in the order of plot_configs)
            for config in plot_configs:
                p_id_key = config["id"]
                if p_id_key in plot_ui_objects:
                     apply_filter_and_sync_outputs.append(plot_ui_objects[p_id_key]["plot_component"])
                else:
                    # Add a placeholder None if a plot component wasn't created, to maintain output list length.
                    # This helps prevent errors if plot_ui_objects somehow doesn't contain an expected key.
                    apply_filter_and_sync_outputs.append(None) 
                    logging.error(f"Plot component for {p_id_key} missing in plot_ui_objects for apply_filter_outputs.")


            # Add global insights components and state
            apply_filter_and_sync_outputs.extend([
                global_insights_column_ui, 
                global_insights_markdown_ui, 
                active_insight_plot_id_state
            ])
            
            logging.info(f"Total outputs for apply_filter/sync: {len(apply_filter_and_sync_outputs)}")


            # --- Connect Apply Filter Button ---
            apply_filter_btn.click(
                fn=refresh_all_analytics_ui_elements,
                inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker],
                outputs=apply_filter_and_sync_outputs,
                show_progress="full"
            )

        with gr.TabItem("3️⃣ Mentions", id="tab_mentions"):
            # ... (Mentions tab content remains the same) ...
            refresh_mentions_display_btn = gr.Button("πŸ”„ Refresh Mentions Display (from local data)", variant="secondary")
            mentions_html = gr.HTML("Mentions data loads from Bubble after sync. Click refresh to view current local data.")
            mentions_sentiment_dist_plot = gr.Plot(label="Mention Sentiment Distribution")
            refresh_mentions_display_btn.click(
                fn=run_mentions_tab_display, inputs=[token_state],
                outputs=[mentions_html, mentions_sentiment_dist_plot],
                show_progress="full"
            )

        with gr.TabItem("4️⃣ Follower Stats", id="tab_follower_stats"):
            # ... (Follower Stats tab content remains the same) ...
            refresh_follower_stats_btn = gr.Button("πŸ”„ Refresh Follower Stats Display (from local data)", variant="secondary")
            follower_stats_html = gr.HTML("Follower statistics load from Bubble after sync. Click refresh to view current local data.")
            with gr.Row():
                fs_plot_monthly_gains = gr.Plot(label="Monthly Follower Gains")
            with gr.Row():
                fs_plot_seniority = gr.Plot(label="Followers by Seniority (Top 10 Organic)")
                fs_plot_industry = gr.Plot(label="Followers by Industry (Top 10 Organic)")

            refresh_follower_stats_btn.click(
                fn=run_follower_stats_tab_display, inputs=[token_state],
                outputs=[follower_stats_html, fs_plot_monthly_gains, fs_plot_seniority, fs_plot_industry],
                show_progress="full"
            )
    
    # --- Define the full sync_click_event chain HERE ---
    sync_event_part1 = sync_data_btn.click(
        fn=sync_all_linkedin_data_orchestrator,
        inputs=[token_state],
        outputs=[sync_status_html_output, token_state], 
        show_progress="full"
    )
    sync_event_part2 = sync_event_part1.then(
        fn=process_and_store_bubble_token,
        inputs=[url_user_token_display, org_urn_display, token_state], 
        outputs=[status_box, token_state, sync_data_btn], 
        show_progress=False
    )
    sync_event_part3 = sync_event_part2.then(
        fn=display_main_dashboard,
        inputs=[token_state], 
        outputs=[dashboard_display_html],
        show_progress=False
    )
    # Connect to refresh analytics UI after sync
    sync_event_final = sync_event_part3.then(
        fn=refresh_all_analytics_ui_elements,
        inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker], 
        outputs=apply_filter_and_sync_outputs, 
        show_progress="full"
    )

if __name__ == "__main__":
    if not os.environ.get(LINKEDIN_CLIENT_ID_ENV_VAR):
        logging.warning(f"WARNING: '{LINKEDIN_CLIENT_ID_ENV_VAR}' env var not set.")
    if not os.environ.get(BUBBLE_APP_NAME_ENV_VAR) or \
       not os.environ.get(BUBBLE_API_KEY_PRIVATE_ENV_VAR) or \
       not os.environ.get(BUBBLE_API_ENDPOINT_ENV_VAR):
        logging.warning("WARNING: Bubble env vars not fully set.")

    try:
        logging.info(f"Matplotlib version: {matplotlib.__version__}, Backend: {matplotlib.get_backend()}")
    except ImportError:
        logging.error("Matplotlib is not installed. Plots will not be generated.")

    app.launch(server_name="0.0.0.0", server_port=7860, debug=True)