Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -9,22 +9,19 @@ import time # For profiling if needed
|
|
9 |
|
10 |
# --- Module Imports ---
|
11 |
from gradio_utils import get_url_user_token
|
12 |
-
|
13 |
# Functions from newly created/refactored modules
|
14 |
from config import (
|
15 |
LINKEDIN_CLIENT_ID_ENV_VAR, BUBBLE_APP_NAME_ENV_VAR,
|
16 |
-
BUBBLE_API_KEY_PRIVATE_ENV_VAR, BUBBLE_API_ENDPOINT_ENV_VAR
|
17 |
-
)
|
18 |
from state_manager import process_and_store_bubble_token
|
19 |
from sync_logic import sync_all_linkedin_data_orchestrator
|
20 |
from ui_generators import (
|
21 |
display_main_dashboard,
|
22 |
run_mentions_tab_display,
|
23 |
run_follower_stats_tab_display,
|
24 |
-
build_analytics_tab_plot_area,
|
25 |
-
BOMB_ICON, EXPLORE_ICON, FORMULA_ICON, ACTIVE_ICON
|
26 |
)
|
27 |
-
# Corrected import for analytics_data_processing
|
28 |
from analytics_data_processing import prepare_filtered_analytics_data
|
29 |
from analytics_plot_generator import (
|
30 |
generate_posts_activity_plot,
|
@@ -45,7 +42,12 @@ from analytics_plot_generator import (
|
|
45 |
generate_content_format_breakdown_plot,
|
46 |
generate_content_topic_breakdown_plot
|
47 |
)
|
48 |
-
from formulas import PLOT_FORMULAS
|
|
|
|
|
|
|
|
|
|
|
49 |
|
50 |
# Configure logging
|
51 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(module)s - %(message)s')
|
@@ -72,27 +74,24 @@ PLOT_ID_TO_FORMULA_KEY_MAP = {
|
|
72 |
"post_frequency_cs": "post_frequency",
|
73 |
"content_format_breakdown_cs": "content_format_breakdown",
|
74 |
"content_topic_breakdown_cs": "content_topic_breakdown",
|
75 |
-
"mention_analysis_volume": "mentions_activity",
|
76 |
-
"mention_analysis_sentiment": "mention_sentiment"
|
77 |
}
|
78 |
|
79 |
-
|
80 |
# --- Analytics Tab: Plot Figure Generation Function ---
|
81 |
def update_analytics_plots_figures(token_state_value, date_filter_option, custom_start_date, custom_end_date):
|
82 |
logging.info(f"Updating analytics plot figures. Filter: {date_filter_option}, Custom Start: {custom_start_date}, Custom End: {custom_end_date}")
|
83 |
num_expected_plots = 19
|
84 |
-
|
85 |
if not token_state_value or not token_state_value.get("token"):
|
86 |
message = "❌ Accesso negato. Nessun token. Impossibile generare le analisi."
|
87 |
logging.warning(message)
|
88 |
placeholder_figs = [create_placeholder_plot(title="Accesso Negato", message="Nessun token.") for _ in range(num_expected_plots)]
|
89 |
return [message] + placeholder_figs
|
90 |
-
|
91 |
try:
|
92 |
-
(filtered_merged_posts_df,
|
93 |
-
filtered_mentions_df,
|
94 |
-
date_filtered_follower_stats_df,
|
95 |
-
raw_follower_stats_df,
|
96 |
start_dt_for_msg, end_dt_for_msg) = \
|
97 |
prepare_filtered_analytics_data(
|
98 |
token_state_value, date_filter_option, custom_start_date, custom_end_date
|
@@ -102,55 +101,64 @@ def update_analytics_plots_figures(token_state_value, date_filter_option, custom
|
|
102 |
logging.error(error_msg, exc_info=True)
|
103 |
placeholder_figs = [create_placeholder_plot(title="Errore Preparazione Dati", message=str(e)) for _ in range(num_expected_plots)]
|
104 |
return [error_msg] + placeholder_figs
|
105 |
-
|
106 |
date_column_posts = token_state_value.get("config_date_col_posts", "published_at")
|
107 |
date_column_mentions = token_state_value.get("config_date_col_mentions", "date")
|
108 |
media_type_col_name = token_state_value.get("config_media_type_col", "media_type")
|
109 |
eb_labels_col_name = token_state_value.get("config_eb_labels_col", "li_eb_label")
|
110 |
-
|
111 |
plot_figs = []
|
112 |
try:
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
136 |
message = f"📊 Analisi aggiornate per il periodo: {date_filter_option}"
|
137 |
-
if date_filter_option == "Custom Range"
|
138 |
s_display = start_dt_for_msg.strftime('%Y-%m-%d') if start_dt_for_msg else "Qualsiasi"
|
139 |
e_display = end_dt_for_msg.strftime('%Y-%m-%d') if end_dt_for_msg else "Qualsiasi"
|
140 |
message += f" (Da: {s_display} A: {e_display})"
|
141 |
-
|
142 |
final_plot_figs = []
|
143 |
for i, p_fig in enumerate(plot_figs):
|
144 |
-
if p_fig is not None and not isinstance(p_fig, str):
|
145 |
final_plot_figs.append(p_fig)
|
146 |
else:
|
147 |
-
logging.warning(f"
|
148 |
final_plot_figs.append(create_placeholder_plot(title="Errore Grafico", message="Impossibile generare questa figura."))
|
149 |
|
|
|
150 |
while len(final_plot_figs) < num_expected_plots:
|
151 |
-
logging.warning(f"
|
152 |
-
final_plot_figs.append(create_placeholder_plot(title="Grafico Mancante", message="
|
153 |
-
|
154 |
return [message] + final_plot_figs[:num_expected_plots]
|
155 |
|
156 |
except Exception as e:
|
@@ -163,7 +171,6 @@ def update_analytics_plots_figures(token_state_value, date_filter_option, custom
|
|
163 |
# --- Gradio UI Blocks ---
|
164 |
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
165 |
title="LinkedIn Organization Dashboard") as app:
|
166 |
-
|
167 |
token_state = gr.State(value={
|
168 |
"token": None, "client_id": None, "org_urn": None,
|
169 |
"bubble_posts_df": pd.DataFrame(), "bubble_post_stats_df": pd.DataFrame(),
|
@@ -171,14 +178,21 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
|
171 |
"fetch_count_for_api": 0, "url_user_token_temp_storage": None,
|
172 |
"config_date_col_posts": "published_at", "config_date_col_mentions": "date",
|
173 |
"config_date_col_followers": "date", "config_media_type_col": "media_type",
|
174 |
-
"config_eb_labels_col": "li_eb_label"
|
175 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
176 |
|
177 |
gr.Markdown("# 🚀 LinkedIn Organization Dashboard")
|
178 |
url_user_token_display = gr.Textbox(label="User Token (Nascosto)", interactive=False, visible=False)
|
179 |
status_box = gr.Textbox(label="Stato Generale Token LinkedIn", interactive=False, value="Inizializzazione...")
|
180 |
org_urn_display = gr.Textbox(label="URN Organizzazione (Nascosto)", interactive=False, visible=False)
|
181 |
-
|
182 |
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)
|
183 |
|
184 |
def initial_load_sequence(url_token, org_urn_val, current_state):
|
@@ -192,7 +206,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
|
192 |
sync_data_btn = gr.Button("🔄 Sincronizza Dati LinkedIn", variant="primary", visible=False, interactive=False)
|
193 |
sync_status_html_output = gr.HTML("<p style='text-align:center;'>Stato sincronizzazione...</p>")
|
194 |
dashboard_display_html = gr.HTML("<p style='text-align:center;'>Caricamento dashboard...</p>")
|
195 |
-
|
196 |
org_urn_display.change(
|
197 |
fn=initial_load_sequence,
|
198 |
inputs=[url_user_token_display, org_urn_display, token_state],
|
@@ -202,16 +216,15 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
|
202 |
|
203 |
with gr.TabItem("2️⃣ Analisi", id="tab_analytics"):
|
204 |
gr.Markdown("## 📈 Analisi Performance LinkedIn")
|
205 |
-
gr.Markdown("Seleziona un intervallo di date. Clicca i pulsanti per
|
206 |
-
|
207 |
analytics_status_md = gr.Markdown("Stato analisi...")
|
208 |
-
|
209 |
with gr.Row():
|
210 |
date_filter_selector = gr.Radio(
|
211 |
["Sempre", "Ultimi 7 Giorni", "Ultimi 30 Giorni", "Intervallo Personalizzato"],
|
212 |
label="Seleziona Intervallo Date", value="Sempre", scale=3
|
213 |
)
|
214 |
-
with gr.Column(scale=2):
|
215 |
custom_start_date_picker = gr.DateTime(label="Data Inizio", visible=False, include_time=False, type="datetime")
|
216 |
custom_end_date_picker = gr.DateTime(label="Data Fine", visible=False, include_time=False, type="datetime")
|
217 |
|
@@ -226,7 +239,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
|
226 |
inputs=[date_filter_selector],
|
227 |
outputs=[custom_start_date_picker, custom_end_date_picker]
|
228 |
)
|
229 |
-
|
230 |
plot_configs = [
|
231 |
{"label": "Numero di Follower nel Tempo", "id": "followers_count", "section": "Dinamiche dei Follower"},
|
232 |
{"label": "Tasso di Crescita Follower", "id": "followers_growth_rate", "section": "Dinamiche dei Follower"},
|
@@ -250,162 +263,325 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
|
250 |
]
|
251 |
assert len(plot_configs) == 19, "Mancata corrispondenza in plot_configs e grafici attesi."
|
252 |
|
253 |
-
active_panel_action_state = gr.State(None)
|
254 |
-
explored_plot_id_state = gr.State(None)
|
255 |
-
|
256 |
-
plot_ui_objects = {}
|
257 |
|
258 |
with gr.Row(equal_height=False):
|
259 |
with gr.Column(scale=8) as plots_area_col:
|
260 |
plot_ui_objects = build_analytics_tab_plot_area(plot_configs)
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
276 |
|
277 |
hypothetical_new_active_state = {"plot_id": plot_id_clicked, "type": action_type}
|
278 |
is_toggling_off = current_active_action_from_state == hypothetical_new_active_state
|
279 |
|
280 |
new_active_action_state_to_set = None
|
281 |
-
|
282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
283 |
|
284 |
if is_toggling_off:
|
285 |
new_active_action_state_to_set = None
|
286 |
-
|
287 |
-
|
288 |
logging.info(f"Chiusura pannello {action_type} per {plot_id_clicked}")
|
289 |
else:
|
290 |
new_active_action_state_to_set = hypothetical_new_active_state
|
291 |
-
action_col_visible = True
|
292 |
if action_type == "insights":
|
293 |
-
|
294 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
295 |
elif action_type == "formula":
|
|
|
296 |
formula_key = PLOT_ID_TO_FORMULA_KEY_MAP.get(plot_id_clicked)
|
|
|
297 |
if formula_key and formula_key in PLOT_FORMULAS:
|
298 |
formula_data = PLOT_FORMULAS[formula_key]
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
for step in formula_data['calculation_steps']:
|
303 |
-
|
304 |
else:
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
|
|
|
|
310 |
for cfg_item in plot_configs:
|
311 |
p_id_iter = cfg_item["id"]
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
else:
|
316 |
-
all_button_updates.append(gr.update(value=BOMB_ICON))
|
317 |
-
|
318 |
-
if new_active_action_state_to_set == {"plot_id": p_id_iter, "type": "formula"}:
|
319 |
-
all_button_updates.append(gr.update(value=ACTIVE_ICON))
|
320 |
-
else:
|
321 |
-
all_button_updates.append(gr.update(value=FORMULA_ICON))
|
322 |
else:
|
323 |
-
|
|
|
|
|
|
|
|
|
|
|
324 |
|
325 |
final_updates = [
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
|
|
|
|
|
|
|
|
|
|
330 |
|
331 |
return final_updates
|
332 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
333 |
def handle_explore_click(plot_id_clicked, current_explored_plot_id_from_state):
|
|
|
|
|
334 |
logging.info(f"Click su Esplora per: {plot_id_clicked}. Attualmente esplorato da stato: {current_explored_plot_id_from_state}")
|
335 |
-
|
336 |
if not plot_ui_objects:
|
337 |
logging.error("plot_ui_objects non popolato durante handle_explore_click.")
|
338 |
-
|
339 |
-
|
|
|
|
|
|
|
|
|
340 |
new_explored_id_to_set = None
|
341 |
is_toggling_off = (plot_id_clicked == current_explored_plot_id_from_state)
|
342 |
-
|
343 |
if is_toggling_off:
|
344 |
new_explored_id_to_set = None
|
345 |
logging.info(f"Interruzione esplorazione grafico: {plot_id_clicked}")
|
346 |
else:
|
347 |
new_explored_id_to_set = plot_id_clicked
|
348 |
logging.info(f"Esplorazione grafico: {plot_id_clicked}")
|
349 |
-
|
350 |
panel_and_button_updates = []
|
351 |
for cfg in plot_configs:
|
352 |
p_id = cfg["id"]
|
353 |
if p_id in plot_ui_objects:
|
354 |
panel_visible = not new_explored_id_to_set or (p_id == new_explored_id_to_set)
|
355 |
-
panel_and_button_updates.append(gr.update(visible=panel_visible))
|
356 |
-
|
357 |
-
if p_id == new_explored_id_to_set:
|
358 |
panel_and_button_updates.append(gr.update(value=ACTIVE_ICON))
|
359 |
else:
|
360 |
panel_and_button_updates.append(gr.update(value=EXPLORE_ICON))
|
361 |
-
else:
|
362 |
panel_and_button_updates.extend([gr.update(), gr.update()])
|
363 |
-
|
364 |
-
final_updates = [new_explored_id_to_set] + panel_and_button_updates
|
365 |
return final_updates
|
366 |
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
371 |
]
|
372 |
-
for cfg_item_action in plot_configs:
|
373 |
pid_action = cfg_item_action["id"]
|
374 |
if pid_action in plot_ui_objects:
|
375 |
-
|
376 |
-
|
377 |
-
else:
|
378 |
-
|
379 |
|
|
|
380 |
explore_buttons_outputs_list = [explored_plot_id_state]
|
381 |
for cfg_item_explore in plot_configs:
|
382 |
pid_explore = cfg_item_explore["id"]
|
383 |
if pid_explore in plot_ui_objects:
|
384 |
explore_buttons_outputs_list.append(plot_ui_objects[pid_explore]["panel_component"])
|
385 |
explore_buttons_outputs_list.append(plot_ui_objects[pid_explore]["explore_button"])
|
386 |
-
else:
|
387 |
explore_buttons_outputs_list.extend([None, None])
|
388 |
|
389 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
390 |
explore_click_inputs = [explored_plot_id_state]
|
391 |
|
|
|
392 |
for config_item in plot_configs:
|
393 |
plot_id = config_item["id"]
|
|
|
394 |
if plot_id in plot_ui_objects:
|
395 |
ui_obj = plot_ui_objects[plot_id]
|
396 |
-
|
397 |
ui_obj["bomb_button"].click(
|
398 |
-
fn=lambda current_active_val,
|
399 |
-
inputs=action_click_inputs,
|
400 |
-
outputs=
|
401 |
api_name=f"action_insights_{plot_id}"
|
402 |
)
|
|
|
403 |
ui_obj["formula_button"].click(
|
404 |
-
fn=lambda current_active_val,
|
405 |
inputs=action_click_inputs,
|
406 |
-
outputs=
|
407 |
api_name=f"action_formula_{plot_id}"
|
408 |
)
|
|
|
409 |
ui_obj["explore_button"].click(
|
410 |
fn=lambda current_explored_val, p_id=plot_id: handle_explore_click(p_id, current_explored_val),
|
411 |
inputs=explore_click_inputs,
|
@@ -414,74 +590,130 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
|
414 |
)
|
415 |
else:
|
416 |
logging.warning(f"Oggetto UI per plot_id '{plot_id}' non trovato durante il tentativo di associare i gestori di click.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
417 |
|
418 |
-
|
419 |
-
|
|
|
|
|
420 |
plot_generation_results = update_analytics_plots_figures(
|
421 |
current_token_state, date_filter_val, custom_start_val, custom_end_val
|
422 |
)
|
423 |
-
|
424 |
status_message_update = plot_generation_results[0]
|
425 |
-
generated_plot_figures = plot_generation_results[1:]
|
426 |
-
|
427 |
-
all_updates = [status_message_update]
|
428 |
-
|
|
|
429 |
for i in range(len(plot_configs)):
|
430 |
if i < len(generated_plot_figures):
|
431 |
-
all_updates.append(generated_plot_figures[i])
|
432 |
-
else:
|
433 |
all_updates.append(create_placeholder_plot("Errore Figura", f"Figura mancante per grafico {plot_configs[i]['id']}"))
|
434 |
|
435 |
-
|
436 |
-
all_updates.
|
437 |
-
|
438 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
439 |
for cfg in plot_configs:
|
440 |
pid = cfg["id"]
|
441 |
if pid in plot_ui_objects:
|
442 |
-
all_updates.append(gr.update(value=BOMB_ICON))
|
443 |
-
all_updates.append(gr.update(value=FORMULA_ICON))
|
444 |
-
all_updates.append(gr.update(value=EXPLORE_ICON))
|
445 |
-
all_updates.append(gr.update(visible=True))
|
446 |
-
else:
|
447 |
all_updates.extend([None, None, None, None])
|
448 |
|
449 |
-
all_updates.append(None)
|
450 |
-
|
|
|
451 |
return all_updates
|
452 |
|
453 |
-
|
454 |
-
|
455 |
-
|
|
|
456 |
pid_filter_sync = config_item_filter_sync["id"]
|
457 |
if pid_filter_sync in plot_ui_objects and "plot_component" in plot_ui_objects[pid_filter_sync]:
|
458 |
apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync]["plot_component"])
|
459 |
else:
|
460 |
-
apply_filter_and_sync_outputs_list.append(None)
|
461 |
|
|
|
462 |
apply_filter_and_sync_outputs_list.extend([
|
463 |
-
global_actions_column_ui,
|
464 |
-
|
465 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
466 |
])
|
467 |
-
|
468 |
-
for
|
|
|
469 |
pid_filter_sync_btns = cfg_filter_sync_btns["id"]
|
470 |
if pid_filter_sync_btns in plot_ui_objects:
|
471 |
apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync_btns]["bomb_button"])
|
472 |
apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync_btns]["formula_button"])
|
473 |
apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync_btns]["explore_button"])
|
474 |
-
apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync_btns]["panel_component"])
|
475 |
else:
|
476 |
-
apply_filter_and_sync_outputs_list.extend([None, None, None, None])
|
477 |
-
|
478 |
-
apply_filter_and_sync_outputs_list.append(explored_plot_id_state)
|
|
|
|
|
479 |
|
480 |
-
logging.info(f"Output totali per apply_filter/sync: {len(apply_filter_and_sync_outputs_list)}")
|
481 |
-
|
482 |
apply_filter_btn.click(
|
483 |
fn=refresh_all_analytics_ui_elements,
|
484 |
-
inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker],
|
485 |
outputs=apply_filter_and_sync_outputs_list,
|
486 |
show_progress="full"
|
487 |
)
|
@@ -499,35 +731,38 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
|
499 |
with gr.TabItem("4️⃣ Statistiche Follower", id="tab_follower_stats"):
|
500 |
refresh_follower_stats_btn = gr.Button("🔄 Aggiorna Visualizzazione Statistiche Follower", variant="secondary")
|
501 |
follower_stats_html = gr.HTML("Statistiche follower...")
|
502 |
-
with gr.Row():
|
503 |
fs_plot_monthly_gains = gr.Plot(label="Guadagni Mensili Follower")
|
504 |
with gr.Row():
|
505 |
fs_plot_seniority = gr.Plot(label="Follower per Anzianità (Top 10 Organici)")
|
506 |
fs_plot_industry = gr.Plot(label="Follower per Settore (Top 10 Organici)")
|
507 |
-
|
508 |
refresh_follower_stats_btn.click(
|
509 |
fn=run_follower_stats_tab_display, inputs=[token_state],
|
510 |
outputs=[follower_stats_html, fs_plot_monthly_gains, fs_plot_seniority, fs_plot_industry],
|
511 |
show_progress="full"
|
512 |
)
|
513 |
-
|
|
|
514 |
sync_event_part1 = sync_data_btn.click(
|
515 |
fn=sync_all_linkedin_data_orchestrator,
|
516 |
inputs=[token_state], outputs=[sync_status_html_output, token_state], show_progress="full"
|
517 |
)
|
518 |
-
sync_event_part2 = sync_event_part1.then(
|
519 |
-
fn=process_and_store_bubble_token,
|
520 |
inputs=[url_user_token_display, org_urn_display, token_state],
|
521 |
-
outputs=[status_box, token_state, sync_data_btn], show_progress=False
|
522 |
)
|
523 |
sync_event_part3 = sync_event_part2.then(
|
524 |
-
fn=display_main_dashboard,
|
525 |
inputs=[token_state], outputs=[dashboard_display_html], show_progress=False
|
526 |
)
|
|
|
527 |
sync_event_final = sync_event_part3.then(
|
528 |
fn=refresh_all_analytics_ui_elements,
|
529 |
-
inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker],
|
530 |
-
outputs=apply_filter_and_sync_outputs_list,
|
|
|
531 |
)
|
532 |
|
533 |
if __name__ == "__main__":
|
@@ -537,10 +772,10 @@ if __name__ == "__main__":
|
|
537 |
not os.environ.get(BUBBLE_API_KEY_PRIVATE_ENV_VAR) or \
|
538 |
not os.environ.get(BUBBLE_API_ENDPOINT_ENV_VAR):
|
539 |
logging.warning("ATTENZIONE: Variabili d'ambiente Bubble non completamente impostate.")
|
540 |
-
|
541 |
try:
|
542 |
logging.info(f"Versione Matplotlib: {matplotlib.__version__}, Backend: {matplotlib.get_backend()}")
|
543 |
-
except ImportError:
|
544 |
-
logging.
|
545 |
-
|
546 |
-
app.launch(server_name="0.0.0.0", server_port=7860, debug=True)
|
|
|
9 |
|
10 |
# --- Module Imports ---
|
11 |
from gradio_utils import get_url_user_token
|
|
|
12 |
# Functions from newly created/refactored modules
|
13 |
from config import (
|
14 |
LINKEDIN_CLIENT_ID_ENV_VAR, BUBBLE_APP_NAME_ENV_VAR,
|
15 |
+
BUBBLE_API_KEY_PRIVATE_ENV_VAR, BUBBLE_API_ENDPOINT_ENV_VAR)
|
|
|
16 |
from state_manager import process_and_store_bubble_token
|
17 |
from sync_logic import sync_all_linkedin_data_orchestrator
|
18 |
from ui_generators import (
|
19 |
display_main_dashboard,
|
20 |
run_mentions_tab_display,
|
21 |
run_follower_stats_tab_display,
|
22 |
+
build_analytics_tab_plot_area,
|
23 |
+
BOMB_ICON, EXPLORE_ICON, FORMULA_ICON, ACTIVE_ICON
|
24 |
)
|
|
|
25 |
from analytics_data_processing import prepare_filtered_analytics_data
|
26 |
from analytics_plot_generator import (
|
27 |
generate_posts_activity_plot,
|
|
|
42 |
generate_content_format_breakdown_plot,
|
43 |
generate_content_topic_breakdown_plot
|
44 |
)
|
45 |
+
from formulas import PLOT_FORMULAS
|
46 |
+
|
47 |
+
# --- NEW CHATBOT MODULE IMPORTS ---
|
48 |
+
from chatbot_prompts import get_initial_insight_and_suggestions
|
49 |
+
from chatbot_handler import generate_llm_response
|
50 |
+
# --- END NEW CHATBOT MODULE IMPORTS ---
|
51 |
|
52 |
# Configure logging
|
53 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(module)s - %(message)s')
|
|
|
74 |
"post_frequency_cs": "post_frequency",
|
75 |
"content_format_breakdown_cs": "content_format_breakdown",
|
76 |
"content_topic_breakdown_cs": "content_topic_breakdown",
|
77 |
+
"mention_analysis_volume": "mentions_activity",
|
78 |
+
"mention_analysis_sentiment": "mention_sentiment"
|
79 |
}
|
80 |
|
|
|
81 |
# --- Analytics Tab: Plot Figure Generation Function ---
|
82 |
def update_analytics_plots_figures(token_state_value, date_filter_option, custom_start_date, custom_end_date):
|
83 |
logging.info(f"Updating analytics plot figures. Filter: {date_filter_option}, Custom Start: {custom_start_date}, Custom End: {custom_end_date}")
|
84 |
num_expected_plots = 19
|
|
|
85 |
if not token_state_value or not token_state_value.get("token"):
|
86 |
message = "❌ Accesso negato. Nessun token. Impossibile generare le analisi."
|
87 |
logging.warning(message)
|
88 |
placeholder_figs = [create_placeholder_plot(title="Accesso Negato", message="Nessun token.") for _ in range(num_expected_plots)]
|
89 |
return [message] + placeholder_figs
|
|
|
90 |
try:
|
91 |
+
(filtered_merged_posts_df,
|
92 |
+
filtered_mentions_df,
|
93 |
+
date_filtered_follower_stats_df,
|
94 |
+
raw_follower_stats_df,
|
95 |
start_dt_for_msg, end_dt_for_msg) = \
|
96 |
prepare_filtered_analytics_data(
|
97 |
token_state_value, date_filter_option, custom_start_date, custom_end_date
|
|
|
101 |
logging.error(error_msg, exc_info=True)
|
102 |
placeholder_figs = [create_placeholder_plot(title="Errore Preparazione Dati", message=str(e)) for _ in range(num_expected_plots)]
|
103 |
return [error_msg] + placeholder_figs
|
104 |
+
|
105 |
date_column_posts = token_state_value.get("config_date_col_posts", "published_at")
|
106 |
date_column_mentions = token_state_value.get("config_date_col_mentions", "date")
|
107 |
media_type_col_name = token_state_value.get("config_media_type_col", "media_type")
|
108 |
eb_labels_col_name = token_state_value.get("config_eb_labels_col", "li_eb_label")
|
109 |
+
|
110 |
plot_figs = []
|
111 |
try:
|
112 |
+
# Generate plots, ensuring all 19 slots are potentially filled
|
113 |
+
plot_fns_args = [
|
114 |
+
(generate_followers_count_over_time_plot, [date_filtered_follower_stats_df, 'follower_gains_monthly']),
|
115 |
+
(generate_followers_growth_rate_plot, [date_filtered_follower_stats_df, 'follower_gains_monthly']),
|
116 |
+
(generate_followers_by_demographics_plot, [raw_follower_stats_df, 'follower_geo', "Follower per Località"]),
|
117 |
+
(generate_followers_by_demographics_plot, [raw_follower_stats_df, 'follower_function', "Follower per Ruolo"]),
|
118 |
+
(generate_followers_by_demographics_plot, [raw_follower_stats_df, 'follower_industry', "Follower per Settore"]),
|
119 |
+
(generate_followers_by_demographics_plot, [raw_follower_stats_df, 'follower_seniority', "Follower per Anzianità"]),
|
120 |
+
(generate_engagement_rate_over_time_plot, [filtered_merged_posts_df, date_column_posts]),
|
121 |
+
(generate_reach_over_time_plot, [filtered_merged_posts_df, date_column_posts]),
|
122 |
+
(generate_impressions_over_time_plot, [filtered_merged_posts_df, date_column_posts]),
|
123 |
+
(generate_likes_over_time_plot, [filtered_merged_posts_df, date_column_posts]),
|
124 |
+
(generate_clicks_over_time_plot, [filtered_merged_posts_df, date_column_posts]),
|
125 |
+
(generate_shares_over_time_plot, [filtered_merged_posts_df, date_column_posts]),
|
126 |
+
(generate_comments_over_time_plot, [filtered_merged_posts_df, date_column_posts]),
|
127 |
+
(generate_comments_sentiment_breakdown_plot, [filtered_merged_posts_df, 'comment_sentiment']),
|
128 |
+
(generate_post_frequency_plot, [filtered_merged_posts_df, date_column_posts]),
|
129 |
+
(generate_content_format_breakdown_plot, [filtered_merged_posts_df, media_type_col_name]),
|
130 |
+
(generate_content_topic_breakdown_plot, [filtered_merged_posts_df, eb_labels_col_name]),
|
131 |
+
(generate_mentions_activity_plot, [filtered_mentions_df, date_column_mentions]), # Shared for mention_analysis_volume
|
132 |
+
(generate_mention_sentiment_plot, [filtered_mentions_df]) # Shared for mention_analysis_sentiment
|
133 |
+
]
|
134 |
+
|
135 |
+
for i, (plot_fn, args) in enumerate(plot_fns_args):
|
136 |
+
try:
|
137 |
+
fig = plot_fn(*args)
|
138 |
+
plot_figs.append(fig)
|
139 |
+
except Exception as plot_e:
|
140 |
+
logging.error(f"Error generating plot for slot {i} ({plot_fn.__name__}): {plot_e}", exc_info=True)
|
141 |
+
plot_figs.append(create_placeholder_plot(title=f"Errore Grafico {i+1}", message=f"Dettaglio: {str(plot_e)[:100]}"))
|
142 |
+
|
143 |
message = f"📊 Analisi aggiornate per il periodo: {date_filter_option}"
|
144 |
+
if date_filter_option == "Intervallo Personalizzato": # Corrected from "Custom Range"
|
145 |
s_display = start_dt_for_msg.strftime('%Y-%m-%d') if start_dt_for_msg else "Qualsiasi"
|
146 |
e_display = end_dt_for_msg.strftime('%Y-%m-%d') if end_dt_for_msg else "Qualsiasi"
|
147 |
message += f" (Da: {s_display} A: {e_display})"
|
148 |
+
|
149 |
final_plot_figs = []
|
150 |
for i, p_fig in enumerate(plot_figs):
|
151 |
+
if p_fig is not None and not isinstance(p_fig, str): # Checking if it's a valid plot object
|
152 |
final_plot_figs.append(p_fig)
|
153 |
else:
|
154 |
+
logging.warning(f"Plot generation failed or unexpected type for slot {i}, using placeholder. Figure: {p_fig}")
|
155 |
final_plot_figs.append(create_placeholder_plot(title="Errore Grafico", message="Impossibile generare questa figura."))
|
156 |
|
157 |
+
# Ensure the list has exactly num_expected_plots items
|
158 |
while len(final_plot_figs) < num_expected_plots:
|
159 |
+
logging.warning(f"Adding missing plot placeholder. Expected {num_expected_plots}, got {len(final_plot_figs)}.")
|
160 |
+
final_plot_figs.append(create_placeholder_plot(title="Grafico Mancante", message="Figura non generata."))
|
161 |
+
|
162 |
return [message] + final_plot_figs[:num_expected_plots]
|
163 |
|
164 |
except Exception as e:
|
|
|
171 |
# --- Gradio UI Blocks ---
|
172 |
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
173 |
title="LinkedIn Organization Dashboard") as app:
|
|
|
174 |
token_state = gr.State(value={
|
175 |
"token": None, "client_id": None, "org_urn": None,
|
176 |
"bubble_posts_df": pd.DataFrame(), "bubble_post_stats_df": pd.DataFrame(),
|
|
|
178 |
"fetch_count_for_api": 0, "url_user_token_temp_storage": None,
|
179 |
"config_date_col_posts": "published_at", "config_date_col_mentions": "date",
|
180 |
"config_date_col_followers": "date", "config_media_type_col": "media_type",
|
181 |
+
"config_eb_labels_col": "li_eb_label"
|
182 |
})
|
183 |
+
|
184 |
+
# --- NEW CHATBOT STATES ---
|
185 |
+
# Stores chat history for each plot_id: {plot_id: [{"role": "user/assistant", "content": "..."}, ...]}
|
186 |
+
chat_histories_st = gr.State({})
|
187 |
+
# Stores the plot_id of the currently active chat, e.g., "followers_count"
|
188 |
+
current_chat_plot_id_st = gr.State(None)
|
189 |
+
# --- END NEW CHATBOT STATES ---
|
190 |
|
191 |
gr.Markdown("# 🚀 LinkedIn Organization Dashboard")
|
192 |
url_user_token_display = gr.Textbox(label="User Token (Nascosto)", interactive=False, visible=False)
|
193 |
status_box = gr.Textbox(label="Stato Generale Token LinkedIn", interactive=False, value="Inizializzazione...")
|
194 |
org_urn_display = gr.Textbox(label="URN Organizzazione (Nascosto)", interactive=False, visible=False)
|
195 |
+
|
196 |
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)
|
197 |
|
198 |
def initial_load_sequence(url_token, org_urn_val, current_state):
|
|
|
206 |
sync_data_btn = gr.Button("🔄 Sincronizza Dati LinkedIn", variant="primary", visible=False, interactive=False)
|
207 |
sync_status_html_output = gr.HTML("<p style='text-align:center;'>Stato sincronizzazione...</p>")
|
208 |
dashboard_display_html = gr.HTML("<p style='text-align:center;'>Caricamento dashboard...</p>")
|
209 |
+
|
210 |
org_urn_display.change(
|
211 |
fn=initial_load_sequence,
|
212 |
inputs=[url_user_token_display, org_urn_display, token_state],
|
|
|
216 |
|
217 |
with gr.TabItem("2️⃣ Analisi", id="tab_analytics"):
|
218 |
gr.Markdown("## 📈 Analisi Performance LinkedIn")
|
219 |
+
gr.Markdown("Seleziona un intervallo di date. Clicca i pulsanti (💣 Insights, ƒ Formula, 🧭 Esplora) su un grafico per azioni.")
|
|
|
220 |
analytics_status_md = gr.Markdown("Stato analisi...")
|
221 |
+
|
222 |
with gr.Row():
|
223 |
date_filter_selector = gr.Radio(
|
224 |
["Sempre", "Ultimi 7 Giorni", "Ultimi 30 Giorni", "Intervallo Personalizzato"],
|
225 |
label="Seleziona Intervallo Date", value="Sempre", scale=3
|
226 |
)
|
227 |
+
with gr.Column(scale=2): # Ensure this column is defined for date pickers
|
228 |
custom_start_date_picker = gr.DateTime(label="Data Inizio", visible=False, include_time=False, type="datetime")
|
229 |
custom_end_date_picker = gr.DateTime(label="Data Fine", visible=False, include_time=False, type="datetime")
|
230 |
|
|
|
239 |
inputs=[date_filter_selector],
|
240 |
outputs=[custom_start_date_picker, custom_end_date_picker]
|
241 |
)
|
242 |
+
|
243 |
plot_configs = [
|
244 |
{"label": "Numero di Follower nel Tempo", "id": "followers_count", "section": "Dinamiche dei Follower"},
|
245 |
{"label": "Tasso di Crescita Follower", "id": "followers_growth_rate", "section": "Dinamiche dei Follower"},
|
|
|
263 |
]
|
264 |
assert len(plot_configs) == 19, "Mancata corrispondenza in plot_configs e grafici attesi."
|
265 |
|
266 |
+
active_panel_action_state = gr.State(None) # Stores {"plot_id": "...", "type": "insights/formula"}
|
267 |
+
explored_plot_id_state = gr.State(None) # Stores plot_id of the currently "explored" (maximized) plot
|
268 |
+
|
269 |
+
plot_ui_objects = {} # Will be populated by build_analytics_tab_plot_area
|
270 |
|
271 |
with gr.Row(equal_height=False):
|
272 |
with gr.Column(scale=8) as plots_area_col:
|
273 |
plot_ui_objects = build_analytics_tab_plot_area(plot_configs)
|
274 |
+
|
275 |
+
# --- UPDATED GLOBAL ACTIONS COLUMN with CHATBOT ---
|
276 |
+
with gr.Column(scale=4, visible=False) as global_actions_column_ui: # This column's visibility is controlled
|
277 |
+
gr.Markdown("### 💡 Azioni Contestuali Grafico") # General title for the panel
|
278 |
+
|
279 |
+
# Chatbot components (initially hidden)
|
280 |
+
insights_chatbot_ui = gr.Chatbot(
|
281 |
+
label="Chat Insights", type="messages", height=450,
|
282 |
+
bubble_full_width=False, visible=False, show_label=False,
|
283 |
+
placeholder="L'analisi AI del grafico apparirà qui. Fai domande di approfondimento!"
|
284 |
+
)
|
285 |
+
insights_chat_input_ui = gr.Textbox(
|
286 |
+
label="La tua domanda:", placeholder="Chiedi all'AI riguardo a questo grafico...",
|
287 |
+
lines=2, visible=False, show_label=False
|
288 |
+
)
|
289 |
+
with gr.Row(visible=False) as insights_suggestions_row_ui:
|
290 |
+
insights_suggestion_1_btn = gr.Button(value="Suggerimento 1", size="sm", min_width=50)
|
291 |
+
insights_suggestion_2_btn = gr.Button(value="Suggerimento 2", size="sm", min_width=50)
|
292 |
+
insights_suggestion_3_btn = gr.Button(value="Suggerimento 3", size="sm", min_width=50)
|
293 |
+
|
294 |
+
# Formula display component (initially hidden)
|
295 |
+
formula_display_markdown_ui = gr.Markdown(
|
296 |
+
"I dettagli sulla formula/metodologia appariranno qui.", visible=False
|
297 |
+
)
|
298 |
+
# --- END UPDATED GLOBAL ACTIONS COLUMN ---
|
299 |
+
|
300 |
+
# --- Event Handler for Insights (Chatbot) and Formula Buttons ---
|
301 |
+
async def handle_panel_action(
|
302 |
+
plot_id_clicked: str,
|
303 |
+
action_type: str, # "insights" or "formula"
|
304 |
+
current_active_action_from_state: dict, # Stored active action {"plot_id": ..., "type": ...}
|
305 |
+
# token_state_val: dict, # No longer directly needed here for chat/formula text
|
306 |
+
current_chat_histories: dict, # from chat_histories_st
|
307 |
+
current_chat_plot_id: str # from current_chat_plot_id_st
|
308 |
+
):
|
309 |
+
logging.info(f"Azione '{action_type}' per grafico: {plot_id_clicked}. Attualmente attivo: {current_active_action_from_state}")
|
310 |
+
|
311 |
+
# Find the label for the clicked plot_id
|
312 |
+
clicked_plot_config = next((p for p in plot_configs if p["id"] == plot_id_clicked), None)
|
313 |
+
if not clicked_plot_config:
|
314 |
+
logging.error(f"Configurazione non trovata per plot_id {plot_id_clicked}")
|
315 |
+
# Basic error feedback if needed, though this shouldn't happen if UI is built correctly
|
316 |
+
return [gr.update(visible=False)] * 7 + [current_active_action_from_state, current_chat_plot_id, current_chat_histories] + [gr.update() for _ in range(2 * len(plot_configs))]
|
317 |
+
|
318 |
+
clicked_plot_label = clicked_plot_config["label"]
|
319 |
|
320 |
hypothetical_new_active_state = {"plot_id": plot_id_clicked, "type": action_type}
|
321 |
is_toggling_off = current_active_action_from_state == hypothetical_new_active_state
|
322 |
|
323 |
new_active_action_state_to_set = None
|
324 |
+
action_col_visible_update = gr.update(visible=True)
|
325 |
+
|
326 |
+
# Default visibility for components in the action column
|
327 |
+
insights_chatbot_visible_update = gr.update(visible=False)
|
328 |
+
insights_chat_input_visible_update = gr.update(visible=False)
|
329 |
+
insights_suggestions_row_visible_update = gr.update(visible=False)
|
330 |
+
formula_display_visible_update = gr.update(visible=False)
|
331 |
+
|
332 |
+
# Chat-specific updates
|
333 |
+
chatbot_content_update = gr.update()
|
334 |
+
suggestion_1_update = gr.update()
|
335 |
+
suggestion_2_update = gr.update()
|
336 |
+
suggestion_3_update = gr.update()
|
337 |
+
new_current_chat_plot_id = current_chat_plot_id # Preserve by default
|
338 |
+
updated_chat_histories = current_chat_histories # Preserve by default
|
339 |
+
|
340 |
+
# Formula-specific updates
|
341 |
+
formula_content_update = gr.update()
|
342 |
|
343 |
if is_toggling_off:
|
344 |
new_active_action_state_to_set = None
|
345 |
+
action_col_visible_update = gr.update(visible=False)
|
346 |
+
new_current_chat_plot_id = None # Clear active chat plot ID when panel closes
|
347 |
logging.info(f"Chiusura pannello {action_type} per {plot_id_clicked}")
|
348 |
else:
|
349 |
new_active_action_state_to_set = hypothetical_new_active_state
|
|
|
350 |
if action_type == "insights":
|
351 |
+
insights_chatbot_visible_update = gr.update(visible=True)
|
352 |
+
insights_chat_input_visible_update = gr.update(visible=True)
|
353 |
+
insights_suggestions_row_visible_update = gr.update(visible=True)
|
354 |
+
|
355 |
+
new_current_chat_plot_id = plot_id_clicked
|
356 |
+
chat_history_for_this_plot = current_chat_histories.get(plot_id_clicked, [])
|
357 |
+
|
358 |
+
if not chat_history_for_this_plot: # First time opening chat for this plot
|
359 |
+
initial_insight_msg, suggestions = get_initial_insight_and_suggestions(plot_id_clicked, clicked_plot_label)
|
360 |
+
chat_history_for_this_plot = [initial_insight_msg]
|
361 |
+
updated_chat_histories = current_chat_histories.copy()
|
362 |
+
updated_chat_histories[plot_id_clicked] = chat_history_for_this_plot
|
363 |
+
else: # History exists, get suggestions again (or could store them)
|
364 |
+
_, suggestions = get_initial_insight_and_suggestions(plot_id_clicked, clicked_plot_label)
|
365 |
+
|
366 |
+
chatbot_content_update = gr.update(value=chat_history_for_this_plot)
|
367 |
+
suggestion_1_update = gr.update(value=suggestions[0])
|
368 |
+
suggestion_2_update = gr.update(value=suggestions[1])
|
369 |
+
suggestion_3_update = gr.update(value=suggestions[2])
|
370 |
+
logging.info(f"Apertura pannello CHAT per {plot_id_clicked} ('{clicked_plot_label}')")
|
371 |
+
|
372 |
elif action_type == "formula":
|
373 |
+
formula_display_visible_update = gr.update(visible=True)
|
374 |
formula_key = PLOT_ID_TO_FORMULA_KEY_MAP.get(plot_id_clicked)
|
375 |
+
formula_text = f"**Formula/Metodologia per: {clicked_plot_label}**\n\nID Grafico: `{plot_id_clicked}`.\n\n"
|
376 |
if formula_key and formula_key in PLOT_FORMULAS:
|
377 |
formula_data = PLOT_FORMULAS[formula_key]
|
378 |
+
formula_text += f"### {formula_data['title']}\n\n"
|
379 |
+
formula_text += f"**Descrizione:**\n{formula_data['description']}\n\n"
|
380 |
+
formula_text += "**Come viene calcolato:**\n"
|
381 |
for step in formula_data['calculation_steps']:
|
382 |
+
formula_text += f"- {step}\n"
|
383 |
else:
|
384 |
+
formula_text += "(Nessuna informazione dettagliata sulla formula trovata per questo ID grafico in `formulas.py`)"
|
385 |
+
formula_content_update = gr.update(value=formula_text)
|
386 |
+
new_current_chat_plot_id = None # Clear active chat plot ID when formula panel opens
|
387 |
+
logging.info(f"Apertura pannello FORMULA per {plot_id_clicked} (mappato a {formula_key})")
|
388 |
+
|
389 |
+
# Update button icons
|
390 |
+
all_button_icon_updates = []
|
391 |
for cfg_item in plot_configs:
|
392 |
p_id_iter = cfg_item["id"]
|
393 |
+
# Insights (Bomb) button
|
394 |
+
if new_active_action_state_to_set == {"plot_id": p_id_iter, "type": "insights"}:
|
395 |
+
all_button_icon_updates.append(gr.update(value=ACTIVE_ICON))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
396 |
else:
|
397 |
+
all_button_icon_updates.append(gr.update(value=BOMB_ICON))
|
398 |
+
# Formula button
|
399 |
+
if new_active_action_state_to_set == {"plot_id": p_id_iter, "type": "formula"}:
|
400 |
+
all_button_icon_updates.append(gr.update(value=ACTIVE_ICON))
|
401 |
+
else:
|
402 |
+
all_button_icon_updates.append(gr.update(value=FORMULA_ICON))
|
403 |
|
404 |
final_updates = [
|
405 |
+
action_col_visible_update,
|
406 |
+
insights_chatbot_visible_update, chatbot_content_update,
|
407 |
+
insights_chat_input_visible_update,
|
408 |
+
insights_suggestions_row_visible_update, suggestion_1_update, suggestion_2_update, suggestion_3_update,
|
409 |
+
formula_display_visible_update, formula_content_update,
|
410 |
+
new_active_action_state_to_set, # For active_panel_action_state
|
411 |
+
new_current_chat_plot_id, # For current_chat_plot_id_st
|
412 |
+
updated_chat_histories # For chat_histories_st
|
413 |
+
] + all_button_icon_updates
|
414 |
|
415 |
return final_updates
|
416 |
|
417 |
+
# --- Event Handler for Chat Message Submission ---
|
418 |
+
async def handle_chat_message_submission(
|
419 |
+
user_message: str,
|
420 |
+
current_plot_id: str, # From current_chat_plot_id_st
|
421 |
+
chat_histories: dict, # From chat_histories_st
|
422 |
+
# token_state_val: dict # If needed for context, but LLM should get context from history
|
423 |
+
):
|
424 |
+
if not current_plot_id or not user_message.strip():
|
425 |
+
# Return current history for the plot_id if message is empty
|
426 |
+
history_for_plot = chat_histories.get(current_plot_id, [])
|
427 |
+
return history_for_plot, "", chat_histories # Chatbot, Textbox, Histories State
|
428 |
+
|
429 |
+
# Find plot label for context
|
430 |
+
plot_config = next((p for p in plot_configs if p["id"] == current_plot_id), None)
|
431 |
+
plot_label = plot_config["label"] if plot_config else "Grafico Selezionato"
|
432 |
+
|
433 |
+
history_for_plot = chat_histories.get(current_plot_id, []).copy()
|
434 |
+
history_for_plot.append({"role": "user", "content": user_message})
|
435 |
+
|
436 |
+
# Show user message immediately
|
437 |
+
yield history_for_plot, "", chat_histories # Update chatbot, clear input, keep histories
|
438 |
+
|
439 |
+
# Generate bot response
|
440 |
+
bot_response_text = await generate_llm_response(user_message, current_plot_id, plot_label, history_for_plot)
|
441 |
+
|
442 |
+
history_for_plot.append({"role": "assistant", "content": bot_response_text})
|
443 |
+
|
444 |
+
updated_chat_histories = chat_histories.copy()
|
445 |
+
updated_chat_histories[current_plot_id] = history_for_plot
|
446 |
+
|
447 |
+
yield history_for_plot, "", updated_chat_histories # Final update
|
448 |
+
|
449 |
+
# --- Event Handler for Suggested Question Click ---
|
450 |
+
async def handle_suggested_question_click(
|
451 |
+
suggestion_text: str,
|
452 |
+
current_plot_id: str, # From current_chat_plot_id_st
|
453 |
+
chat_histories: dict, # From chat_histories_st
|
454 |
+
# token_state_val: dict
|
455 |
+
):
|
456 |
+
# This will effectively call handle_chat_message_submission
|
457 |
+
# We need to ensure the output signature matches what Gradio expects for this button's .click event
|
458 |
+
# which is the same as handle_chat_message_submission's output.
|
459 |
+
# The 'yield' pattern is for streaming, if handle_chat_message_submission uses it, this should too.
|
460 |
+
|
461 |
+
# Simulate the submission process
|
462 |
+
if not current_plot_id or not suggestion_text.strip():
|
463 |
+
history_for_plot = chat_histories.get(current_plot_id, [])
|
464 |
+
return history_for_plot, "", chat_histories
|
465 |
+
|
466 |
+
plot_config = next((p for p in plot_configs if p["id"] == current_plot_id), None)
|
467 |
+
plot_label = plot_config["label"] if plot_config else "Grafico Selezionato"
|
468 |
+
|
469 |
+
history_for_plot = chat_histories.get(current_plot_id, []).copy()
|
470 |
+
history_for_plot.append({"role": "user", "content": suggestion_text})
|
471 |
+
|
472 |
+
yield history_for_plot, "", chat_histories # Update chatbot, clear input (though no input here), keep histories
|
473 |
+
|
474 |
+
bot_response_text = await generate_llm_response(suggestion_text, current_plot_id, plot_label, history_for_plot)
|
475 |
+
history_for_plot.append({"role": "assistant", "content": bot_response_text})
|
476 |
+
|
477 |
+
updated_chat_histories = chat_histories.copy()
|
478 |
+
updated_chat_histories[current_plot_id] = history_for_plot
|
479 |
+
|
480 |
+
yield history_for_plot, "", updated_chat_histories
|
481 |
+
|
482 |
+
|
483 |
+
# --- Explore button logic (remains largely the same) ---
|
484 |
def handle_explore_click(plot_id_clicked, current_explored_plot_id_from_state):
|
485 |
+
# (This function's logic for showing/hiding plot panels based on explore state)
|
486 |
+
# ... (original logic from user's code) ...
|
487 |
logging.info(f"Click su Esplora per: {plot_id_clicked}. Attualmente esplorato da stato: {current_explored_plot_id_from_state}")
|
|
|
488 |
if not plot_ui_objects:
|
489 |
logging.error("plot_ui_objects non popolato durante handle_explore_click.")
|
490 |
+
# Need to return updates for all explore buttons and panels
|
491 |
+
updates_for_missing_ui = [current_explored_plot_id_from_state]
|
492 |
+
for _ in plot_configs:
|
493 |
+
updates_for_missing_ui.extend([gr.update(), gr.update()]) # panel, button
|
494 |
+
return updates_for_missing_ui
|
495 |
+
|
496 |
new_explored_id_to_set = None
|
497 |
is_toggling_off = (plot_id_clicked == current_explored_plot_id_from_state)
|
498 |
+
|
499 |
if is_toggling_off:
|
500 |
new_explored_id_to_set = None
|
501 |
logging.info(f"Interruzione esplorazione grafico: {plot_id_clicked}")
|
502 |
else:
|
503 |
new_explored_id_to_set = plot_id_clicked
|
504 |
logging.info(f"Esplorazione grafico: {plot_id_clicked}")
|
505 |
+
|
506 |
panel_and_button_updates = []
|
507 |
for cfg in plot_configs:
|
508 |
p_id = cfg["id"]
|
509 |
if p_id in plot_ui_objects:
|
510 |
panel_visible = not new_explored_id_to_set or (p_id == new_explored_id_to_set)
|
511 |
+
panel_and_button_updates.append(gr.update(visible=panel_visible)) # Panel component
|
512 |
+
|
513 |
+
if p_id == new_explored_id_to_set: # Explored button icon
|
514 |
panel_and_button_updates.append(gr.update(value=ACTIVE_ICON))
|
515 |
else:
|
516 |
panel_and_button_updates.append(gr.update(value=EXPLORE_ICON))
|
517 |
+
else: # Should not happen if UI built correctly
|
518 |
panel_and_button_updates.extend([gr.update(), gr.update()])
|
519 |
+
|
520 |
+
final_updates = [new_explored_id_to_set] + panel_and_button_updates # state + N*(panel_update, button_update)
|
521 |
return final_updates
|
522 |
|
523 |
+
# --- Define output lists for event handlers ---
|
524 |
+
|
525 |
+
# Outputs for handle_panel_action (insights/formula clicks)
|
526 |
+
action_panel_outputs_list = [
|
527 |
+
global_actions_column_ui, # Column visibility
|
528 |
+
insights_chatbot_ui, insights_chatbot_ui, # Visibility, Content for chatbot
|
529 |
+
insights_chat_input_ui, # Visibility for chat input
|
530 |
+
insights_suggestions_row_ui, insights_suggestion_1_btn, insights_suggestion_2_btn, insights_suggestion_3_btn, # Row visibility, button content
|
531 |
+
formula_display_markdown_ui, formula_display_markdown_ui, # Visibility, Content for formula
|
532 |
+
active_panel_action_state,
|
533 |
+
current_chat_plot_id_st,
|
534 |
+
chat_histories_st
|
535 |
]
|
536 |
+
for cfg_item_action in plot_configs: # Add bomb and formula button icon updates
|
537 |
pid_action = cfg_item_action["id"]
|
538 |
if pid_action in plot_ui_objects:
|
539 |
+
action_panel_outputs_list.append(plot_ui_objects[pid_action]["bomb_button"])
|
540 |
+
action_panel_outputs_list.append(plot_ui_objects[pid_action]["formula_button"])
|
541 |
+
else: # Should not happen
|
542 |
+
action_panel_outputs_list.extend([None, None])
|
543 |
|
544 |
+
# Outputs for handle_explore_click
|
545 |
explore_buttons_outputs_list = [explored_plot_id_state]
|
546 |
for cfg_item_explore in plot_configs:
|
547 |
pid_explore = cfg_item_explore["id"]
|
548 |
if pid_explore in plot_ui_objects:
|
549 |
explore_buttons_outputs_list.append(plot_ui_objects[pid_explore]["panel_component"])
|
550 |
explore_buttons_outputs_list.append(plot_ui_objects[pid_explore]["explore_button"])
|
551 |
+
else: # Should not happen
|
552 |
explore_buttons_outputs_list.extend([None, None])
|
553 |
|
554 |
+
# Inputs for action clicks (insights/formula)
|
555 |
+
action_click_inputs = [
|
556 |
+
active_panel_action_state,
|
557 |
+
# token_state, # No longer passing full token_state if not needed by handle_panel_action
|
558 |
+
chat_histories_st,
|
559 |
+
current_chat_plot_id_st
|
560 |
+
]
|
561 |
+
# Inputs for explore clicks
|
562 |
explore_click_inputs = [explored_plot_id_state]
|
563 |
|
564 |
+
# Wire up action and explore buttons
|
565 |
for config_item in plot_configs:
|
566 |
plot_id = config_item["id"]
|
567 |
+
plot_label = config_item["label"] # Get label for context
|
568 |
if plot_id in plot_ui_objects:
|
569 |
ui_obj = plot_ui_objects[plot_id]
|
570 |
+
# Insights (Bomb) Button
|
571 |
ui_obj["bomb_button"].click(
|
572 |
+
fn=lambda current_active_val, current_chats_val, current_chat_pid, p_id=plot_id: handle_panel_action(p_id, "insights", current_active_val, current_chats_val, current_chat_pid),
|
573 |
+
inputs=action_click_inputs, # current_active_action_from_state, current_chat_histories, current_chat_plot_id
|
574 |
+
outputs=action_panel_outputs_list,
|
575 |
api_name=f"action_insights_{plot_id}"
|
576 |
)
|
577 |
+
# Formula Button
|
578 |
ui_obj["formula_button"].click(
|
579 |
+
fn=lambda current_active_val, current_chats_val, current_chat_pid, p_id=plot_id: handle_panel_action(p_id, "formula", current_active_val, current_chats_val, current_chat_pid),
|
580 |
inputs=action_click_inputs,
|
581 |
+
outputs=action_panel_outputs_list,
|
582 |
api_name=f"action_formula_{plot_id}"
|
583 |
)
|
584 |
+
# Explore Button
|
585 |
ui_obj["explore_button"].click(
|
586 |
fn=lambda current_explored_val, p_id=plot_id: handle_explore_click(p_id, current_explored_val),
|
587 |
inputs=explore_click_inputs,
|
|
|
590 |
)
|
591 |
else:
|
592 |
logging.warning(f"Oggetto UI per plot_id '{plot_id}' non trovato durante il tentativo di associare i gestori di click.")
|
593 |
+
|
594 |
+
# Wire up chat input submission
|
595 |
+
chat_submission_outputs = [insights_chatbot_ui, insights_chat_input_ui, chat_histories_st]
|
596 |
+
insights_chat_input_ui.submit(
|
597 |
+
fn=handle_chat_message_submission,
|
598 |
+
inputs=[insights_chat_input_ui, current_chat_plot_id_st, chat_histories_st], # token_state removed
|
599 |
+
outputs=chat_submission_outputs,
|
600 |
+
api_name="submit_chat_message"
|
601 |
+
)
|
602 |
+
|
603 |
+
# Wire up suggested question buttons
|
604 |
+
insights_suggestion_1_btn.click(
|
605 |
+
fn=handle_suggested_question_click,
|
606 |
+
inputs=[insights_suggestion_1_btn, current_chat_plot_id_st, chat_histories_st], # token_state removed
|
607 |
+
outputs=chat_submission_outputs, # Same outputs as direct message submission
|
608 |
+
api_name="click_suggestion_1"
|
609 |
+
)
|
610 |
+
insights_suggestion_2_btn.click(
|
611 |
+
fn=handle_suggested_question_click,
|
612 |
+
inputs=[insights_suggestion_2_btn, current_chat_plot_id_st, chat_histories_st], # token_state removed
|
613 |
+
outputs=chat_submission_outputs,
|
614 |
+
api_name="click_suggestion_2"
|
615 |
+
)
|
616 |
+
insights_suggestion_3_btn.click(
|
617 |
+
fn=handle_suggested_question_click,
|
618 |
+
inputs=[insights_suggestion_3_btn, current_chat_plot_id_st, chat_histories_st], # token_state removed
|
619 |
+
outputs=chat_submission_outputs,
|
620 |
+
api_name="click_suggestion_3"
|
621 |
+
)
|
622 |
|
623 |
+
|
624 |
+
# --- Function to refresh all analytics UI elements (plots and action panel states) ---
|
625 |
+
def refresh_all_analytics_ui_elements(current_token_state, date_filter_val, custom_start_val, custom_end_val, current_chat_histories):
|
626 |
+
logging.info("Aggiornamento di tutti gli elementi UI delle analisi e reset delle azioni/chat.")
|
627 |
plot_generation_results = update_analytics_plots_figures(
|
628 |
current_token_state, date_filter_val, custom_start_val, custom_end_val
|
629 |
)
|
|
|
630 |
status_message_update = plot_generation_results[0]
|
631 |
+
generated_plot_figures = plot_generation_results[1:]
|
632 |
+
|
633 |
+
all_updates = [status_message_update] # For analytics_status_md
|
634 |
+
|
635 |
+
# Add plot figure updates
|
636 |
for i in range(len(plot_configs)):
|
637 |
if i < len(generated_plot_figures):
|
638 |
+
all_updates.append(generated_plot_figures[i]) # Plot component
|
639 |
+
else: # Should not happen if update_analytics_plots_figures is correct
|
640 |
all_updates.append(create_placeholder_plot("Errore Figura", f"Figura mancante per grafico {plot_configs[i]['id']}"))
|
641 |
|
642 |
+
# Reset global actions column and its content
|
643 |
+
all_updates.extend([
|
644 |
+
gr.update(visible=False), # global_actions_column_ui
|
645 |
+
gr.update(value=[], visible=False), # insights_chatbot_ui (clear history, hide)
|
646 |
+
gr.update(value="", visible=False), # insights_chat_input_ui (clear text, hide)
|
647 |
+
gr.update(visible=False), # insights_suggestions_row_ui
|
648 |
+
gr.update(value="Suggerimento 1", visible=True), # insights_suggestion_1_btn (reset text, keep visible within hidden row)
|
649 |
+
gr.update(value="Suggerimento 2", visible=True), # insights_suggestion_2_btn
|
650 |
+
gr.update(value="Suggerimento 3", visible=True), # insights_suggestion_3_btn
|
651 |
+
gr.update(value="I dettagli sulla formula/metodologia appariranno qui.", visible=False), # formula_display_markdown_ui
|
652 |
+
None, # active_panel_action_state (reset to None)
|
653 |
+
None, # current_chat_plot_id_st (reset to None)
|
654 |
+
current_chat_histories, # chat_histories_st (preserve existing histories across refresh unless explicitly cleared)
|
655 |
+
# If you want to clear all chats on refresh: use `{}` instead of current_chat_histories
|
656 |
+
])
|
657 |
+
|
658 |
+
# Reset action buttons (bomb, formula, explore icons) and plot panel visibility (for explore)
|
659 |
for cfg in plot_configs:
|
660 |
pid = cfg["id"]
|
661 |
if pid in plot_ui_objects:
|
662 |
+
all_updates.append(gr.update(value=BOMB_ICON)) # bomb_button
|
663 |
+
all_updates.append(gr.update(value=FORMULA_ICON)) # formula_button
|
664 |
+
all_updates.append(gr.update(value=EXPLORE_ICON)) # explore_button
|
665 |
+
all_updates.append(gr.update(visible=True)) # panel_component (reset to default visibility)
|
666 |
+
else: # Should not happen
|
667 |
all_updates.extend([None, None, None, None])
|
668 |
|
669 |
+
all_updates.append(None) # explored_plot_id_state (reset to None)
|
670 |
+
|
671 |
+
logging.info(f"Preparati {len(all_updates)} aggiornamenti per il refresh completo delle analisi.")
|
672 |
return all_updates
|
673 |
|
674 |
+
# Define the output list for apply_filter_btn and sync events that refresh analytics
|
675 |
+
apply_filter_and_sync_outputs_list = [analytics_status_md] # Status message
|
676 |
+
# Add plot components
|
677 |
+
for config_item_filter_sync in plot_configs:
|
678 |
pid_filter_sync = config_item_filter_sync["id"]
|
679 |
if pid_filter_sync in plot_ui_objects and "plot_component" in plot_ui_objects[pid_filter_sync]:
|
680 |
apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync]["plot_component"])
|
681 |
else:
|
682 |
+
apply_filter_and_sync_outputs_list.append(None) # Placeholder if UI object not found
|
683 |
|
684 |
+
# Add updates for the global actions column and its contents + states
|
685 |
apply_filter_and_sync_outputs_list.extend([
|
686 |
+
global_actions_column_ui, # Column visibility
|
687 |
+
insights_chatbot_ui, # Chatbot content and visibility
|
688 |
+
insights_chat_input_ui, # Chat input text and visibility
|
689 |
+
insights_suggestions_row_ui, # Suggestions row visibility
|
690 |
+
insights_suggestion_1_btn, # Suggestion button 1 text/visibility
|
691 |
+
insights_suggestion_2_btn, # Suggestion button 2 text/visibility
|
692 |
+
insights_suggestion_3_btn, # Suggestion button 3 text/visibility
|
693 |
+
formula_display_markdown_ui, # Formula markdown content and visibility
|
694 |
+
active_panel_action_state, # State for active panel
|
695 |
+
current_chat_plot_id_st, # State for current chat plot ID
|
696 |
+
chat_histories_st # State for all chat histories
|
697 |
])
|
698 |
+
|
699 |
+
# Add updates for individual plot action buttons (bomb, formula, explore) and plot panels (explore visibility)
|
700 |
+
for cfg_filter_sync_btns in plot_configs:
|
701 |
pid_filter_sync_btns = cfg_filter_sync_btns["id"]
|
702 |
if pid_filter_sync_btns in plot_ui_objects:
|
703 |
apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync_btns]["bomb_button"])
|
704 |
apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync_btns]["formula_button"])
|
705 |
apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync_btns]["explore_button"])
|
706 |
+
apply_filter_and_sync_outputs_list.append(plot_ui_objects[pid_filter_sync_btns]["panel_component"]) # For explore visibility
|
707 |
else:
|
708 |
+
apply_filter_and_sync_outputs_list.extend([None, None, None, None]) # Placeholders
|
709 |
+
|
710 |
+
apply_filter_and_sync_outputs_list.append(explored_plot_id_state) # State for explored plot ID
|
711 |
+
|
712 |
+
logging.info(f"Output totali definiti per apply_filter/sync: {len(apply_filter_and_sync_outputs_list)}")
|
713 |
|
|
|
|
|
714 |
apply_filter_btn.click(
|
715 |
fn=refresh_all_analytics_ui_elements,
|
716 |
+
inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker, chat_histories_st],
|
717 |
outputs=apply_filter_and_sync_outputs_list,
|
718 |
show_progress="full"
|
719 |
)
|
|
|
731 |
with gr.TabItem("4️⃣ Statistiche Follower", id="tab_follower_stats"):
|
732 |
refresh_follower_stats_btn = gr.Button("🔄 Aggiorna Visualizzazione Statistiche Follower", variant="secondary")
|
733 |
follower_stats_html = gr.HTML("Statistiche follower...")
|
734 |
+
with gr.Row(): # Ensure plots are within rows or columns for layout
|
735 |
fs_plot_monthly_gains = gr.Plot(label="Guadagni Mensili Follower")
|
736 |
with gr.Row():
|
737 |
fs_plot_seniority = gr.Plot(label="Follower per Anzianità (Top 10 Organici)")
|
738 |
fs_plot_industry = gr.Plot(label="Follower per Settore (Top 10 Organici)")
|
739 |
+
|
740 |
refresh_follower_stats_btn.click(
|
741 |
fn=run_follower_stats_tab_display, inputs=[token_state],
|
742 |
outputs=[follower_stats_html, fs_plot_monthly_gains, fs_plot_seniority, fs_plot_industry],
|
743 |
show_progress="full"
|
744 |
)
|
745 |
+
|
746 |
+
# Sync data flow
|
747 |
sync_event_part1 = sync_data_btn.click(
|
748 |
fn=sync_all_linkedin_data_orchestrator,
|
749 |
inputs=[token_state], outputs=[sync_status_html_output, token_state], show_progress="full"
|
750 |
)
|
751 |
+
sync_event_part2 = sync_event_part1.then( # Use .then() for sequential execution
|
752 |
+
fn=process_and_store_bubble_token, # This function was defined in state_manager.py
|
753 |
inputs=[url_user_token_display, org_urn_display, token_state],
|
754 |
+
outputs=[status_box, token_state, sync_data_btn], show_progress=False # Assuming this is correct from original
|
755 |
)
|
756 |
sync_event_part3 = sync_event_part2.then(
|
757 |
+
fn=display_main_dashboard, # This function was defined in ui_generators.py
|
758 |
inputs=[token_state], outputs=[dashboard_display_html], show_progress=False
|
759 |
)
|
760 |
+
# After sync, refresh analytics tab including plots and resetting chat/formula panels
|
761 |
sync_event_final = sync_event_part3.then(
|
762 |
fn=refresh_all_analytics_ui_elements,
|
763 |
+
inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker, chat_histories_st],
|
764 |
+
outputs=apply_filter_and_sync_outputs_list, # Use the comprehensive list
|
765 |
+
show_progress="full"
|
766 |
)
|
767 |
|
768 |
if __name__ == "__main__":
|
|
|
772 |
not os.environ.get(BUBBLE_API_KEY_PRIVATE_ENV_VAR) or \
|
773 |
not os.environ.get(BUBBLE_API_ENDPOINT_ENV_VAR):
|
774 |
logging.warning("ATTENZIONE: Variabili d'ambiente Bubble non completamente impostate.")
|
775 |
+
|
776 |
try:
|
777 |
logging.info(f"Versione Matplotlib: {matplotlib.__version__}, Backend: {matplotlib.get_backend()}")
|
778 |
+
except ImportError: # Matplotlib might not be an explicit import if only used by plot generators
|
779 |
+
logging.warning("Matplotlib non trovato direttamente, ma potrebbe essere usato dai generatori di grafici.")
|
780 |
+
|
781 |
+
app.launch(server_name="0.0.0.0", server_port=7860, debug=True)
|