GuglielmoTor commited on
Commit
0612e1d
Β·
verified Β·
1 Parent(s): 6f117a4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +40 -112
app.py CHANGED
@@ -1,10 +1,7 @@
1
- # app.py
2
- # -- coding: utf-8 --
3
  import gradio as gr
4
  import pandas as pd
5
  import os
6
  import logging
7
- import time # Added for simulating delay
8
 
9
  # --- Module Imports ---
10
  # Functions from your existing/provided custom modules
@@ -27,36 +24,21 @@ from ui_generators import (
27
  # Configure logging
28
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
29
 
30
- # --- Helper function to load HTML animation ---
31
- def get_sync_animation_html():
32
- """Loads the HTML content for the sync animation."""
33
- try:
34
- # Ensure this path is correct relative to where app.py is run
35
- # Make sure 'sync_animation.html' is in the same directory as app.py
36
- with open("sync_animation.html", "r", encoding="utf-8") as f:
37
- return f.read()
38
- except FileNotFoundError:
39
- logging.error("sync_animation.html not found. Please ensure it's in the same directory as app.py.")
40
- return "<p style='text-align:center; color: red;'>Animation file not found. Syncing...</p>"
41
- except Exception as e:
42
- logging.error(f"Error loading sync_animation.html: {e}")
43
- return f"<p style='text-align:center; color: red;'>Error loading animation: {e}. Syncing...</p>"
44
-
45
  # --- Guarded Analytics Fetch ---
46
  def guarded_fetch_analytics(token_state):
47
  """Guarded call to fetch_and_render_analytics, ensuring token and basic data structures."""
48
  if not token_state or not token_state.get("token"):
49
  logging.warning("Analytics fetch: Access denied. No token.")
 
50
  return ("❌ Access denied. No token.", None, None, None, None, None, None, None)
51
 
52
- # Ensure dataframes are at least empty DataFrames if not found in state
53
  posts_df_analytics = token_state.get("bubble_posts_df", pd.DataFrame())
54
  mentions_df_analytics = token_state.get("bubble_mentions_df", pd.DataFrame())
55
  follower_stats_df_analytics = token_state.get("bubble_follower_stats_df", pd.DataFrame())
56
 
57
  logging.info("Calling fetch_and_render_analytics with current token_state data.")
58
  try:
59
- # Call the actual or placeholder function
60
  return fetch_and_render_analytics(
61
  token_state.get("client_id"),
62
  token_state.get("token"),
@@ -69,108 +51,70 @@ def guarded_fetch_analytics(token_state):
69
  logging.error(f"Error in guarded_fetch_analytics calling fetch_and_render_analytics: {e}", exc_info=True)
70
  return (f"❌ Error fetching analytics: {e}", None, None, None, None, None, None, None)
71
 
72
- # --- Animation Test Function (Generator) ---
73
- def show_animation_then_simulate_processing():
74
- """
75
- Yields the animation HTML, then simulates a delay,
76
- and finally yields a completion message.
77
- """
78
- logging.info("TEST BUTTON: Yielding animation HTML.")
79
- yield get_sync_animation_html() # First update: Display the animation
80
-
81
- logging.info("TEST BUTTON: Simulating processing (server-side delay of 8 seconds).")
82
- time.sleep(8) # Server-side delay
83
-
84
- logging.info("TEST BUTTON: Simulation complete. Yielding completion message.")
85
- yield "<p style='text-align:center; color: green; font-size: 1.2em;'>βœ… Animation Test Complete!</p>" # Second update
86
-
87
 
88
  # --- Gradio UI Blocks ---
89
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
90
  title="LinkedIn Organization Dashboard") as app:
91
 
 
92
  token_state = gr.State(value={
93
  "token": None, "client_id": None, "org_urn": None,
94
  "bubble_posts_df": pd.DataFrame(), "fetch_count_for_api": 0,
95
  "bubble_mentions_df": pd.DataFrame(),
96
  "bubble_follower_stats_df": pd.DataFrame(),
97
- "bubble_operations_log_df": pd.DataFrame(),
98
- "mentions_should_sync_now": False,
99
- "fs_should_sync_now": False,
100
- "url_user_token_temp_storage": None # Not used in current logic, but kept from original
101
  })
102
 
103
  gr.Markdown("# πŸš€ LinkedIn Organization Dashboard")
104
- # These textboxes are used to capture URL parameters via app.load()
105
  url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False)
106
  status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...")
107
  org_urn_display = gr.Textbox(label="Organization URN (from URL - Hidden)", interactive=False, visible=False)
108
 
109
- # This function runs when the Gradio app loads in the browser.
110
- # It calls `get_url_user_token` which should be JavaScript or a Gradio request handler.
111
- # For it to work with JavaScript `fn=None, inputs=None, ..., js="() => { ... return [token, urn]; }"
112
- # For Python function, it needs a gr.Request input.
113
  app.load(fn=get_url_user_token, inputs=None, outputs=[url_user_token_display, org_urn_display], api_name="get_url_params", show_progress=False)
114
 
115
- def initial_load_sequence(url_token, org_urn_val, current_state, request: gr.Request): # Added request for get_url_user_token if it needs it
116
- """Handles the initial processing after URL parameters are potentially loaded."""
117
- # If get_url_user_token is pure Python and needs request, it's passed here.
118
- # If get_url_user_token was JS, url_token and org_urn_val would be populated from its output.
119
-
120
- # If url_token and org_urn_val are NOT populated by app.load (e.g. if get_url_user_token is a JS snippet that didn't run or returned null)
121
- # you might need to call get_url_user_token here again if it's a Python function that needs the request object.
122
- # However, app.load is designed to populate its outputs.
123
- # For this example, we assume url_token and org_urn_val are correctly passed from org_urn_display.change
124
- # which is triggered after app.load populates org_urn_display.
125
-
126
- logging.info(f"Initial load sequence triggered. Org URN via change: {org_urn_val}, URL Token via change: {'Present' if url_token else 'Absent'}")
127
-
128
- # Call state_manager.process_and_store_bubble_token
129
  status_msg, new_state, btn_update = process_and_store_bubble_token(url_token, org_urn_val, current_state)
130
-
131
- # Call ui_generators.display_main_dashboard
132
- dashboard_content_html = display_main_dashboard(new_state)
133
-
134
- return status_msg, new_state, btn_update, dashboard_content_html
135
 
136
  with gr.Tabs():
137
  with gr.TabItem("1️⃣ Dashboard & Sync"):
138
  gr.Markdown("System checks for existing data from Bubble. The 'Sync' button activates if new data needs to be fetched from LinkedIn based on the last sync times and data availability.")
139
-
140
- with gr.Row():
141
- sync_data_btn = gr.Button("πŸ”„ Sync LinkedIn Data", variant="primary", visible=False, interactive=False)
142
- test_animation_btn = gr.Button("πŸ§ͺ Test Animation Display", variant="secondary")
143
-
144
- # HTML component to display sync status or animation
145
- sync_status_html_output = gr.HTML("<p style='text-align:center;'>Sync status will appear here.</p>")
146
-
147
- # HTML component to display the main dashboard content
148
  dashboard_display_html = gr.HTML("<p style='text-align:center;'>Dashboard loading...</p>")
149
 
150
- # When org_urn_display changes (after app.load), trigger initial_load_sequence
151
  org_urn_display.change(
152
  fn=initial_load_sequence,
153
  inputs=[url_user_token_display, org_urn_display, token_state],
154
  outputs=[status_box, token_state, sync_data_btn, dashboard_display_html],
155
  show_progress="full"
156
  )
157
-
158
- # Original Sync Button Logic (multi-step process)
 
 
 
 
 
 
159
  sync_data_btn.click(
160
- fn=lambda: get_sync_animation_html(), # Show animation first
161
- inputs=None,
162
- outputs=[sync_status_html_output],
163
- show_progress=False
164
- ).then(
165
- fn=sync_all_linkedin_data_orchestrator, # Perform actual sync
166
  inputs=[token_state],
167
- outputs=[sync_status_html_output, token_state],
168
- show_progress=False # Animation is the progress
169
  ).then(
170
- fn=process_and_store_bubble_token, # Re-process token/state after sync
171
- inputs=[url_user_token_display, org_urn_display, token_state],
172
- outputs=[status_box, token_state, sync_data_btn],
173
- show_progress=False
174
  ).then(
175
  fn=display_main_dashboard, # Refresh dashboard display
176
  inputs=[token_state],
@@ -178,29 +122,14 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
178
  show_progress=False
179
  )
180
 
181
- # New Test Animation Button Logic (uses the generator function)
182
- test_animation_btn.click(
183
- fn=show_animation_then_simulate_processing,
184
- inputs=None,
185
- outputs=[sync_status_html_output],
186
- show_progress=False # Animation itself is the progress indicator
187
- )
188
-
189
  with gr.TabItem("2️⃣ Analytics"):
190
  fetch_analytics_btn = gr.Button("πŸ“ˆ Fetch/Refresh Full Analytics", variant="primary")
191
- follower_count_md = gr.Markdown("Analytics data will load here...") # For HTML content like follower count
192
- with gr.Row():
193
- follower_plot = gr.Plot(label="Follower Demographics")
194
- growth_plot = gr.Plot(label="Follower Growth")
195
- with gr.Row():
196
- eng_rate_plot = gr.Plot(label="Engagement Rate")
197
- with gr.Row():
198
- interaction_plot = gr.Plot(label="Post Interactions")
199
- with gr.Row():
200
- eb_plot = gr.Plot(label="Engagement Benchmark")
201
- with gr.Row():
202
- mentions_vol_plot = gr.Plot(label="Mentions Volume")
203
- mentions_sentiment_plot = gr.Plot(label="Mentions Sentiment")
204
 
205
  fetch_analytics_btn.click(
206
  fn=guarded_fetch_analytics, inputs=[token_state],
@@ -213,7 +142,6 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
213
  refresh_mentions_display_btn = gr.Button("πŸ”„ Refresh Mentions Display (from local data)", variant="secondary")
214
  mentions_html = gr.HTML("Mentions data loads from Bubble after sync. Click refresh to view current local data.")
215
  mentions_sentiment_dist_plot = gr.Plot(label="Mention Sentiment Distribution")
216
-
217
  refresh_mentions_display_btn.click(
218
  fn=run_mentions_tab_display, inputs=[token_state],
219
  outputs=[mentions_html, mentions_sentiment_dist_plot],
@@ -236,7 +164,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
236
  )
237
 
238
  if __name__ == "__main__":
239
- # Check for environment variables
240
  if not os.environ.get(LINKEDIN_CLIENT_ID_ENV_VAR):
241
  logging.warning(f"WARNING: '{LINKEDIN_CLIENT_ID_ENV_VAR}' environment variable not set. The app may not function correctly for LinkedIn API calls.")
242
  if not os.environ.get(BUBBLE_APP_NAME_ENV_VAR) or \
@@ -244,12 +172,12 @@ if __name__ == "__main__":
244
  not os.environ.get(BUBBLE_API_ENDPOINT_ENV_VAR):
245
  logging.warning("WARNING: One or more Bubble environment variables (BUBBLE_APP_NAME, BUBBLE_API_KEY_PRIVATE, BUBBLE_API_ENDPOINT) are not set. Bubble integration will fail.")
246
 
247
- # Check for Matplotlib (optional, but good for plots)
248
  try:
249
  import matplotlib
250
  logging.info(f"Matplotlib version: {matplotlib.__version__} found. Backend: {matplotlib.get_backend()}")
 
251
  except ImportError:
252
  logging.error("Matplotlib is not installed. Plots will not be generated. Please install it: pip install matplotlib")
253
 
254
  # Launch the Gradio app
255
- app.launch(server_name="0.0.0.0", server_port=7860, debug=True, share=False)
 
 
 
1
  import gradio as gr
2
  import pandas as pd
3
  import os
4
  import logging
 
5
 
6
  # --- Module Imports ---
7
  # Functions from your existing/provided custom modules
 
24
  # Configure logging
25
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  # --- Guarded Analytics Fetch ---
28
  def guarded_fetch_analytics(token_state):
29
  """Guarded call to fetch_and_render_analytics, ensuring token and basic data structures."""
30
  if not token_state or not token_state.get("token"):
31
  logging.warning("Analytics fetch: Access denied. No token.")
32
+ # Ensure the number of returned Nones matches the expected number of outputs for the plots
33
  return ("❌ Access denied. No token.", None, None, None, None, None, None, None)
34
 
35
+ # Ensure DataFrames are passed, even if empty, to avoid errors in the analytics function
36
  posts_df_analytics = token_state.get("bubble_posts_df", pd.DataFrame())
37
  mentions_df_analytics = token_state.get("bubble_mentions_df", pd.DataFrame())
38
  follower_stats_df_analytics = token_state.get("bubble_follower_stats_df", pd.DataFrame())
39
 
40
  logging.info("Calling fetch_and_render_analytics with current token_state data.")
41
  try:
 
42
  return fetch_and_render_analytics(
43
  token_state.get("client_id"),
44
  token_state.get("token"),
 
51
  logging.error(f"Error in guarded_fetch_analytics calling fetch_and_render_analytics: {e}", exc_info=True)
52
  return (f"❌ Error fetching analytics: {e}", None, None, None, None, None, None, None)
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
  # --- Gradio UI Blocks ---
56
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
57
  title="LinkedIn Organization Dashboard") as app:
58
 
59
+ # Central state for holding token, client_id, org_urn, and fetched dataframes
60
  token_state = gr.State(value={
61
  "token": None, "client_id": None, "org_urn": None,
62
  "bubble_posts_df": pd.DataFrame(), "fetch_count_for_api": 0,
63
  "bubble_mentions_df": pd.DataFrame(),
64
  "bubble_follower_stats_df": pd.DataFrame(),
65
+ "url_user_token_temp_storage": None
 
 
 
66
  })
67
 
68
  gr.Markdown("# πŸš€ LinkedIn Organization Dashboard")
69
+ # Hidden textboxes to capture URL parameters
70
  url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False)
71
  status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...")
72
  org_urn_display = gr.Textbox(label="Organization URN (from URL - Hidden)", interactive=False, visible=False)
73
 
74
+ # Load URL parameters when the Gradio app loads
 
 
 
75
  app.load(fn=get_url_user_token, inputs=None, outputs=[url_user_token_display, org_urn_display], api_name="get_url_params", show_progress=False)
76
 
77
+ # This function will run after URL params are loaded and org_urn_display changes
78
+ def initial_load_sequence(url_token, org_urn_val, current_state):
79
+ logging.info(f"Initial load sequence triggered. Org URN: {org_urn_val}, URL Token: {'Present' if url_token else 'Absent'}")
80
+ # Process token, fetch Bubble data, determine sync needs
 
 
 
 
 
 
 
 
 
 
81
  status_msg, new_state, btn_update = process_and_store_bubble_token(url_token, org_urn_val, current_state)
82
+ # Display initial dashboard content based on (potentially empty) Bubble data
83
+ dashboard_content = display_main_dashboard(new_state)
84
+ return status_msg, new_state, btn_update, dashboard_content
 
 
85
 
86
  with gr.Tabs():
87
  with gr.TabItem("1️⃣ Dashboard & Sync"):
88
  gr.Markdown("System checks for existing data from Bubble. The 'Sync' button activates if new data needs to be fetched from LinkedIn based on the last sync times and data availability.")
89
+ sync_data_btn = gr.Button("πŸ”„ Sync LinkedIn Data", variant="primary", visible=False, interactive=False)
90
+ sync_status_html_output = gr.HTML("<p style='text-align:center;'>Sync status will appear here.</p>")
 
 
 
 
 
 
 
91
  dashboard_display_html = gr.HTML("<p style='text-align:center;'>Dashboard loading...</p>")
92
 
93
+ # Chain of events for initial load:
94
  org_urn_display.change(
95
  fn=initial_load_sequence,
96
  inputs=[url_user_token_display, org_urn_display, token_state],
97
  outputs=[status_box, token_state, sync_data_btn, dashboard_display_html],
98
  show_progress="full"
99
  )
100
+ # Also trigger initial_load_sequence if url_user_token_display changes (e.g. if it loads after org_urn)
101
+ # This helps ensure it runs once both are potentially available.
102
+ # Note: `org_urn_display.change` might be sufficient if `get_url_user_token` updates both nearly simultaneously.
103
+ # Adding this for robustness, but ensure it doesn't cause unwanted multiple runs if state isn't managed carefully.
104
+ # Consider using a flag in token_state if multiple triggers become an issue.
105
+ # For now, relying on org_urn_display.change as the primary trigger post-load.
106
+
107
+ # When Sync button is clicked:
108
  sync_data_btn.click(
109
+ fn=sync_all_linkedin_data_orchestrator,
 
 
 
 
 
110
  inputs=[token_state],
111
+ outputs=[sync_status_html_output, token_state], # token_state is updated here
112
+ show_progress="full"
113
  ).then(
114
+ fn=process_and_store_bubble_token, # Re-check sync status and update button
115
+ inputs=[url_user_token_display, org_urn_display, token_state], # Pass current token_state
116
+ outputs=[status_box, token_state, sync_data_btn], # token_state updated again
117
+ show_progress=False # Typically "full" for user-initiated actions, "minimal" or False for quick updates
118
  ).then(
119
  fn=display_main_dashboard, # Refresh dashboard display
120
  inputs=[token_state],
 
122
  show_progress=False
123
  )
124
 
 
 
 
 
 
 
 
 
125
  with gr.TabItem("2️⃣ Analytics"):
126
  fetch_analytics_btn = gr.Button("πŸ“ˆ Fetch/Refresh Full Analytics", variant="primary")
127
+ follower_count_md = gr.Markdown("Analytics data will load here...")
128
+ with gr.Row(): follower_plot, growth_plot = gr.Plot(label="Follower Demographics"), gr.Plot(label="Follower Growth")
129
+ with gr.Row(): eng_rate_plot = gr.Plot(label="Engagement Rate")
130
+ with gr.Row(): interaction_plot = gr.Plot(label="Post Interactions")
131
+ with gr.Row(): eb_plot = gr.Plot(label="Engagement Benchmark")
132
+ with gr.Row(): mentions_vol_plot, mentions_sentiment_plot = gr.Plot(label="Mentions Volume"), gr.Plot(label="Mentions Sentiment")
 
 
 
 
 
 
 
133
 
134
  fetch_analytics_btn.click(
135
  fn=guarded_fetch_analytics, inputs=[token_state],
 
142
  refresh_mentions_display_btn = gr.Button("πŸ”„ Refresh Mentions Display (from local data)", variant="secondary")
143
  mentions_html = gr.HTML("Mentions data loads from Bubble after sync. Click refresh to view current local data.")
144
  mentions_sentiment_dist_plot = gr.Plot(label="Mention Sentiment Distribution")
 
145
  refresh_mentions_display_btn.click(
146
  fn=run_mentions_tab_display, inputs=[token_state],
147
  outputs=[mentions_html, mentions_sentiment_dist_plot],
 
164
  )
165
 
166
  if __name__ == "__main__":
167
+ # Check for essential environment variables
168
  if not os.environ.get(LINKEDIN_CLIENT_ID_ENV_VAR):
169
  logging.warning(f"WARNING: '{LINKEDIN_CLIENT_ID_ENV_VAR}' environment variable not set. The app may not function correctly for LinkedIn API calls.")
170
  if not os.environ.get(BUBBLE_APP_NAME_ENV_VAR) or \
 
172
  not os.environ.get(BUBBLE_API_ENDPOINT_ENV_VAR):
173
  logging.warning("WARNING: One or more Bubble environment variables (BUBBLE_APP_NAME, BUBBLE_API_KEY_PRIVATE, BUBBLE_API_ENDPOINT) are not set. Bubble integration will fail.")
174
 
 
175
  try:
176
  import matplotlib
177
  logging.info(f"Matplotlib version: {matplotlib.__version__} found. Backend: {matplotlib.get_backend()}")
178
+ # The backend is now set in ui_generators.py, which is good practice.
179
  except ImportError:
180
  logging.error("Matplotlib is not installed. Plots will not be generated. Please install it: pip install matplotlib")
181
 
182
  # Launch the Gradio app
183
+ app.launch(server_name="0.0.0.0", server_port=7860, debug=True)