GuglielmoTor commited on
Commit
f9d8231
Β·
verified Β·
1 Parent(s): 2a3b22e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +74 -77
app.py CHANGED
@@ -1,9 +1,10 @@
 
1
  import gradio as gr
2
  import json
3
  import os
4
  import logging
5
  import html
6
- import pandas as pd
7
 
8
  # Import functions from your custom modules
9
  from Data_Fetching_and_Rendering import fetch_and_render_dashboard
@@ -35,69 +36,77 @@ def process_and_store_bubble_token(url_user_token, org_urn, token_state):
35
  """
36
  Processes the user token from the URL, fetches LinkedIn token from Bubble,
37
  fetches initial posts from Bubble, and updates the token state and UI accordingly.
 
38
  """
39
  logging.info(f"Processing token with URL user token: '{url_user_token}', Org URN: '{org_urn}'")
 
40
  # Initialize or copy existing state, adding bubble_posts_df
41
  new_state = token_state.copy() if token_state else {"token": None, "client_id": None, "org_urn": None, "bubble_posts_df": None}
42
- new_state.update({"token": None, "org_urn": org_urn, "bubble_posts_df": None}) # Ensure bubble_posts_df is reset/initialized
 
 
43
 
44
- # Default button state: invisible and non-interactive
45
- button_update = gr.Button(
46
- value="πŸ”„ Fetch, Analyze & Store Posts to Bubble",
47
- variant="primary",
48
- visible=False,
49
- interactive=False
50
- )
51
 
52
  client_id = os.environ.get("Linkedin_client_id")
53
  if not client_id:
54
  logging.error("CRITICAL ERROR: 'Linkedin_client_id' environment variable not set.")
55
  new_state["client_id"] = "ENV VAR MISSING"
56
- # Even if client_id is missing, we might still be able to fetch from Bubble if org_urn is present
57
- # and then decide button visibility.
58
  else:
59
  new_state["client_id"] = client_id
60
 
61
  # Attempt to fetch LinkedIn token from Bubble (related to LinkedIn API access)
62
  if url_user_token and "not found" not in url_user_token and "Could not access" not in url_user_token:
63
  logging.info(f"Attempting to fetch LinkedIn token from Bubble with user token: {url_user_token}")
64
- parsed_linkedin_token = fetch_linkedin_token_from_bubble(url_user_token)
65
- if isinstance(parsed_linkedin_token, dict) and "access_token" in parsed_linkedin_token:
66
- new_state["token"] = parsed_linkedin_token
67
- logging.info("βœ… LinkedIn Token successfully fetched from Bubble.")
68
- else:
69
- logging.warning("❌ Failed to fetch a valid LinkedIn token from Bubble.")
 
 
 
 
 
70
  else:
 
71
  logging.info("No valid URL user token provided for LinkedIn token fetch, or an error was indicated.")
72
 
73
- # Fetch posts from Bubble using org_urn, regardless of LinkedIn token status for this specific fetch
74
  current_org_urn = new_state.get("org_urn")
75
  if current_org_urn:
76
  logging.info(f"Attempting to fetch posts from Bubble for org_urn: {current_org_urn}")
77
  try:
78
  # Assuming fetch_posts_from_bubble returns a Pandas DataFrame or None
79
  df_bubble_posts = fetch_posts_from_bubble(current_org_urn)
80
- new_state["bubble_posts_df"] = df_bubble_posts
81
 
82
  if df_bubble_posts is not None and not df_bubble_posts.empty:
83
  logging.info(f"βœ… Successfully fetched {len(df_bubble_posts)} posts from Bubble. Sync button will be enabled.")
84
- button_update = gr.Button(
85
- value="πŸ”„ Fetch, Analyze & Store Posts to Bubble",
86
- variant="primary",
87
- visible=True,
88
- interactive=True
89
- )
90
  else:
91
  logging.info("ℹ️ No posts found in Bubble for this organization or DataFrame is empty. Sync button will remain hidden.")
 
92
  except Exception as e:
93
  logging.error(f"❌ Error fetching posts from Bubble: {e}")
94
- # Keep button hidden on error
95
  else:
96
  logging.warning("Org URN not available in state. Cannot fetch posts from Bubble.")
 
97
 
98
- token_status_message = check_token_status(new_state)
99
- logging.info(f"Token processing complete. Status: {token_status_message}. Button visible: {button_update.visible}")
100
- return token_status_message, new_state, button_update
 
 
 
 
 
 
101
 
102
  def guarded_fetch_posts(token_state):
103
  """
@@ -105,12 +114,12 @@ def guarded_fetch_posts(token_state):
105
  This function is guarded by token availability.
106
  """
107
  logging.info("Starting guarded_fetch_posts process.")
108
- if not token_state or not token_state.get("token"):
109
  logging.error("Access denied for guarded_fetch_posts. No LinkedIn token available.")
110
- return "<p style='color:red; text-align:center;'>❌ Access denied. LinkedIn token not available.</p>"
111
 
112
  client_id = token_state.get("client_id")
113
- token_dict = token_state.get("token")
114
  org_urn = token_state.get('org_urn')
115
 
116
  if not org_urn:
@@ -118,20 +127,25 @@ def guarded_fetch_posts(token_state):
118
  return "<p style='color:red; text-align:center;'>❌ Configuration error: Organization URN missing.</p>"
119
  if not client_id or client_id == "ENV VAR MISSING":
120
  logging.error("Client ID not found or missing in token_state for guarded_fetch_posts.")
121
- return "<p style='color:red; text-align:center;'>❌ Configuration error: LinkedIn Client ID missing.</p>"
 
 
 
 
 
122
 
123
  try:
124
- logging.info(f"Step 1: Fetching core posts for org_urn: {org_urn}")
125
  processed_raw_posts, stats_map, _ = fetch_linkedin_posts_core(client_id, token_dict, org_urn)
126
 
127
  if not processed_raw_posts:
128
- logging.info("No posts found to process after step 1.")
129
- return "<p style='color:orange; text-align:center;'>ℹ️ No new LinkedIn posts found to process.</p>"
130
 
131
  post_urns = [post["id"] for post in processed_raw_posts if post.get("id")]
132
  logging.info(f"Extracted {len(post_urns)} post URNs for further processing.")
133
 
134
- logging.info("Step 2: Fetching comments.")
135
  all_comments_data = fetch_comments(client_id, token_dict, post_urns, stats_map)
136
 
137
  logging.info("Step 3: Analyzing sentiment.")
@@ -148,26 +162,20 @@ def guarded_fetch_posts(token_state):
148
  bulk_upload_to_bubble(li_post_stats, "LI_post_stats")
149
  bulk_upload_to_bubble(li_post_comments, "LI_post_comments")
150
 
151
- logging.info("Successfully fetched and uploaded posts and comments to Bubble.")
152
- return "<p style='color:green; text-align:center;'>βœ… Posts and comments uploaded to Bubble.</p>"
153
 
154
  except ValueError as ve:
155
  logging.error(f"ValueError during LinkedIn data processing: {ve}")
156
  return f"<p style='color:red; text-align:center;'>❌ Error: {html.escape(str(ve))}</p>"
157
  except Exception as e:
158
- logging.exception("An unexpected error occurred in guarded_fetch_posts.")
159
- return "<p style='color:red; text-align:center;'>❌ An unexpected error occurred. Please check logs.</p>"
160
 
161
  def guarded_fetch_dashboard(token_state):
162
  """Fetches and renders the dashboard if token is available."""
163
  if not token_state or not token_state.get("token"):
164
  return "❌ Access denied. No token available for dashboard."
165
- # This function is not used in the current UI structure for the first tab's main content
166
- # but kept for potential future use or if it's called elsewhere.
167
- # The first tab's content is now primarily the button and its output.
168
- # If you intend to display a dashboard here *after* fetching, this would need integration.
169
- # For now, returning a placeholder or status.
170
- # return fetch_and_render_dashboard(token_state.get("client_id"), token_state.get("token"))
171
  return "<p style='text-align: center; color: #555;'>Dashboard content would load here if implemented.</p>"
172
 
173
 
@@ -195,50 +203,43 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
195
  gr.Markdown("Token is supplied via URL parameter for Bubble.io lookup. Then explore dashboard and analytics.")
196
 
197
  url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False)
198
- status_box = gr.Textbox(label="Overall Token Status", interactive=False, value="Initializing...") # Initial status
199
- org_urn_display = gr.Textbox(label="Organization URN (from URL - Hidden)", interactive=False, visible=False) # Renamed for clarity
200
 
201
- # Load user token and org URN from URL parameters
202
  app.load(fn=get_url_user_token, inputs=None, outputs=[url_user_token_display, org_urn_display])
203
 
204
  with gr.Tabs():
205
  with gr.TabItem("1️⃣ Dashboard & Sync"):
206
- gr.Markdown("View your organization's recent posts and their engagement statistics. "
207
- "Fetch new posts from LinkedIn, analyze, and store them in Bubble.")
208
 
209
- # Button is initially not visible and not interactive.
210
- # Its state will be updated by process_and_store_bubble_token
211
  sync_posts_to_bubble_btn = gr.Button(
212
- "πŸ”„ Fetch, Analyze & Store Posts to Bubble",
213
  variant="primary",
214
  visible=False,
215
  interactive=False
216
  )
217
 
218
  dashboard_html_output = gr.HTML(
219
- "<p style='text-align: center; color: #555;'>System initializing... Status and actions will appear shortly. "
220
- "If data is found in Bubble, the 'Fetch, Analyze & Store' button will become active.</p>"
221
  )
222
 
223
- # Event: When URL token or org URN is loaded/changed, process it.
224
- # This will update token_state and the sync_posts_to_bubble_btn.
225
- # Using org_urn_display.change as the primary trigger after app.load completes.
226
- # If get_url_user_token is very fast, app.load might be better, but .change is robust.
227
  org_urn_display.change(
228
  fn=process_and_store_bubble_token,
229
  inputs=[url_user_token_display, org_urn_display, token_state],
230
- outputs=[status_box, token_state, sync_posts_to_bubble_btn] # Added button to outputs
231
  )
232
- # Also trigger if url_user_token_display changes, in case org_urn loads first
233
- # but token processing depends on url_user_token_display.
234
- # This creates a dependency: if one changes, the function runs with current values of both.
235
  url_user_token_display.change(
236
  fn=process_and_store_bubble_token,
237
  inputs=[url_user_token_display, org_urn_display, token_state],
238
  outputs=[status_box, token_state, sync_posts_to_bubble_btn]
239
  )
240
 
241
- # Click handler for the sync button
242
  sync_posts_to_bubble_btn.click(
243
  fn=guarded_fetch_posts,
244
  inputs=[token_state],
@@ -246,9 +247,9 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
246
  )
247
 
248
  with gr.TabItem("2️⃣ Analytics"):
249
- gr.Markdown("View follower count and monthly gains for your organization.")
250
  fetch_analytics_btn = gr.Button("πŸ“ˆ Fetch Follower Analytics", variant="primary")
251
- follower_count = gr.Markdown("<p style='text-align: center; color: #555;'>Waiting for token...</p>")
252
 
253
  with gr.Row():
254
  follower_plot, growth_plot = gr.Plot(), gr.Plot()
@@ -269,9 +270,9 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
269
  )
270
 
271
  with gr.TabItem("3️⃣ Mentions"):
272
- gr.Markdown("Analyze sentiment of recent posts that mention your organization.")
273
  fetch_mentions_btn = gr.Button("🧠 Fetch Mentions & Sentiment", variant="primary")
274
- mentions_html = gr.HTML("<p style='text-align: center; color: #555;'>Waiting for token...</p>")
275
  mentions_plot = gr.Plot()
276
  fetch_mentions_btn.click(
277
  fn=run_mentions_and_load,
@@ -279,18 +280,14 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
279
  outputs=[mentions_html, mentions_plot]
280
  )
281
 
282
- # Initial check of token status on app load (primarily for the status_box)
283
- # The button visibility is handled by process_and_store_bubble_token
284
  app.load(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
285
- # Timer to periodically update the token status display (optional, but good for UX)
286
  gr.Timer(15.0).tick(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
287
 
288
 
289
  if __name__ == "__main__":
290
  if not os.environ.get("Linkedin_client_id"):
291
  logging.warning("WARNING: The 'Linkedin_client_id' environment variable is not set. The application may not function correctly for LinkedIn API calls.")
292
- # Ensure the app launches.
293
- # For testing, you might want share=False or specific server_name/port.
294
- # share=True is useful for public sharing via Gradio link.
295
  app.launch(server_name="0.0.0.0", server_port=7860, share=True)
296
- # app.launch(share=True) # Simpler launch for testing if specific port/host not needed
 
1
+ # -- coding: utf-8 --
2
  import gradio as gr
3
  import json
4
  import os
5
  import logging
6
  import html
7
+ import pandas as pd # Ensure pandas is imported if you're dealing with DataFrames
8
 
9
  # Import functions from your custom modules
10
  from Data_Fetching_and_Rendering import fetch_and_render_dashboard
 
36
  """
37
  Processes the user token from the URL, fetches LinkedIn token from Bubble,
38
  fetches initial posts from Bubble, and updates the token state and UI accordingly.
39
+ Returns updates for status_box, token_state, and sync_posts_to_bubble_btn.
40
  """
41
  logging.info(f"Processing token with URL user token: '{url_user_token}', Org URN: '{org_urn}'")
42
+
43
  # Initialize or copy existing state, adding bubble_posts_df
44
  new_state = token_state.copy() if token_state else {"token": None, "client_id": None, "org_urn": None, "bubble_posts_df": None}
45
+ # Ensure org_urn is updated from input, and bubble_posts_df is reset/initialized for this run.
46
+ # Token will be set later if fetched.
47
+ new_state.update({"org_urn": org_urn, "bubble_posts_df": None, "token": new_state.get("token")})
48
 
49
+ # Determine button properties - default to hidden and non-interactive
50
+ button_visible = False
51
+ button_interactive = False
 
 
 
 
52
 
53
  client_id = os.environ.get("Linkedin_client_id")
54
  if not client_id:
55
  logging.error("CRITICAL ERROR: 'Linkedin_client_id' environment variable not set.")
56
  new_state["client_id"] = "ENV VAR MISSING"
 
 
57
  else:
58
  new_state["client_id"] = client_id
59
 
60
  # Attempt to fetch LinkedIn token from Bubble (related to LinkedIn API access)
61
  if url_user_token and "not found" not in url_user_token and "Could not access" not in url_user_token:
62
  logging.info(f"Attempting to fetch LinkedIn token from Bubble with user token: {url_user_token}")
63
+ try:
64
+ parsed_linkedin_token = fetch_linkedin_token_from_bubble(url_user_token)
65
+ if isinstance(parsed_linkedin_token, dict) and "access_token" in parsed_linkedin_token:
66
+ new_state["token"] = parsed_linkedin_token # Update token in new_state
67
+ logging.info("βœ… LinkedIn Token successfully fetched from Bubble.")
68
+ else:
69
+ new_state["token"] = None # Explicitly set to None if fetch fails
70
+ logging.warning(f"❌ Failed to fetch a valid LinkedIn token from Bubble. Response: {parsed_linkedin_token}")
71
+ except Exception as e:
72
+ new_state["token"] = None # Explicitly set to None on error
73
+ logging.error(f"❌ Exception while fetching LinkedIn token from Bubble: {e}")
74
  else:
75
+ new_state["token"] = None # Ensure token is None if no valid url_user_token
76
  logging.info("No valid URL user token provided for LinkedIn token fetch, or an error was indicated.")
77
 
78
+ # Fetch posts from Bubble using org_urn
79
  current_org_urn = new_state.get("org_urn")
80
  if current_org_urn:
81
  logging.info(f"Attempting to fetch posts from Bubble for org_urn: {current_org_urn}")
82
  try:
83
  # Assuming fetch_posts_from_bubble returns a Pandas DataFrame or None
84
  df_bubble_posts = fetch_posts_from_bubble(current_org_urn)
85
+ new_state["bubble_posts_df"] = df_bubble_posts # Store DataFrame in state
86
 
87
  if df_bubble_posts is not None and not df_bubble_posts.empty:
88
  logging.info(f"βœ… Successfully fetched {len(df_bubble_posts)} posts from Bubble. Sync button will be enabled.")
89
+ button_visible = True
90
+ button_interactive = True
 
 
 
 
91
  else:
92
  logging.info("ℹ️ No posts found in Bubble for this organization or DataFrame is empty. Sync button will remain hidden.")
93
+ # button_visible and button_interactive remain False
94
  except Exception as e:
95
  logging.error(f"❌ Error fetching posts from Bubble: {e}")
96
+ # button_visible and button_interactive remain False
97
  else:
98
  logging.warning("Org URN not available in state. Cannot fetch posts from Bubble.")
99
+ # button_visible and button_interactive remain False
100
 
101
+ token_status_message = check_token_status(new_state) # Check based on potentially updated new_state["token"]
102
+
103
+ # Log the determined visibility before creating the update object
104
+ logging.info(f"Token processing complete. LinkedIn Token Status: {token_status_message}. Button visible: {button_visible}, Button interactive: {button_interactive}")
105
+
106
+ # Create a gr.update object for the button
107
+ button_component_update = gr.update(visible=button_visible, interactive=button_interactive)
108
+
109
+ return token_status_message, new_state, button_component_update
110
 
111
  def guarded_fetch_posts(token_state):
112
  """
 
114
  This function is guarded by token availability.
115
  """
116
  logging.info("Starting guarded_fetch_posts process.")
117
+ if not token_state or not token_state.get("token"): # Checks for LinkedIn token
118
  logging.error("Access denied for guarded_fetch_posts. No LinkedIn token available.")
119
+ return "<p style='color:red; text-align:center;'>❌ Access denied. LinkedIn token not available. Please ensure token is fetched via URL parameter.</p>"
120
 
121
  client_id = token_state.get("client_id")
122
+ token_dict = token_state.get("token") # This is the LinkedIn token dict
123
  org_urn = token_state.get('org_urn')
124
 
125
  if not org_urn:
 
127
  return "<p style='color:red; text-align:center;'>❌ Configuration error: Organization URN missing.</p>"
128
  if not client_id or client_id == "ENV VAR MISSING":
129
  logging.error("Client ID not found or missing in token_state for guarded_fetch_posts.")
130
+ return "<p style='color:red; text-align:center;'>❌ Configuration error: LinkedIn Client ID missing (check .env file or environment variables).</p>"
131
+
132
+ # Additional check: Ensure the button was meant to be clickable (i.e., Bubble posts were found)
133
+ # This is an indirect check, as the button's clickability should prevent this if UI works as intended.
134
+ # However, adding a check on bubble_posts_df might be redundant if the button is correctly managed.
135
+ # For now, relying on the LinkedIn token check as the primary guard for this function.
136
 
137
  try:
138
+ logging.info(f"Step 1: Fetching core posts for org_urn: {org_urn} using LinkedIn API.")
139
  processed_raw_posts, stats_map, _ = fetch_linkedin_posts_core(client_id, token_dict, org_urn)
140
 
141
  if not processed_raw_posts:
142
+ logging.info("No posts found to process via LinkedIn API after step 1.")
143
+ return "<p style='color:orange; text-align:center;'>ℹ️ No new LinkedIn posts found to process at this time.</p>"
144
 
145
  post_urns = [post["id"] for post in processed_raw_posts if post.get("id")]
146
  logging.info(f"Extracted {len(post_urns)} post URNs for further processing.")
147
 
148
+ logging.info("Step 2: Fetching comments via LinkedIn API.")
149
  all_comments_data = fetch_comments(client_id, token_dict, post_urns, stats_map)
150
 
151
  logging.info("Step 3: Analyzing sentiment.")
 
162
  bulk_upload_to_bubble(li_post_stats, "LI_post_stats")
163
  bulk_upload_to_bubble(li_post_comments, "LI_post_comments")
164
 
165
+ logging.info("Successfully fetched from LinkedIn and uploaded posts and comments to Bubble.")
166
+ return "<p style='color:green; text-align:center;'>βœ… Posts and comments from LinkedIn uploaded to Bubble.</p>"
167
 
168
  except ValueError as ve:
169
  logging.error(f"ValueError during LinkedIn data processing: {ve}")
170
  return f"<p style='color:red; text-align:center;'>❌ Error: {html.escape(str(ve))}</p>"
171
  except Exception as e:
172
+ logging.exception("An unexpected error occurred in guarded_fetch_posts.") # Logs full traceback
173
+ return "<p style='color:red; text-align:center;'>❌ An unexpected error occurred while processing LinkedIn data. Please check logs.</p>"
174
 
175
  def guarded_fetch_dashboard(token_state):
176
  """Fetches and renders the dashboard if token is available."""
177
  if not token_state or not token_state.get("token"):
178
  return "❌ Access denied. No token available for dashboard."
 
 
 
 
 
 
179
  return "<p style='text-align: center; color: #555;'>Dashboard content would load here if implemented.</p>"
180
 
181
 
 
203
  gr.Markdown("Token is supplied via URL parameter for Bubble.io lookup. Then explore dashboard and analytics.")
204
 
205
  url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False)
206
+ status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...")
207
+ org_urn_display = gr.Textbox(label="Organization URN (from URL - Hidden)", interactive=False, visible=False)
208
 
 
209
  app.load(fn=get_url_user_token, inputs=None, outputs=[url_user_token_display, org_urn_display])
210
 
211
  with gr.Tabs():
212
  with gr.TabItem("1️⃣ Dashboard & Sync"):
213
+ gr.Markdown("Fetch initial data from Bubble. If posts are found, you can choose to sync newer posts from LinkedIn.")
 
214
 
 
 
215
  sync_posts_to_bubble_btn = gr.Button(
216
+ "πŸ”„ Fetch from LinkedIn, Analyze & Store to Bubble", # Updated label for clarity
217
  variant="primary",
218
  visible=False,
219
  interactive=False
220
  )
221
 
222
  dashboard_html_output = gr.HTML(
223
+ "<p style='text-align: center; color: #555;'>System initializing... "
224
+ "Checking for existing data in Bubble. The 'Fetch from LinkedIn...' button will activate if initial data is found.</p>"
225
  )
226
 
227
+ # Combined trigger: process tokens and Bubble data once both URL params are potentially loaded.
228
+ # Using .then() to chain after initial load.
229
+ # The `process_and_store_bubble_token` will run when `org_urn_display` (which is an output of app.load)
230
+ # receives its value.
231
  org_urn_display.change(
232
  fn=process_and_store_bubble_token,
233
  inputs=[url_user_token_display, org_urn_display, token_state],
234
+ outputs=[status_box, token_state, sync_posts_to_bubble_btn]
235
  )
236
+ # Fallback if url_user_token_display changes after org_urn_display (less likely but for robustness)
 
 
237
  url_user_token_display.change(
238
  fn=process_and_store_bubble_token,
239
  inputs=[url_user_token_display, org_urn_display, token_state],
240
  outputs=[status_box, token_state, sync_posts_to_bubble_btn]
241
  )
242
 
 
243
  sync_posts_to_bubble_btn.click(
244
  fn=guarded_fetch_posts,
245
  inputs=[token_state],
 
247
  )
248
 
249
  with gr.TabItem("2️⃣ Analytics"):
250
+ gr.Markdown("View follower count and monthly gains for your organization (requires LinkedIn token).")
251
  fetch_analytics_btn = gr.Button("πŸ“ˆ Fetch Follower Analytics", variant="primary")
252
+ follower_count = gr.Markdown("<p style='text-align: center; color: #555;'>Waiting for LinkedIn token...</p>")
253
 
254
  with gr.Row():
255
  follower_plot, growth_plot = gr.Plot(), gr.Plot()
 
270
  )
271
 
272
  with gr.TabItem("3️⃣ Mentions"):
273
+ gr.Markdown("Analyze sentiment of recent posts that mention your organization (requires LinkedIn token).")
274
  fetch_mentions_btn = gr.Button("🧠 Fetch Mentions & Sentiment", variant="primary")
275
+ mentions_html = gr.HTML("<p style='text-align: center; color: #555;'>Waiting for LinkedIn token...</p>")
276
  mentions_plot = gr.Plot()
277
  fetch_mentions_btn.click(
278
  fn=run_mentions_and_load,
 
280
  outputs=[mentions_html, mentions_plot]
281
  )
282
 
283
+ # This app.load updates the status_box based on the initial token_state.
284
+ # The process_and_store_bubble_token function will provide a more definitive update soon after.
285
  app.load(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
286
+ # Timer to periodically update the LinkedIn token status display
287
  gr.Timer(15.0).tick(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
288
 
289
 
290
  if __name__ == "__main__":
291
  if not os.environ.get("Linkedin_client_id"):
292
  logging.warning("WARNING: The 'Linkedin_client_id' environment variable is not set. The application may not function correctly for LinkedIn API calls.")
 
 
 
293
  app.launch(server_name="0.0.0.0", server_port=7860, share=True)