GuglielmoTor commited on
Commit
f7fc39b
Β·
verified Β·
1 Parent(s): d371308

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +186 -75
app.py CHANGED
@@ -1,88 +1,195 @@
1
  # -*- coding: utf-8 -*-
2
  import gradio as gr
3
  import json
 
 
 
 
4
  # Assuming these custom modules exist in your project directory or Python path
5
  from Data_Fetching_and_Rendering import fetch_and_render_dashboard
6
  from analytics_fetch_and_rendering import fetch_and_render_analytics
7
  from mentions_dashboard import generate_mentions_dashboard
8
 
9
- from gradio_utils import get_url_user_token
 
10
 
11
- # Shared state for token received via POST
12
  token_received = {"status": False, "token": None, "client_id": None}
13
 
14
-
15
  # --- Handlers for token reception (POST) and status ---
16
  def receive_token(accessToken: str, client_id: str):
17
  """
18
  Called by a hidden POST mechanism to supply the OAuth code/token and client ID.
19
  """
20
  try:
21
- # The .replace("'", '"') is kept from your original code.
22
- # Be cautious if accessToken format can vary.
23
  token_dict = json.loads(accessToken.replace("'", '"'))
24
  except json.JSONDecodeError as e:
25
- print(f"Error decoding accessToken: {e}")
26
- token_received["status"] = False # Ensure status reflects failure
27
  token_received["token"] = None
28
- token_received["client_id"] = client_id # Keep client_id if provided
29
- return {
30
- "status": "❌ Invalid token format (POST)",
31
- "token": "",
32
- "client_id": client_id
33
- }
34
 
35
  token_received["status"] = True
36
- token_received["token"] = token_dict
37
  token_received["client_id"] = client_id
38
  print(f"Token (from POST) received successfully. Client ID: {client_id}")
39
- return {
40
- "status": "βœ… Token received (POST)",
41
- "token": token_dict.get("access_token", "Access token key missing"), # Display part of the token
42
- "client_id": client_id
43
- }
44
 
45
  def check_status():
46
- return "βœ… Token received (POST)" if token_received["status"] else "❌ Waiting for token (POST)…"
47
 
48
- def show_token(): # Shows token from POST
49
- if token_received["status"] and token_received["token"]:
50
- return token_received["token"].get("access_token", "Access token key missing")
 
 
51
  return ""
52
 
53
- def show_client(): # Shows client_id from POST
54
  return token_received["client_id"] if token_received["status"] and token_received["client_id"] else ""
55
 
56
- # --- Guarded fetch functions (using token from POST) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  def guarded_fetch_dashboard():
58
  if not token_received["status"]:
59
- return "<p style='color:red; text-align:center;'>❌ Access denied. No token (POST) available. Please send token first.</p>"
60
- # token_received["client_id"] and token_received["token"] required by fetch function
61
  html = fetch_and_render_dashboard(
62
  token_received["client_id"],
63
- token_received["token"]
64
  )
65
  return html
66
 
67
  def guarded_fetch_analytics():
68
  if not token_received["status"]:
69
  return (
70
- "<p style='color:red; text-align:center;'>❌ Access denied. No token (POST) available.</p>",
71
- None, None, None, None, None, None, None # Match number of outputs
72
  )
73
- # Assuming fetch_and_render_analytics returns 8 values
74
  count_md, plot, growth_plot, avg_post_eng_rate, interaction_metrics, eb_metrics, mentions_vol_metrics, mentions_sentiment_metrics = fetch_and_render_analytics(
75
  token_received["client_id"],
76
- token_received["token"]
77
  )
78
  return count_md, plot, growth_plot, avg_post_eng_rate, interaction_metrics, eb_metrics, mentions_vol_metrics, mentions_sentiment_metrics
79
 
80
  def run_mentions_and_load():
81
- if not token_received["status"]: # Added guard similar to other functions
82
- return ("<p style='color:red; text-align:center;'>❌ Access denied. No token (POST) available.</p>", None)
83
  html, fig = generate_mentions_dashboard(
84
  token_received["client_id"],
85
- token_received["token"]
86
  )
87
  return html, fig
88
 
@@ -90,46 +197,51 @@ def run_mentions_and_load():
90
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
91
  title="LinkedIn Post Viewer & Analytics") as app:
92
  gr.Markdown("# πŸš€ LinkedIn Organization Post Viewer & Analytics")
93
- gr.Markdown("Send your OAuth token via API call (POST), then explore dashboard and analytics. URL parameters can also be displayed.")
94
 
95
  # Hidden elements: simulate POST endpoint for OAuth token
96
- hidden_token = gr.Textbox(visible=False, elem_id="hidden_token")
97
- hidden_client = gr.Textbox(visible=False, elem_id="hidden_client_id")
98
  hidden_btn = gr.Button(visible=False, elem_id="hidden_btn")
99
 
100
  # --- Display elements ---
101
- # Textbox for the user_token from URL
102
- url_user_token_display = gr.Textbox(label="User Token (from URL)", interactive=False, placeholder="Attempting to load from URL...")
 
 
 
 
103
 
104
- status_box = gr.Textbox(label="POST Token Status", interactive=False) # Clarified label
105
- token_display = gr.Textbox(label="Access Token (from POST)", interactive=False)
106
- client_display = gr.Textbox(label="Client ID (from POST)", interactive=False)
107
 
108
- # --- Load URL parameter on app start ---
109
- # The `get_url_user_token` function will be called when the app loads.
110
- # `gr.Request` is automatically passed to `get_url_user_token`.
 
 
111
  app.load(
112
- fn=get_url_user_token,
113
- inputs=None, # No explicit Gradio inputs needed, only gr.Request
114
- outputs=[url_user_token_display]
 
 
 
 
 
 
115
  )
116
 
117
- # Wire hidden POST handler for OAuth token
118
  hidden_btn.click(
119
  fn=receive_token,
120
- inputs=[hidden_token, hidden_client],
121
- outputs=[status_box, token_display, client_display]
122
  )
123
-
124
- # Polling timer to update status and displays for the POSTed token
125
- # Initial values are set by app.load for status_box, token_display, client_display
126
- # then updated by timer ticks or hidden_btn click.
127
- # We call check_status, show_token, show_client once at load time and then via timer.
128
  app.load(fn=check_status, outputs=status_box)
129
  app.load(fn=show_token, outputs=token_display)
130
  app.load(fn=show_client, outputs=client_display)
131
 
132
- timer = gr.Timer(1.0) # Poll every 1 second
133
  timer.tick(fn=check_status, outputs=status_box)
134
  timer.tick(fn=show_token, outputs=token_display)
135
  timer.tick(fn=show_client, outputs=client_display)
@@ -139,7 +251,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
139
  with gr.TabItem("1️⃣ Dashboard"):
140
  gr.Markdown("View your organization's recent posts and their engagement statistics.")
141
  fetch_dashboard_btn = gr.Button("πŸ“Š Fetch Posts & Stats", variant="primary")
142
- dashboard_html = gr.HTML(value="<p style='text-align: center; color: #555;'>Waiting for POST token...</p>")
143
  fetch_dashboard_btn.click(
144
  fn=guarded_fetch_dashboard,
145
  inputs=[],
@@ -150,38 +262,36 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
150
  gr.Markdown("View follower count and monthly gains for your organization.")
151
  fetch_analytics_btn = gr.Button("πŸ“ˆ Fetch Follower Analytics", variant="primary")
152
 
153
- follower_count = gr.Markdown("<p style='text-align: center; color: #555;'>Waiting for POST token...</p>")
154
 
155
  with gr.Row():
156
- follower_plot = gr.Plot(visible=True) # Made visible, will be empty until data
157
- growth_rate_plot = gr.Plot(visible=True) # Made visible
158
 
159
  with gr.Row():
160
- post_eng_rate_plot = gr.Plot(visible=True) # Made visible
161
 
162
  with gr.Row():
163
- interaction_data = gr.Plot(visible=True) # Made visible
164
 
165
  with gr.Row():
166
- eb_data = gr.Plot(visible=True) # Made visible
167
 
168
  with gr.Row():
169
- mentions_vol_data = gr.Plot(visible=True) # Made visible
170
- mentions_sentiment_data = gr.Plot(visible=True) # Made visible
171
 
172
  fetch_analytics_btn.click(
173
  fn=guarded_fetch_analytics,
174
  inputs=[],
175
- outputs=[follower_count, follower_plot, growth_rate_plot, post_eng_rate_plot, interaction_data, eb_data, mentions_vol_data, mentions_sentiment_data],
176
- # Show plots after click; they might need to be initially invisible if fetch_and_render_analytics can return None for plots on error
177
- # For simplicity, keeping them visible. Handle None returns in your fetch function if necessary.
178
  )
179
 
180
  with gr.TabItem("3️⃣ Mentions"):
181
  gr.Markdown("Analyze sentiment of recent posts that mention your organization.")
182
  fetch_mentions_btn = gr.Button("🧠 Fetch Mentions & Sentiment", variant="primary")
183
- mentions_html = gr.HTML(value="<p style='text-align: center; color: #555;'>Waiting for POST token...</p>") # Added placeholder
184
- mentions_plot = gr.Plot(visible=True) # Made visible
185
  fetch_mentions_btn.click(
186
  fn=run_mentions_and_load,
187
  inputs=[],
@@ -190,7 +300,8 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
190
 
191
  # Launch the app
192
  if __name__ == "__main__":
193
- # The share=True option creates a public link. Be mindful of security.
194
- # For embedding, you'll use the server_name and server_port you configure for your hosting.
 
 
195
  app.launch(server_name="0.0.0.0", server_port=7860, share=True)
196
-
 
1
  # -*- coding: utf-8 -*-
2
  import gradio as gr
3
  import json
4
+ import requests # Added for API calls
5
+ import os # Added for environment variables
6
+ import urllib.parse # Added for URL encoding (though requests handles params well)
7
+
8
  # Assuming these custom modules exist in your project directory or Python path
9
  from Data_Fetching_and_Rendering import fetch_and_render_dashboard
10
  from analytics_fetch_and_rendering import fetch_and_render_analytics
11
  from mentions_dashboard import generate_mentions_dashboard
12
 
13
+ # Import the function from your utils file
14
+ from gradio_utils import get_url_user_token # Assuming gradio_utils.py is in the same directory
15
 
16
+ # Shared state for token received via POST or Bubble
17
  token_received = {"status": False, "token": None, "client_id": None}
18
 
 
19
  # --- Handlers for token reception (POST) and status ---
20
  def receive_token(accessToken: str, client_id: str):
21
  """
22
  Called by a hidden POST mechanism to supply the OAuth code/token and client ID.
23
  """
24
  try:
 
 
25
  token_dict = json.loads(accessToken.replace("'", '"'))
26
  except json.JSONDecodeError as e:
27
+ print(f"Error decoding accessToken (POST): {e}")
28
+ token_received["status"] = False
29
  token_received["token"] = None
30
+ token_received["client_id"] = client_id
31
+ return "❌ Invalid token format (POST)", "", client_id
 
 
 
 
32
 
33
  token_received["status"] = True
34
+ token_received["token"] = token_dict # This should be the dict like {"access_token": "value"}
35
  token_received["client_id"] = client_id
36
  print(f"Token (from POST) received successfully. Client ID: {client_id}")
37
+ # Update status box, token display, client display directly
38
+ return check_status(), show_token(), show_client()
39
+
 
 
40
 
41
  def check_status():
42
+ return "βœ… Token available" if token_received["status"] else "❌ Waiting for token…"
43
 
44
+ def show_token(): # Shows access_token if available
45
+ if token_received["status"] and token_received["token"] and isinstance(token_received["token"], dict):
46
+ return token_received["token"].get("access_token", "Access token key missing in dict")
47
+ elif token_received["status"] and token_received["token"]: # If token is a raw string (should not happen with new logic)
48
+ return str(token_received["token"]) # Fallback, but ideally token_received["token"] is always a dict if status is True
49
  return ""
50
 
51
+ def show_client():
52
  return token_received["client_id"] if token_received["status"] and token_received["client_id"] else ""
53
 
54
+ # --- Function to fetch LinkedIn Token from Bubble.io ---
55
+ def fetch_linkedin_token_from_bubble(url_user_token_str):
56
+ """
57
+ Fetches LinkedIn access token from Bubble.io API using the state value (url_user_token_str).
58
+ The token is expected in a 'Raw_text' field as a JSON string, which is then parsed.
59
+ Updates the global token_received state if successful.
60
+ Returns status messages for UI update.
61
+ """
62
+ # Initial UI states (in case of early exit or error)
63
+ current_status = check_status()
64
+ current_token_display = show_token()
65
+ current_client_display = show_client()
66
+
67
+ bubble_api_key = os.environ.get("Bubble_API")
68
+ if not bubble_api_key:
69
+ error_msg = "❌ Bubble API Error: The 'Bubble_API' environment variable is not set."
70
+ print(error_msg)
71
+ return error_msg, current_status, current_token_display, current_client_display
72
+
73
+ if not url_user_token_str or "not found" in url_user_token_str or "Could not access" in url_user_token_str:
74
+ return f"ℹ️ No valid user token from URL to query Bubble. ({url_user_token_str})", current_status, current_token_display, current_client_display
75
+
76
+ base_url = "https://app.ingaze.ai/version-test/api/1.1/obj/Linkedin_access"
77
+ constraints = [{"key": "state", "constraint_type": "equals", "value": url_user_token_str}]
78
+ params = {'constraints': json.dumps(constraints)}
79
+ headers = {"Authorization": f"Bearer {bubble_api_key}"}
80
+
81
+ bubble_api_status_msg = f"Attempting to fetch token from Bubble for state: {url_user_token_str}..."
82
+ print(bubble_api_status_msg)
83
+
84
+ response = None
85
+ try:
86
+ response = requests.get(base_url, params=params, headers=headers, timeout=15) # Increased timeout slightly
87
+ response.raise_for_status()
88
+
89
+ data = response.json()
90
+ results = data.get("response", {}).get("results", [])
91
+
92
+ if results:
93
+ raw_text_from_bubble = results[0].get("Raw_text", None)
94
+ parsed_token_dict = None
95
+
96
+ if raw_text_from_bubble and isinstance(raw_text_from_bubble, str):
97
+ try:
98
+ parsed_token_dict = json.loads(raw_text_from_bubble)
99
+ if not isinstance(parsed_token_dict, dict):
100
+ bubble_api_status_msg = (f"⚠️ Bubble API: 'Raw_text' field did not contain a valid JSON dictionary string. "
101
+ f"Content type: {type(raw_text_from_bubble)}, Value: {raw_text_from_bubble}")
102
+ print(bubble_api_status_msg)
103
+ parsed_token_dict = None
104
+ # If it is a dict, parsed_token_dict is now the token dictionary itself
105
+ except json.JSONDecodeError as e:
106
+ bubble_api_status_msg = (f"⚠️ Bubble API: Error decoding 'Raw_text' JSON string: {e}. "
107
+ f"Content: {raw_text_from_bubble}")
108
+ print(bubble_api_status_msg)
109
+ parsed_token_dict = None
110
+ elif raw_text_from_bubble: # It exists but is not a string
111
+ bubble_api_status_msg = (f"⚠️ Bubble API: 'Raw_text' field was not a string. "
112
+ f"Type: {type(raw_text_from_bubble)}, Value: {raw_text_from_bubble}")
113
+ print(bubble_api_status_msg)
114
+
115
+
116
+ if parsed_token_dict and "access_token" in parsed_token_dict:
117
+ token_received["status"] = True
118
+ token_received["token"] = parsed_token_dict # Store the entire parsed dictionary
119
+ token_received["client_id"] = f"Bubble (state: {url_user_token_str})"
120
+ bubble_api_status_msg = f"βœ… LinkedIn Token successfully fetched and parsed from Bubble 'Raw_text' for state: {url_user_token_str}"
121
+ print(bubble_api_status_msg)
122
+ elif raw_text_from_bubble and not parsed_token_dict:
123
+ # Error message already set by parsing logic if raw_text_from_bubble existed but parsing failed.
124
+ # If bubble_api_status_msg wasn't set by specific parsing errors, use a general one.
125
+ if not bubble_api_status_msg.startswith("⚠️"): # Avoid overwriting specific parsing error
126
+ bubble_api_status_msg = f"⚠️ Bubble API: 'Raw_text' found but could not be parsed into a valid token dictionary for state: {url_user_token_str}."
127
+ print(bubble_api_status_msg)
128
+ elif not raw_text_from_bubble:
129
+ bubble_api_status_msg = (f"⚠️ Bubble API: Token field ('Raw_text') "
130
+ f"not found or is null in response for state: {url_user_token_str}. Result: {results[0]}")
131
+ print(bubble_api_status_msg)
132
+ elif parsed_token_dict and "access_token" not in parsed_token_dict: # Parsed OK, but missing the crucial key
133
+ bubble_api_status_msg = (f"⚠️ Bubble API: 'access_token' key missing in parsed 'Raw_text' dictionary for state: {url_user_token_str}. Parsed: {parsed_token_dict}")
134
+ print(bubble_api_status_msg)
135
+ # If none of the above, the initial bubble_api_status_msg will be used or an error below will catch it.
136
+
137
+ else: # No results from Bubble for the given state
138
+ bubble_api_status_msg = f"❌ Bubble API: No results found for state: {url_user_token_str}"
139
+ print(bubble_api_status_msg)
140
+
141
+ except requests.exceptions.HTTPError as http_err:
142
+ error_details = response.text if response else "No response content"
143
+ bubble_api_status_msg = f"❌ Bubble API HTTP error: {http_err} - Response: {error_details}"
144
+ print(bubble_api_status_msg)
145
+ except requests.exceptions.Timeout:
146
+ bubble_api_status_msg = "❌ Bubble API Request timed out."
147
+ print(bubble_api_status_msg)
148
+ except requests.exceptions.RequestException as req_err:
149
+ bubble_api_status_msg = f"❌ Bubble API Request error: {req_err}"
150
+ print(bubble_api_status_msg)
151
+ except json.JSONDecodeError as json_err: # Error decoding the main Bubble response, not Raw_text
152
+ error_details = response.text if response else "No response content"
153
+ bubble_api_status_msg = f"❌ Bubble API main response JSON decode error: {json_err}. Response: {error_details}"
154
+ print(bubble_api_status_msg)
155
+ except Exception as e:
156
+ bubble_api_status_msg = f"❌ An unexpected error occurred while fetching from Bubble: {str(e)}"
157
+ print(bubble_api_status_msg)
158
+
159
+ # Return values to update all relevant UI components
160
+ return bubble_api_status_msg, check_status(), show_token(), show_client()
161
+
162
+
163
+ # --- Guarded fetch functions (using token from POST or Bubble) ---
164
+ # These functions expect token_received["token"] to be a dictionary
165
+ # like {"access_token": "actual_token_value", ...}
166
  def guarded_fetch_dashboard():
167
  if not token_received["status"]:
168
+ return "<p style='color:red; text-align:center;'>❌ Access denied. No token available. Please send token first or ensure URL token is valid.</p>"
 
169
  html = fetch_and_render_dashboard(
170
  token_received["client_id"],
171
+ token_received["token"]
172
  )
173
  return html
174
 
175
  def guarded_fetch_analytics():
176
  if not token_received["status"]:
177
  return (
178
+ "<p style='color:red; text-align:center;'>❌ Access denied. No token available.</p>",
179
+ None, None, None, None, None, None, None
180
  )
 
181
  count_md, plot, growth_plot, avg_post_eng_rate, interaction_metrics, eb_metrics, mentions_vol_metrics, mentions_sentiment_metrics = fetch_and_render_analytics(
182
  token_received["client_id"],
183
+ token_received["token"]
184
  )
185
  return count_md, plot, growth_plot, avg_post_eng_rate, interaction_metrics, eb_metrics, mentions_vol_metrics, mentions_sentiment_metrics
186
 
187
  def run_mentions_and_load():
188
+ if not token_received["status"]:
189
+ return ("<p style='color:red; text-align:center;'>❌ Access denied. No token available.</p>", None)
190
  html, fig = generate_mentions_dashboard(
191
  token_received["client_id"],
192
+ token_received["token"]
193
  )
194
  return html, fig
195
 
 
197
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
198
  title="LinkedIn Post Viewer & Analytics") as app:
199
  gr.Markdown("# πŸš€ LinkedIn Organization Post Viewer & Analytics")
200
+ gr.Markdown("Token can be supplied via URL parameter (for Bubble.io lookup) or hidden POST. Then explore dashboard and analytics.")
201
 
202
  # Hidden elements: simulate POST endpoint for OAuth token
203
+ hidden_token_input = gr.Textbox(visible=False, elem_id="hidden_token")
204
+ hidden_client_input = gr.Textbox(visible=False, elem_id="hidden_client_id")
205
  hidden_btn = gr.Button(visible=False, elem_id="hidden_btn")
206
 
207
  # --- Display elements ---
208
+ url_user_token_display = gr.Textbox(
209
+ label="User Token (from URL - Hidden)",
210
+ interactive=False,
211
+ placeholder="Attempting to load from URL...",
212
+ visible=False
213
+ )
214
 
215
+ bubble_status_display = gr.Textbox(label="Bubble API Call Status", interactive=False, placeholder="Waiting for URL token...")
 
 
216
 
217
+ status_box = gr.Textbox(label="Overall Token Status", interactive=False)
218
+ token_display = gr.Textbox(label="Access Token (Active)", interactive=False)
219
+ client_display = gr.Textbox(label="Client ID (Active)", interactive=False)
220
+
221
+ # --- Load URL parameter on app start & Link to Bubble Fetch ---
222
  app.load(
223
+ fn=get_url_user_token,
224
+ inputs=None,
225
+ outputs=[url_user_token_display]
226
+ )
227
+
228
+ url_user_token_display.change(
229
+ fn=fetch_linkedin_token_from_bubble,
230
+ inputs=[url_user_token_display],
231
+ outputs=[bubble_status_display, status_box, token_display, client_display]
232
  )
233
 
 
234
  hidden_btn.click(
235
  fn=receive_token,
236
+ inputs=[hidden_token_input, hidden_client_input],
237
+ outputs=[status_box, token_display, client_display]
238
  )
239
+
 
 
 
 
240
  app.load(fn=check_status, outputs=status_box)
241
  app.load(fn=show_token, outputs=token_display)
242
  app.load(fn=show_client, outputs=client_display)
243
 
244
+ timer = gr.Timer(2.0)
245
  timer.tick(fn=check_status, outputs=status_box)
246
  timer.tick(fn=show_token, outputs=token_display)
247
  timer.tick(fn=show_client, outputs=client_display)
 
251
  with gr.TabItem("1️⃣ Dashboard"):
252
  gr.Markdown("View your organization's recent posts and their engagement statistics.")
253
  fetch_dashboard_btn = gr.Button("πŸ“Š Fetch Posts & Stats", variant="primary")
254
+ dashboard_html = gr.HTML(value="<p style='text-align: center; color: #555;'>Waiting for token...</p>")
255
  fetch_dashboard_btn.click(
256
  fn=guarded_fetch_dashboard,
257
  inputs=[],
 
262
  gr.Markdown("View follower count and monthly gains for your organization.")
263
  fetch_analytics_btn = gr.Button("πŸ“ˆ Fetch Follower Analytics", variant="primary")
264
 
265
+ follower_count = gr.Markdown("<p style='text-align: center; color: #555;'>Waiting for token...</p>")
266
 
267
  with gr.Row():
268
+ follower_plot = gr.Plot(visible=True)
269
+ growth_rate_plot = gr.Plot(visible=True)
270
 
271
  with gr.Row():
272
+ post_eng_rate_plot = gr.Plot(visible=True)
273
 
274
  with gr.Row():
275
+ interaction_data = gr.Plot(visible=True)
276
 
277
  with gr.Row():
278
+ eb_data = gr.Plot(visible=True)
279
 
280
  with gr.Row():
281
+ mentions_vol_data = gr.Plot(visible=True)
282
+ mentions_sentiment_data = gr.Plot(visible=True)
283
 
284
  fetch_analytics_btn.click(
285
  fn=guarded_fetch_analytics,
286
  inputs=[],
287
+ outputs=[follower_count, follower_plot, growth_rate_plot, post_eng_rate_plot, interaction_data, eb_data, mentions_vol_data, mentions_sentiment_data]
 
 
288
  )
289
 
290
  with gr.TabItem("3️⃣ Mentions"):
291
  gr.Markdown("Analyze sentiment of recent posts that mention your organization.")
292
  fetch_mentions_btn = gr.Button("🧠 Fetch Mentions & Sentiment", variant="primary")
293
+ mentions_html = gr.HTML(value="<p style='text-align: center; color: #555;'>Waiting for token...</p>")
294
+ mentions_plot = gr.Plot(visible=True)
295
  fetch_mentions_btn.click(
296
  fn=run_mentions_and_load,
297
  inputs=[],
 
300
 
301
  # Launch the app
302
  if __name__ == "__main__":
303
+ # Ensure the 'Bubble_API' environment variable is set where this app is run.
304
+ # For local testing, you can set it in your terminal before running:
305
+ # export Bubble_API="YOUR_ACTUAL_BUBBLE_API_KEY"
306
+ # python app.py
307
  app.launch(server_name="0.0.0.0", server_port=7860, share=True)