GuglielmoTor commited on
Commit
4ad44b9
Β·
verified Β·
1 Parent(s): 7dc216d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +331 -216
app.py CHANGED
@@ -4,38 +4,50 @@ import json
4
  import os
5
  import logging
6
  import html
7
- import pandas as pd # Ensure pandas is imported
8
- from datetime import datetime # Used for pd.Timestamp
9
 
10
  # Import functions from your custom modules
11
- from Data_Fetching_and_Rendering import fetch_and_render_dashboard
12
  from analytics_fetch_and_rendering import fetch_and_render_analytics
13
- from mentions_dashboard import generate_mentions_dashboard
14
  from gradio_utils import get_url_user_token
15
- # Updated import to include fetch_posts_from_bubble
16
  from Bubble_API_Calls import (
17
  fetch_linkedin_token_from_bubble,
18
  bulk_upload_to_bubble,
19
- fetch_linkedin_posts_data_from_bubble
 
 
20
  )
 
21
  from Linkedin_Data_API_Calls import (
22
  fetch_linkedin_posts_core,
23
  fetch_comments,
24
- analyze_sentiment,
25
  compile_detailed_posts,
26
- prepare_data_for_bubble
 
 
 
 
27
  )
28
 
29
  # Configure logging
30
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
31
 
32
  # --- Global Constants ---
33
- # Standard number of posts for initial fetch
34
- DEFAULT_INITIAL_FETCH_COUNT = 10
35
- # Key for post URN in data processed from LinkedIn (e.g., in detailed_posts)
36
  LINKEDIN_POST_URN_KEY = 'id'
37
- # Column name for post URN in the DataFrame fetched from Bubble (bubble_posts_df)
38
- BUBBLE_POST_URN_COLUMN_NAME = 'id' # Adjust if your Bubble 'LI_posts' table uses a different column name for URNs
 
 
 
 
 
 
 
 
 
39
 
40
  def check_token_status(token_state):
41
  """Checks the status of the LinkedIn token."""
@@ -43,7 +55,7 @@ def check_token_status(token_state):
43
 
44
  def process_and_store_bubble_token(url_user_token, org_urn, token_state):
45
  """
46
- Processes user token, fetches LinkedIn token, fetches Bubble posts,
47
  and determines if an initial fetch or update is needed for LinkedIn posts.
48
  Updates token state and UI for the sync button.
49
  """
@@ -51,18 +63,24 @@ def process_and_store_bubble_token(url_user_token, org_urn, token_state):
51
 
52
  new_state = token_state.copy() if token_state else {
53
  "token": None, "client_id": None, "org_urn": None,
54
- "bubble_posts_df": None, "fetch_count_for_api": 0
 
 
55
  }
56
- new_state.update({"org_urn": org_urn, "bubble_posts_df": new_state.get("bubble_posts_df"), "fetch_count_for_api": new_state.get("fetch_count_for_api", 0)})
 
 
 
 
 
 
 
57
 
58
- button_update = gr.update(visible=False, interactive=False, value="πŸ”„ Sync LinkedIn Posts")
59
 
60
  client_id = os.environ.get("Linkedin_client_id")
61
- if not client_id:
62
- logging.error("CRITICAL ERROR: 'Linkedin_client_id' environment variable not set.")
63
- new_state["client_id"] = "ENV VAR MISSING"
64
- else:
65
- new_state["client_id"] = client_id
66
 
67
  if url_user_token and "not found" not in url_user_token and "Could not access" not in url_user_token:
68
  logging.info(f"Attempting to fetch LinkedIn token from Bubble with user token: {url_user_token}")
@@ -83,286 +101,383 @@ def process_and_store_bubble_token(url_user_token, org_urn, token_state):
83
 
84
  current_org_urn = new_state.get("org_urn")
85
  if current_org_urn:
 
86
  logging.info(f"Attempting to fetch posts from Bubble for org_urn: {current_org_urn}")
87
  try:
88
- fetched_df, error_message = fetch_linkedin_posts_data_from_bubble(current_org_urn, "LI_posts")
89
- if error_message:
90
- logging.warning(f"Error reported by fetch_linkedin_posts_data_from_bubble: {error_message}. Treating as no data.")
91
- new_state["bubble_posts_df"] = pd.DataFrame() # Ensure it's an empty DataFrame
92
- else:
93
- new_state["bubble_posts_df"] = fetched_df if fetched_df is not None else pd.DataFrame()
 
 
 
 
 
 
 
94
  except Exception as e:
95
- logging.error(f"❌ Error fetching posts from Bubble: {e}. Treating as no data.")
96
- new_state["bubble_posts_df"] = pd.DataFrame()
97
  else:
98
- logging.warning("Org URN not available in state. Cannot fetch posts from Bubble.")
99
  new_state["bubble_posts_df"] = pd.DataFrame()
 
100
 
101
-
102
- DATE_COLUMN_NAME = 'published_at'
103
-
104
- if new_state["bubble_posts_df"] is None or new_state["bubble_posts_df"].empty:
105
- logging.info(f"ℹ️ No posts found in Bubble or DataFrame is empty. Button to fetch initial {DEFAULT_INITIAL_FETCH_COUNT} posts will be visible.")
106
  new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
107
- button_update = gr.update(value=f"πŸ”„ Fetch Initial {DEFAULT_INITIAL_FETCH_COUNT} LinkedIn Posts", visible=True, interactive=True)
108
  else:
109
  try:
110
- df_for_date_check = new_state["bubble_posts_df"].copy()
111
- if DATE_COLUMN_NAME not in df_for_date_check.columns:
112
- logging.warning(f"Date column '{DATE_COLUMN_NAME}' not found in Bubble posts DataFrame. Assuming initial fetch of {DEFAULT_INITIAL_FETCH_COUNT} posts.")
113
- new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
114
- button_update = gr.update(value=f"πŸ”„ Fetch Initial {DEFAULT_INITIAL_FETCH_COUNT} (Date Column Missing)", visible=True, interactive=True)
115
- elif df_for_date_check[DATE_COLUMN_NAME].isnull().all():
116
- logging.warning(f"Date column '{DATE_COLUMN_NAME}' contains all null values. Assuming initial fetch of {DEFAULT_INITIAL_FETCH_COUNT} posts.")
117
  new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
118
- button_update = gr.update(value=f"πŸ”„ Fetch Initial {DEFAULT_INITIAL_FETCH_COUNT} (Date Column Empty)", visible=True, interactive=True)
119
  else:
120
- df_for_date_check[DATE_COLUMN_NAME] = pd.to_datetime(df_for_date_check[DATE_COLUMN_NAME], errors='coerce', utc=True)
121
- last_post_date_utc = df_for_date_check[DATE_COLUMN_NAME].dropna().max()
122
-
123
  if pd.isna(last_post_date_utc):
124
- logging.warning(f"No valid dates found in '{DATE_COLUMN_NAME}' after conversion. Assuming initial fetch of {DEFAULT_INITIAL_FETCH_COUNT} posts.")
125
  new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
126
- button_update = gr.update(value=f"πŸ”„ Fetch Initial {DEFAULT_INITIAL_FETCH_COUNT} (No Valid Dates)", visible=True, interactive=True)
127
  else:
128
- today_utc = pd.Timestamp('now', tz='UTC').normalize()
129
- last_post_date_utc_normalized = last_post_date_utc.normalize()
130
-
131
- time_difference_days = (today_utc - last_post_date_utc_normalized).days
132
- logging.info(f"Last post date (UTC, normalized): {last_post_date_utc_normalized}, Today (UTC, normalized): {today_utc}, Difference: {time_difference_days} days.")
133
-
134
- if time_difference_days >= 7:
135
- num_weeks = max(1, time_difference_days // 7)
136
- fetch_count = num_weeks * 10
137
- new_state['fetch_count_for_api'] = fetch_count
138
- button_label = f"πŸ”„ Update Last {num_weeks} Week(s) (~{fetch_count} Posts)"
139
- logging.info(f"Data is {time_difference_days} days old. Update needed for {num_weeks} weeks, ~{fetch_count} posts.")
140
- button_update = gr.update(value=button_label, visible=True, interactive=True)
141
  else:
142
- logging.info(f"Data is fresh ({time_difference_days} days old). No update needed now.")
143
- new_state['fetch_count_for_api'] = 0
144
- button_update = gr.update(visible=False, interactive=False)
145
  except Exception as e:
146
- logging.error(f"Error processing dates from Bubble posts: {e}. Defaulting to initial fetch of {DEFAULT_INITIAL_FETCH_COUNT} posts.")
147
  new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
148
- button_update = gr.update(value=f"πŸ”„ Fetch Initial {DEFAULT_INITIAL_FETCH_COUNT} (Date Error)", visible=True, interactive=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
 
150
  token_status_message = check_token_status(new_state)
151
- logging.info(f"Token processing complete. LinkedIn Token Status: {token_status_message}. Button update: {button_update}. Fetch count for API: {new_state['fetch_count_for_api']}")
152
  return token_status_message, new_state, button_update
153
 
154
- def guarded_fetch_posts(token_state):
155
- logging.info("Starting guarded_fetch_posts process.")
 
 
156
  if not token_state or not token_state.get("token"):
157
- logging.error("Access denied for guarded_fetch_posts. No LinkedIn token available.")
158
- return "<p style='color:red; text-align:center;'>❌ Access denied. LinkedIn token not available.</p>"
159
 
160
  client_id = token_state.get("client_id")
161
  token_dict = token_state.get("token")
162
  org_urn = token_state.get('org_urn')
163
- fetch_count_value = token_state.get('fetch_count_for_api')
164
- bubble_posts_df = token_state.get("bubble_posts_df") # Get existing posts
165
-
166
- if not org_urn:
167
- logging.error("Organization URN (org_urn) not found in token_state.")
168
- return "<p style='color:red; text-align:center;'>❌ Configuration error: Organization URN missing.</p>"
169
- if not client_id or client_id == "ENV VAR MISSING":
170
- logging.error("Client ID not found or missing in token_state.")
171
- return "<p style='color:red; text-align:center;'>❌ Configuration error: LinkedIn Client ID missing.</p>"
172
-
173
- if fetch_count_value == 0:
174
- logging.info("Data is fresh. No new posts fetched based on date check.")
175
- return "<p style='color:green; text-align:center;'>βœ… Data is already up-to-date. No new posts fetched.</p>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
 
 
 
 
 
177
  try:
178
- logging.info(f"Step 1: Fetching core posts for org_urn: {org_urn}. Fetch count: {fetch_count_value}")
179
- processed_raw_posts, stats_map, _ = fetch_linkedin_posts_core(client_id, token_dict, org_urn, count=fetch_count_value)
180
-
181
- if not processed_raw_posts:
182
- logging.info("No posts retrieved from LinkedIn API.")
183
- return "<p style='color:orange; text-align:center;'>ℹ️ No new LinkedIn posts found to process.</p>"
184
-
185
- # --- Filter out posts already in Bubble ---
186
- existing_post_urns = set()
187
- if bubble_posts_df is not None and not bubble_posts_df.empty and BUBBLE_POST_URN_COLUMN_NAME in bubble_posts_df.columns:
188
- existing_post_urns = set(bubble_posts_df[BUBBLE_POST_URN_COLUMN_NAME].dropna().astype(str))
189
- logging.info(f"Found {len(existing_post_urns)} existing post URNs in Bubble data.")
190
- else:
191
- logging.info("No existing posts found in Bubble data or URN column missing; all fetched posts will be considered new.")
192
 
193
- # Filter processed_raw_posts before compiling detailed_posts
194
- new_raw_posts = [
195
- post for post in processed_raw_posts
196
- if str(post.get(LINKEDIN_POST_URN_KEY)) not in existing_post_urns
197
- ]
198
 
199
- if not new_raw_posts:
200
- logging.info("All fetched LinkedIn posts are already present in Bubble. No new posts to add.")
201
- return "<p style='color:green; text-align:center;'>βœ… All fetched posts already exist in Bubble. Data is up-to-date.</p>"
202
 
203
- logging.info(f"Identified {len(new_raw_posts)} new posts to process after filtering against Bubble data.")
 
204
 
205
- # Continue processing only with new_raw_posts
206
- post_urns_to_process = [post[LINKEDIN_POST_URN_KEY] for post in new_raw_posts if post.get(LINKEDIN_POST_URN_KEY)]
207
-
208
- logging.info("Step 2: Fetching comments for new posts via LinkedIn API.")
209
- # Adjust stats_map if it's keyed by URNs; ensure it's relevant for new_raw_posts
210
- # For simplicity, assuming fetch_comments and subsequent steps can handle potentially fewer URNs
211
- all_comments_data = fetch_comments(client_id, token_dict, post_urns_to_process, stats_map)
212
-
213
- logging.info("Step 3: Analyzing sentiment for new posts.")
214
- sentiments_per_post = analyze_sentiment(all_comments_data) # Assumes all_comments_data is now for new posts
215
-
216
- logging.info("Step 4: Compiling detailed data for new posts.")
217
- # Pass new_raw_posts to compile_detailed_posts
218
- detailed_new_posts = compile_detailed_posts(new_raw_posts, stats_map, sentiments_per_post)
219
-
220
- logging.info("Step 5: Preparing data for Bubble (only new posts).")
221
- # Pass detailed_new_posts to prepare_data_for_bubble
222
- li_posts, li_post_stats, li_post_comments = prepare_data_for_bubble(detailed_new_posts, all_comments_data)
223
-
224
- logging.info(f"Step 6: Uploading {len(li_posts)} new posts and their related data to Bubble.")
225
- if li_posts: # Ensure there's actually something to upload
226
- bulk_upload_to_bubble(li_posts, "LI_posts")
227
- if li_post_stats:
228
- bulk_upload_to_bubble(li_post_stats, "LI_post_stats")
229
- if li_post_comments:
230
- bulk_upload_to_bubble(li_post_comments, "LI_post_comments")
231
-
232
- action_message = f"uploaded {len(li_posts)} new post(s)"
233
- else:
234
- action_message = "found no new posts to upload after detailed processing"
235
- logging.info("No new posts to upload after final preparation for Bubble.")
236
 
 
 
 
 
 
 
237
 
238
- final_message_verb = "Initial data fetch" if fetch_count_value == DEFAULT_INITIAL_FETCH_COUNT and not existing_post_urns else "Data update"
239
- logging.info(f"Successfully completed: {final_message_verb}. {action_message} to Bubble.")
240
- return f"<p style='color:green; text-align:center;'>βœ… {final_message_verb} complete. Successfully {action_message} to Bubble.</p>"
 
 
 
 
241
 
242
  except ValueError as ve:
243
- logging.error(f"ValueError during LinkedIn data processing: {ve}")
244
- return f"<p style='color:red; text-align:center;'>❌ Error: {html.escape(str(ve))}</p>"
245
  except Exception as e:
246
- logging.exception("An unexpected error occurred in guarded_fetch_posts.")
247
- return "<p style='color:red; text-align:center;'>❌ An unexpected error occurred. Please check logs.</p>"
 
248
 
249
- def guarded_fetch_dashboard(token_state):
 
250
  if not token_state or not token_state.get("token"):
251
- return "❌ Access denied. No token available for dashboard."
252
- if token_state.get("bubble_posts_df") is not None and not token_state["bubble_posts_df"].empty:
253
- return f"<p style='text-align: center;'>Dashboard would show {len(token_state['bubble_posts_df'])} posts from Bubble.</p>"
 
 
 
 
 
 
 
 
 
 
 
 
254
  else:
255
- return "<p style='text-align: center; color: #555;'>No posts loaded from Bubble yet for the dashboard.</p>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
 
258
  def guarded_fetch_analytics(token_state):
259
  if not token_state or not token_state.get("token"):
260
- return ("❌ Access denied. No token available for analytics.",
261
- None, None, None, None, None, None, None)
262
- return fetch_and_render_analytics(token_state.get("client_id"), token_state.get("token"))
263
 
264
- def run_mentions_and_load(token_state):
 
 
265
  if not token_state or not token_state.get("token"):
266
  return ("❌ Access denied. No token available for mentions.", None)
267
- return generate_mentions_dashboard(token_state.get("client_id"), token_state.get("token"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
269
  # --- Gradio UI Blocks ---
270
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
271
- title="LinkedIn Post Viewer & Analytics") as app:
272
 
273
  token_state = gr.State(value={
274
- "token": None,
275
- "client_id": None,
276
- "org_urn": None,
277
- "bubble_posts_df": pd.DataFrame(), # Initialize with empty DataFrame
278
- "fetch_count_for_api": 0
279
  })
280
 
281
  gr.Markdown("# πŸš€ LinkedIn Organization Post Viewer & Analytics")
282
- gr.Markdown("Token is supplied via URL parameter for Bubble.io lookup. Then explore dashboard and analytics.")
283
-
284
  url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False)
285
  status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...")
286
  org_urn_display = gr.Textbox(label="Organization URN (from URL - Hidden)", interactive=False, visible=False)
287
 
288
  app.load(fn=get_url_user_token, inputs=None, outputs=[url_user_token_display, org_urn_display])
 
 
 
 
 
 
289
 
290
  with gr.Tabs():
291
  with gr.TabItem("1️⃣ Dashboard & Sync"):
292
- gr.Markdown("System checks for existing data in Bubble. The button below will activate if new posts need to be fetched or updated from LinkedIn.")
293
-
294
- sync_posts_to_bubble_btn = gr.Button(
295
- value="πŸ”„ Sync LinkedIn Posts",
296
- variant="primary",
297
- visible=False,
298
- interactive=False
299
- )
300
-
301
- dashboard_html_output = gr.HTML(
302
- "<p style='text-align: center; color: #555;'>System initializing... "
303
- "Checking for existing data in Bubble and LinkedIn token.</p>"
304
- )
305
 
 
306
  org_urn_display.change(
307
- fn=process_and_store_bubble_token,
308
- inputs=[url_user_token_display, org_urn_display, token_state],
309
- outputs=[status_box, token_state, sync_posts_to_bubble_btn]
310
- )
311
- url_user_token_display.change(
312
- fn=process_and_store_bubble_token,
313
  inputs=[url_user_token_display, org_urn_display, token_state],
314
- outputs=[status_box, token_state, sync_posts_to_bubble_btn]
315
  )
 
 
316
 
317
- sync_posts_to_bubble_btn.click(
318
- fn=guarded_fetch_posts,
319
  inputs=[token_state],
320
- outputs=[dashboard_html_output]
321
  ).then(
322
- fn=process_and_store_bubble_token,
323
  inputs=[url_user_token_display, org_urn_display, token_state],
324
- outputs=[status_box, token_state, sync_posts_to_bubble_btn]
 
 
 
 
325
  )
326
 
327
  with gr.TabItem("2️⃣ Analytics"):
328
- gr.Markdown("View follower count and monthly gains for your organization (requires LinkedIn token).")
329
  fetch_analytics_btn = gr.Button("πŸ“ˆ Fetch Follower Analytics", variant="primary")
330
- follower_count = gr.Markdown("<p style='text-align: center; color: #555;'>Waiting for LinkedIn token...</p>")
331
-
332
- with gr.Row():
333
- follower_plot, growth_plot = gr.Plot(), gr.Plot()
334
- with gr.Row():
335
- eng_rate_plot = gr.Plot()
336
- with gr.Row():
337
- interaction_plot = gr.Plot()
338
- with gr.Row():
339
- eb_plot = gr.Plot()
340
- with gr.Row():
341
- mentions_vol_plot, mentions_sentiment_plot = gr.Plot(), gr.Plot()
342
-
343
  fetch_analytics_btn.click(
344
- fn=guarded_fetch_analytics,
345
- inputs=[token_state],
346
  outputs=[follower_count, follower_plot, growth_plot, eng_rate_plot,
347
  interaction_plot, eb_plot, mentions_vol_plot, mentions_sentiment_plot]
348
  )
349
 
350
  with gr.TabItem("3️⃣ Mentions"):
351
- gr.Markdown("Analyze sentiment of recent posts that mention your organization (requires LinkedIn token).")
352
- fetch_mentions_btn = gr.Button("🧠 Fetch Mentions & Sentiment", variant="primary")
353
- mentions_html = gr.HTML("<p style='text-align: center; color: #555;'>Waiting for LinkedIn token...</p>")
354
- mentions_plot = gr.Plot()
355
- fetch_mentions_btn.click(
356
- fn=run_mentions_and_load,
357
- inputs=[token_state],
358
  outputs=[mentions_html, mentions_plot]
359
  )
360
 
361
  app.load(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
362
  gr.Timer(15.0).tick(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
363
 
364
-
365
  if __name__ == "__main__":
366
  if not os.environ.get("Linkedin_client_id"):
367
- logging.warning("WARNING: The 'Linkedin_client_id' environment variable is not set. The application may not function correctly for LinkedIn API calls.")
368
- app.launch(server_name="0.0.0.0", server_port=7860, share=True)
 
4
  import os
5
  import logging
6
  import html
7
+ import pandas as pd
8
+ from datetime import datetime, timedelta # Used for pd.Timestamp and date checks
9
 
10
  # Import functions from your custom modules
 
11
  from analytics_fetch_and_rendering import fetch_and_render_analytics
 
12
  from gradio_utils import get_url_user_token
13
+
14
  from Bubble_API_Calls import (
15
  fetch_linkedin_token_from_bubble,
16
  bulk_upload_to_bubble,
17
+ fetch_linkedin_posts_data_from_bubble,
18
+ # You need to implement this function in Bubble_API_Calls.py:
19
+ fetch_linkedin_mentions_data_from_bubble
20
  )
21
+
22
  from Linkedin_Data_API_Calls import (
23
  fetch_linkedin_posts_core,
24
  fetch_comments,
25
+ analyze_sentiment, # For post comments
26
  compile_detailed_posts,
27
+ prepare_data_for_bubble, # For posts, stats, comments
28
+ fetch_linkedin_mentions_core,
29
+ analyze_mentions_sentiment, # For individual mentions
30
+ compile_detailed_mentions, # Compiles to user-specified format
31
+ prepare_mentions_for_bubble # Prepares user-specified format for Bubble
32
  )
33
 
34
  # Configure logging
35
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
36
 
37
  # --- Global Constants ---
38
+ DEFAULT_INITIAL_FETCH_COUNT = 10
 
 
39
  LINKEDIN_POST_URN_KEY = 'id'
40
+ BUBBLE_POST_URN_COLUMN_NAME = 'id'
41
+ BUBBLE_POST_DATE_COLUMN_NAME = 'published_at'
42
+
43
+ # Constants for Mentions - these should match the keys used in the data prepared for Bubble
44
+ BUBBLE_MENTIONS_TABLE_NAME = "LI_mentions" # Your Bubble table name for mentions
45
+ BUBBLE_MENTIONS_ID_COLUMN_NAME = "id" # Column in Bubble storing the mention's source post URN (share_urn)
46
+ BUBBLE_MENTIONS_DATE_COLUMN_NAME = "date" # Column in Bubble storing the mention's publication date
47
+
48
+ DEFAULT_MENTIONS_INITIAL_FETCH_COUNT = 20
49
+ DEFAULT_MENTIONS_UPDATE_FETCH_COUNT = 10
50
+
51
 
52
  def check_token_status(token_state):
53
  """Checks the status of the LinkedIn token."""
 
55
 
56
  def process_and_store_bubble_token(url_user_token, org_urn, token_state):
57
  """
58
+ Processes user token, fetches LinkedIn token, fetches existing Bubble posts & mentions,
59
  and determines if an initial fetch or update is needed for LinkedIn posts.
60
  Updates token state and UI for the sync button.
61
  """
 
63
 
64
  new_state = token_state.copy() if token_state else {
65
  "token": None, "client_id": None, "org_urn": None,
66
+ "bubble_posts_df": pd.DataFrame(), "fetch_count_for_api": 0,
67
+ "bubble_mentions_df": pd.DataFrame(), "fetch_count_for_mentions_api": 0,
68
+ "url_user_token_temp_storage": None
69
  }
70
+ new_state.update({
71
+ "org_urn": org_urn,
72
+ "bubble_posts_df": new_state.get("bubble_posts_df", pd.DataFrame()),
73
+ "fetch_count_for_api": new_state.get("fetch_count_for_api", 0),
74
+ "bubble_mentions_df": new_state.get("bubble_mentions_df", pd.DataFrame()),
75
+ "fetch_count_for_mentions_api": new_state.get("fetch_count_for_mentions_api", 0),
76
+ "url_user_token_temp_storage": url_user_token # Store for potential re-use
77
+ })
78
 
79
+ button_update = gr.update(visible=False, interactive=False, value="πŸ”„ Sync LinkedIn Data")
80
 
81
  client_id = os.environ.get("Linkedin_client_id")
82
+ new_state["client_id"] = client_id if client_id else "ENV VAR MISSING"
83
+ if not client_id: logging.error("CRITICAL ERROR: 'Linkedin_client_id' environment variable not set.")
 
 
 
84
 
85
  if url_user_token and "not found" not in url_user_token and "Could not access" not in url_user_token:
86
  logging.info(f"Attempting to fetch LinkedIn token from Bubble with user token: {url_user_token}")
 
101
 
102
  current_org_urn = new_state.get("org_urn")
103
  if current_org_urn:
104
+ # Fetch Posts from Bubble
105
  logging.info(f"Attempting to fetch posts from Bubble for org_urn: {current_org_urn}")
106
  try:
107
+ fetched_posts_df, error_message_posts = fetch_linkedin_posts_data_from_bubble(current_org_urn, "LI_posts")
108
+ new_state["bubble_posts_df"] = pd.DataFrame() if error_message_posts or fetched_posts_df is None else fetched_posts_df
109
+ if error_message_posts: logging.warning(f"Error from fetch_linkedin_posts_data_from_bubble: {error_message_posts}.")
110
+ except Exception as e:
111
+ logging.error(f"❌ Error fetching posts from Bubble: {e}.")
112
+ new_state["bubble_posts_df"] = pd.DataFrame()
113
+
114
+ # Fetch Mentions from Bubble
115
+ logging.info(f"Attempting to fetch mentions from Bubble for org_urn: {current_org_urn}")
116
+ try:
117
+ fetched_mentions_df, error_message_mentions = fetch_linkedin_mentions_data_from_bubble(current_org_urn, BUBBLE_MENTIONS_TABLE_NAME)
118
+ new_state["bubble_mentions_df"] = pd.DataFrame() if error_message_mentions or fetched_mentions_df is None else fetched_mentions_df
119
+ if error_message_mentions: logging.warning(f"Error from fetch_linkedin_mentions_data_from_bubble: {error_message_mentions}.")
120
  except Exception as e:
121
+ logging.error(f"❌ Error fetching mentions from Bubble: {e}.")
122
+ new_state["bubble_mentions_df"] = pd.DataFrame()
123
  else:
124
+ logging.warning("Org URN not available in state. Cannot fetch posts or mentions from Bubble.")
125
  new_state["bubble_posts_df"] = pd.DataFrame()
126
+ new_state["bubble_mentions_df"] = pd.DataFrame()
127
 
128
+ # Determine fetch count for Posts API
129
+ if new_state["bubble_posts_df"].empty:
130
+ logging.info(f"ℹ️ No posts in Bubble. Setting to fetch initial {DEFAULT_INITIAL_FETCH_COUNT} posts.")
 
 
131
  new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
 
132
  else:
133
  try:
134
+ df_posts_check = new_state["bubble_posts_df"].copy()
135
+ if BUBBLE_POST_DATE_COLUMN_NAME not in df_posts_check.columns or df_posts_check[BUBBLE_POST_DATE_COLUMN_NAME].isnull().all():
136
+ logging.warning(f"Date column '{BUBBLE_POST_DATE_COLUMN_NAME}' for posts missing/all null. Initial fetch.")
 
 
 
 
137
  new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
 
138
  else:
139
+ df_posts_check[BUBBLE_POST_DATE_COLUMN_NAME] = pd.to_datetime(df_posts_check[BUBBLE_POST_DATE_COLUMN_NAME], errors='coerce', utc=True)
140
+ last_post_date_utc = df_posts_check[BUBBLE_POST_DATE_COLUMN_NAME].dropna().max()
 
141
  if pd.isna(last_post_date_utc):
 
142
  new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
 
143
  else:
144
+ days_diff = (pd.Timestamp('now', tz='UTC').normalize() - last_post_date_utc.normalize()).days
145
+ if days_diff >= 7:
146
+ new_state['fetch_count_for_api'] = max(1, days_diff // 7) * 10
 
 
 
 
 
 
 
 
 
 
147
  else:
148
+ new_state['fetch_count_for_api'] = 0
 
 
149
  except Exception as e:
150
+ logging.error(f"Error processing post dates: {e}. Defaulting to initial fetch.")
151
  new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
152
+
153
+ # Determine if mentions need fetching (actual count decided in sync_linkedin_mentions)
154
+ mentions_need_sync = False
155
+ if new_state["bubble_mentions_df"].empty:
156
+ mentions_need_sync = True
157
+ else:
158
+ if BUBBLE_MENTIONS_DATE_COLUMN_NAME not in new_state["bubble_mentions_df"].columns or new_state["bubble_mentions_df"][BUBBLE_MENTIONS_DATE_COLUMN_NAME].isnull().all():
159
+ mentions_need_sync = True
160
+ else:
161
+ df_mentions_check = new_state["bubble_mentions_df"].copy()
162
+ df_mentions_check[BUBBLE_MENTIONS_DATE_COLUMN_NAME] = pd.to_datetime(df_mentions_check[BUBBLE_MENTIONS_DATE_COLUMN_NAME], errors='coerce', utc=True)
163
+ last_mention_date_utc = df_mentions_check[BUBBLE_MENTIONS_DATE_COLUMN_NAME].dropna().max()
164
+ if pd.isna(last_mention_date_utc) or (pd.Timestamp('now', tz='UTC').normalize() - last_mention_date_utc.normalize()).days >= 7:
165
+ mentions_need_sync = True
166
+
167
+ if new_state['fetch_count_for_api'] > 0 or (new_state["token"] and mentions_need_sync):
168
+ button_label = "πŸ”„ Sync LinkedIn Data"
169
+ if new_state['fetch_count_for_api'] > 0 and mentions_need_sync:
170
+ button_label += " (Posts & Mentions)"
171
+ elif new_state['fetch_count_for_api'] > 0:
172
+ button_label += f" ({new_state['fetch_count_for_api']} Posts)"
173
+ elif mentions_need_sync:
174
+ button_label += " (Mentions)"
175
+ button_update = gr.update(value=button_label, visible=True, interactive=True)
176
+ else:
177
+ button_update = gr.update(visible=False, interactive=False)
178
 
179
  token_status_message = check_token_status(new_state)
180
+ logging.info(f"Token processing complete. Status: {token_status_message}. Button: {button_update}. Post Fetch: {new_state['fetch_count_for_api']}. Mentions sync needed: {mentions_need_sync}")
181
  return token_status_message, new_state, button_update
182
 
183
+
184
+ def sync_linkedin_mentions(token_state):
185
+ """Fetches and syncs LinkedIn mentions to Bubble based on defined logic."""
186
+ logging.info("Starting LinkedIn mentions sync process.")
187
  if not token_state or not token_state.get("token"):
188
+ logging.error("Mentions sync: Access denied. No LinkedIn token.")
189
+ return "Mentions: No token. ", token_state
190
 
191
  client_id = token_state.get("client_id")
192
  token_dict = token_state.get("token")
193
  org_urn = token_state.get('org_urn')
194
+ bubble_mentions_df = token_state.get("bubble_mentions_df", pd.DataFrame())
195
+
196
+ if not org_urn or not client_id or client_id == "ENV VAR MISSING":
197
+ logging.error("Mentions sync: Configuration error (Org URN or Client ID missing).")
198
+ return "Mentions: Config error. ", token_state
199
+
200
+ fetch_count_for_mentions_api = 0
201
+ if bubble_mentions_df.empty:
202
+ fetch_count_for_mentions_api = DEFAULT_MENTIONS_INITIAL_FETCH_COUNT
203
+ logging.info(f"No mentions in Bubble. Fetching initial {fetch_count_for_mentions_api} mentions.")
204
+ else:
205
+ if BUBBLE_MENTIONS_DATE_COLUMN_NAME not in bubble_mentions_df.columns or bubble_mentions_df[BUBBLE_MENTIONS_DATE_COLUMN_NAME].isnull().all():
206
+ logging.warning(f"Date column '{BUBBLE_MENTIONS_DATE_COLUMN_NAME}' for mentions missing or all null. Fetching initial.")
207
+ fetch_count_for_mentions_api = DEFAULT_MENTIONS_INITIAL_FETCH_COUNT
208
+ else:
209
+ mentions_df_copy = bubble_mentions_df.copy()
210
+ mentions_df_copy[BUBBLE_MENTIONS_DATE_COLUMN_NAME] = pd.to_datetime(mentions_df_copy[BUBBLE_MENTIONS_DATE_COLUMN_NAME], errors='coerce', utc=True)
211
+ last_mention_date_utc = mentions_df_copy[BUBBLE_MENTIONS_DATE_COLUMN_NAME].dropna().max()
212
+
213
+ if pd.isna(last_mention_date_utc):
214
+ logging.warning("No valid dates in mentions data. Fetching initial.")
215
+ fetch_count_for_mentions_api = DEFAULT_MENTIONS_INITIAL_FETCH_COUNT
216
+ else:
217
+ days_since_last_mention = (pd.Timestamp('now', tz='UTC').normalize() - last_mention_date_utc.normalize()).days
218
+ logging.info(f"Days since last mention: {days_since_last_mention}")
219
+ if days_since_last_mention >= 7:
220
+ fetch_count_for_mentions_api = DEFAULT_MENTIONS_UPDATE_FETCH_COUNT
221
+ logging.info(f"Last mention older than 7 days. Fetching update of {fetch_count_for_mentions_api} mentions.")
222
+ else:
223
+ logging.info("Mentions data is fresh. No API fetch needed.")
224
 
225
+ token_state["fetch_count_for_mentions_api"] = fetch_count_for_mentions_api
226
+
227
+ if fetch_count_for_mentions_api == 0:
228
+ return "Mentions: Up-to-date. ", token_state
229
+
230
  try:
231
+ logging.info(f"Fetching {fetch_count_for_mentions_api} core mentions from LinkedIn for org_urn: {org_urn}")
232
+ processed_raw_mentions = fetch_linkedin_mentions_core(client_id, token_dict, org_urn, count=fetch_count_for_mentions_api)
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
+ if not processed_raw_mentions:
235
+ logging.info("No mentions retrieved from LinkedIn API.")
236
+ return "Mentions: None found via API. ", token_state
 
 
237
 
238
+ existing_mention_ids = set()
239
+ if not bubble_mentions_df.empty and BUBBLE_MENTIONS_ID_COLUMN_NAME in bubble_mentions_df.columns:
240
+ existing_mention_ids = set(bubble_mentions_df[BUBBLE_MENTIONS_ID_COLUMN_NAME].dropna().astype(str))
241
 
242
+ sentiments_map = analyze_mentions_sentiment(processed_raw_mentions)
243
+ all_compiled_mentions = compile_detailed_mentions(processed_raw_mentions, sentiments_map)
244
 
245
+ new_compiled_mentions_to_upload = [
246
+ m for m in all_compiled_mentions if str(m.get("id")) not in existing_mention_ids
247
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
 
249
+ if not new_compiled_mentions_to_upload:
250
+ logging.info("All fetched LinkedIn mentions are already present in Bubble.")
251
+ return "Mentions: All fetched already in Bubble. ", token_state
252
+
253
+ logging.info(f"Identified {len(new_compiled_mentions_to_upload)} new mentions to process after filtering.")
254
+ bubble_ready_mentions = prepare_mentions_for_bubble(new_compiled_mentions_to_upload)
255
 
256
+ if bubble_ready_mentions:
257
+ logging.info(f"Uploading {len(bubble_ready_mentions)} new mentions to Bubble table: {BUBBLE_MENTIONS_TABLE_NAME}.")
258
+ bulk_upload_to_bubble(bubble_ready_mentions, BUBBLE_MENTIONS_TABLE_NAME)
259
+ return f"Mentions: Synced {len(bubble_ready_mentions)} new. ", token_state
260
+ else:
261
+ logging.info("No new mentions to upload to Bubble after final preparation.")
262
+ return "Mentions: No new ones to upload. ", token_state
263
 
264
  except ValueError as ve:
265
+ logging.error(f"ValueError during mentions sync: {ve}")
266
+ return f"Mentions Error: {html.escape(str(ve))}. ", token_state
267
  except Exception as e:
268
+ logging.exception("Unexpected error in sync_linkedin_mentions.")
269
+ return "Mentions: Unexpected error. ", token_state
270
+
271
 
272
+ def guarded_fetch_posts_and_mentions(token_state):
273
+ logging.info("Starting guarded_fetch_posts_and_mentions process.")
274
  if not token_state or not token_state.get("token"):
275
+ logging.error("Access denied. No LinkedIn token available.")
276
+ return "<p style='color:red; text-align:center;'>❌ Access denied. LinkedIn token not available.</p>", token_state
277
+
278
+ client_id = token_state.get("client_id")
279
+ token_dict = token_state.get("token")
280
+ org_urn = token_state.get('org_urn')
281
+ fetch_count_for_posts_api = token_state.get('fetch_count_for_api', 0)
282
+ bubble_posts_df = token_state.get("bubble_posts_df", pd.DataFrame())
283
+ posts_sync_message = ""
284
+
285
+ if not org_urn: return "<p style='color:red;'>❌ Config error: Org URN missing.</p>", token_state
286
+ if not client_id or client_id == "ENV VAR MISSING": return "<p style='color:red;'>❌ Config error: Client ID missing.</p>", token_state
287
+
288
+ if fetch_count_for_posts_api == 0:
289
+ posts_sync_message = "Posts: Already up-to-date. "
290
  else:
291
+ try:
292
+ logging.info(f"Fetching {fetch_count_for_posts_api} core posts for org_urn: {org_urn}.")
293
+ processed_raw_posts, stats_map, _ = fetch_linkedin_posts_core(client_id, token_dict, org_urn, count=fetch_count_for_posts_api)
294
+ if not processed_raw_posts: posts_sync_message = "Posts: None found via API. "
295
+ else:
296
+ existing_post_urns = set()
297
+ if not bubble_posts_df.empty and BUBBLE_POST_URN_COLUMN_NAME in bubble_posts_df.columns:
298
+ existing_post_urns = set(bubble_posts_df[BUBBLE_POST_URN_COLUMN_NAME].dropna().astype(str))
299
+ new_raw_posts = [p for p in processed_raw_posts if str(p.get(LINKEDIN_POST_URN_KEY)) not in existing_post_urns]
300
+ if not new_raw_posts: posts_sync_message = "Posts: All fetched already in Bubble. "
301
+ else:
302
+ post_urns_to_process = [p[LINKEDIN_POST_URN_KEY] for p in new_raw_posts if p.get(LINKEDIN_POST_URN_KEY)]
303
+ all_comments_data = fetch_comments(client_id, token_dict, post_urns_to_process, stats_map)
304
+ sentiments_per_post = analyze_sentiment(all_comments_data)
305
+ detailed_new_posts = compile_detailed_posts(new_raw_posts, stats_map, sentiments_per_post)
306
+ li_posts, li_post_stats, li_post_comments = prepare_data_for_bubble(detailed_new_posts, all_comments_data)
307
+ if li_posts:
308
+ bulk_upload_to_bubble(li_posts, "LI_posts")
309
+ if li_post_stats: bulk_upload_to_bubble(li_post_stats, "LI_post_stats")
310
+ if li_post_comments: bulk_upload_to_bubble(li_post_comments, "LI_post_comments")
311
+ posts_sync_message = f"Posts: Synced {len(li_posts)} new. "
312
+ else: posts_sync_message = "Posts: No new ones to upload. "
313
+ except ValueError as ve: posts_sync_message = f"Posts Error: {html.escape(str(ve))}. "
314
+ except Exception: logging.exception("Posts processing error."); posts_sync_message = "Posts: Unexpected error. "
315
+
316
+ mentions_sync_message, updated_token_state = sync_linkedin_mentions(token_state)
317
+ token_state = updated_token_state # Ensure state is updated after mentions sync
318
+
319
+ # Re-fetch data from Bubble to update DataFrames in state for immediate display refresh
320
+ if org_urn:
321
+ try:
322
+ fetched_posts_df, _ = fetch_linkedin_posts_data_from_bubble(org_urn, "LI_posts")
323
+ token_state["bubble_posts_df"] = pd.DataFrame() if fetched_posts_df is None else fetched_posts_df
324
+ fetched_mentions_df, _ = fetch_linkedin_mentions_data_from_bubble(org_urn, BUBBLE_MENTIONS_TABLE_NAME)
325
+ token_state["bubble_mentions_df"] = pd.DataFrame() if fetched_mentions_df is None else fetched_mentions_df
326
+ logging.info("Refreshed posts and mentions DataFrames in state from Bubble after sync.")
327
+ except Exception as e:
328
+ logging.error(f"Error re-fetching data from Bubble post-sync: {e}")
329
+
330
+ final_message = f"<p style='color:green; text-align:center;'>βœ… Sync Attempted. {posts_sync_message} {mentions_sync_message}</p>"
331
+ return final_message, token_state
332
+
333
+
334
+ def display_main_dashboard(token_state):
335
+ if not token_state or not token_state.get("token"):
336
+ return "❌ Access denied. No token available for dashboard."
337
+
338
+ posts_df = token_state.get("bubble_posts_df", pd.DataFrame())
339
+ posts_html = f"<h4>Recent Posts ({len(posts_df)} in Bubble):</h4>"
340
+ if not posts_df.empty:
341
+ cols_to_show_posts = [col for col in [BUBBLE_POST_DATE_COLUMN_NAME, 'text', 'sentiment'] if col in posts_df.columns] # Example columns
342
+ posts_html += posts_df[cols_to_show_posts].head().to_html(escape=True, index=False, classes="table table-striped table-sm") if cols_to_show_posts else "<p>No post data to display or columns missing.</p>"
343
+ else: posts_html += "<p>No posts loaded from Bubble.</p>"
344
+
345
+ mentions_df = token_state.get("bubble_mentions_df", pd.DataFrame())
346
+ mentions_html = f"<h4>Recent Mentions ({len(mentions_df)} in Bubble):</h4>"
347
+ if not mentions_df.empty:
348
+ # Using the exact column names as defined for Bubble upload: date, id, mention_text, organization_urn, sentiment_label
349
+ cols_to_show_mentions = [col for col in ["date", "mention_text", "sentiment_label"] if col in mentions_df.columns]
350
+ mentions_html += mentions_df[cols_to_show_mentions].head().to_html(escape=True, index=False, classes="table table-striped table-sm") if cols_to_show_mentions else "<p>No mention data to display or columns missing.</p>"
351
+ else: mentions_html += "<p>No mentions loaded from Bubble.</p>"
352
+
353
+ return f"<div style='padding:10px;'><h3>Dashboard Overview</h3>{posts_html}<hr/>{mentions_html}</div>"
354
 
355
 
356
  def guarded_fetch_analytics(token_state):
357
  if not token_state or not token_state.get("token"):
358
+ return ("❌ Access denied. No token.", None, None, None, None, None, None, None)
359
+ return fetch_and_render_analytics(token_state.get("client_id"), token_state.get("token"), token_state.get("org_urn"))
 
360
 
361
+
362
+ def run_mentions_tab_display(token_state):
363
+ logging.info("Updating Mentions Tab display.")
364
  if not token_state or not token_state.get("token"):
365
  return ("❌ Access denied. No token available for mentions.", None)
366
+
367
+ mentions_df = token_state.get("bubble_mentions_df", pd.DataFrame())
368
+ if mentions_df.empty:
369
+ return ("<p style='text-align:center;'>No mentions data in Bubble. Try syncing.</p>", None)
370
+
371
+ html_parts = ["<h3 style='text-align:center;'>Recent Mentions</h3>"]
372
+ # Columns expected from Bubble: date, id, mention_text, organization_urn, sentiment_label
373
+ display_columns = [col for col in ["date", "mention_text", "sentiment_label", "id"] if col in mentions_df.columns]
374
+
375
+ if not display_columns:
376
+ html_parts.append("<p>Required columns for mentions display are missing from Bubble data.</p>")
377
+ else:
378
+ mentions_df_sorted = mentions_df.sort_values(by="date", ascending=False, errors='coerce') if "date" in display_columns else mentions_df
379
+ html_parts.append(mentions_df_sorted[display_columns].head(10).to_html(escape=True, index=False, classes="table table-sm"))
380
+
381
+ mentions_html_output = "\n".join(html_parts)
382
+ fig = None
383
+ if not mentions_df.empty and "sentiment_label" in mentions_df.columns:
384
+ try:
385
+ import matplotlib.pyplot as plt
386
+ import io, base64
387
+ plt.switch_backend('Agg') # Ensure non-interactive backend for server use
388
+ fig_plot, ax = plt.subplots(figsize=(6,4))
389
+ sentiment_counts = mentions_df["sentiment_label"].value_counts()
390
+ sentiment_counts.plot(kind='bar', ax=ax)
391
+ ax.set_title("Mention Sentiment Distribution")
392
+ ax.set_ylabel("Count")
393
+ plt.xticks(rotation=45, ha='right')
394
+ plt.tight_layout()
395
+ fig = fig_plot # Return the figure object for Gradio plot component
396
+ except Exception as e:
397
+ logging.error(f"Error generating mentions plot: {e}"); fig = None
398
+ return mentions_html_output, fig
399
+
400
 
401
  # --- Gradio UI Blocks ---
402
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
403
+ title="LinkedIn Organization Post Viewer & Analytics") as app:
404
 
405
  token_state = gr.State(value={
406
+ "token": None, "client_id": None, "org_urn": None,
407
+ "bubble_posts_df": pd.DataFrame(), "fetch_count_for_api": 0,
408
+ "bubble_mentions_df": pd.DataFrame(), "fetch_count_for_mentions_api": 0,
409
+ "url_user_token_temp_storage": None
 
410
  })
411
 
412
  gr.Markdown("# πŸš€ LinkedIn Organization Post Viewer & Analytics")
 
 
413
  url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False)
414
  status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...")
415
  org_urn_display = gr.Textbox(label="Organization URN (from URL - Hidden)", interactive=False, visible=False)
416
 
417
  app.load(fn=get_url_user_token, inputs=None, outputs=[url_user_token_display, org_urn_display])
418
+
419
+ # Chain initial processing and dashboard display
420
+ def initial_load_sequence(url_token, org_urn_val, current_state):
421
+ status_msg, new_state, btn_update = process_and_store_bubble_token(url_token, org_urn_val, current_state)
422
+ dashboard_content = display_main_dashboard(new_state)
423
+ return status_msg, new_state, btn_update, dashboard_content
424
 
425
  with gr.Tabs():
426
  with gr.TabItem("1️⃣ Dashboard & Sync"):
427
+ gr.Markdown("System checks for existing data. Button activates if new posts/mentions need fetching.")
428
+ sync_data_btn = gr.Button("πŸ”„ Sync LinkedIn Data", variant="primary", visible=False, interactive=False)
429
+ dashboard_html_output = gr.HTML("<p style='text-align:center;'>Initializing...</p>")
 
 
 
 
 
 
 
 
 
 
430
 
431
+ # Trigger initial load when org_urn (from URL) is available
432
  org_urn_display.change(
433
+ fn=initial_load_sequence,
 
 
 
 
 
434
  inputs=[url_user_token_display, org_urn_display, token_state],
435
+ outputs=[status_box, token_state, sync_data_btn, dashboard_html_output]
436
  )
437
+ # Also allow re-processing if user token changes (e.g. manual input if that was a feature)
438
+ # url_user_token_display.change(...)
439
 
440
+ sync_data_btn.click(
441
+ fn=guarded_fetch_posts_and_mentions,
442
  inputs=[token_state],
443
+ outputs=[dashboard_html_output, token_state]
444
  ).then(
445
+ fn=process_and_store_bubble_token,
446
  inputs=[url_user_token_display, org_urn_display, token_state],
447
+ outputs=[status_box, token_state, sync_data_btn]
448
+ ).then(
449
+ fn=display_main_dashboard,
450
+ inputs=[token_state],
451
+ outputs=[dashboard_html_output]
452
  )
453
 
454
  with gr.TabItem("2️⃣ Analytics"):
 
455
  fetch_analytics_btn = gr.Button("πŸ“ˆ Fetch Follower Analytics", variant="primary")
456
+ follower_count = gr.Markdown("Waiting for token...")
457
+ with gr.Row(): follower_plot, growth_plot = gr.Plot(), gr.Plot()
458
+ with gr.Row(): eng_rate_plot = gr.Plot()
459
+ with gr.Row(): interaction_plot = gr.Plot()
460
+ with gr.Row(): eb_plot = gr.Plot()
461
+ with gr.Row(): mentions_vol_plot, mentions_sentiment_plot = gr.Plot(), gr.Plot()
 
 
 
 
 
 
 
462
  fetch_analytics_btn.click(
463
+ fn=guarded_fetch_analytics, inputs=[token_state],
 
464
  outputs=[follower_count, follower_plot, growth_plot, eng_rate_plot,
465
  interaction_plot, eb_plot, mentions_vol_plot, mentions_sentiment_plot]
466
  )
467
 
468
  with gr.TabItem("3️⃣ Mentions"):
469
+ refresh_mentions_display_btn = gr.Button("πŸ”„ Refresh Mentions Display", variant="secondary")
470
+ mentions_html = gr.HTML("Mentions data loads from Bubble after sync.")
471
+ mentions_plot = gr.Plot()
472
+ refresh_mentions_display_btn.click(
473
+ fn=run_mentions_tab_display, inputs=[token_state],
 
 
474
  outputs=[mentions_html, mentions_plot]
475
  )
476
 
477
  app.load(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
478
  gr.Timer(15.0).tick(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
479
 
 
480
  if __name__ == "__main__":
481
  if not os.environ.get("Linkedin_client_id"):
482
+ logging.warning("WARNING: 'Linkedin_client_id' env var not set.")
483
+ app.launch(server_name="0.0.0.0", server_port=7860)