GuglielmoTor commited on
Commit
1b37007
·
verified ·
1 Parent(s): 8c31934

Rename services/analytics_handlers.py to services/analytics_tab_module.py

Browse files
services/analytics_handlers.py DELETED
@@ -1,748 +0,0 @@
1
- # handlers/analytics_handlers.py
2
- import gradio as gr
3
- import logging
4
- import time
5
- from ui.analytics_plot_generator import update_analytics_plots_figures, create_placeholder_plot
6
- from ui.ui_generators import BOMB_ICON, EXPLORE_ICON, FORMULA_ICON, ACTIVE_ICON # Make sure these are accessible
7
- from features.chatbot.chatbot_prompts import get_initial_insight_prompt_and_suggestions
8
- from features.chatbot.chatbot_handler import generate_llm_response
9
- from config import PLOT_ID_TO_FORMULA_KEY_MAP # Ensure this is correctly imported from your config
10
- from formulas import PLOT_FORMULAS # Ensure this is correctly imported
11
-
12
- class AnalyticsHandlers:
13
- """Handles all analytics tab events and interactions."""
14
-
15
- def __init__(self, analytics_components, token_state_ref, chat_histories_st_ref,
16
- current_chat_plot_id_st_ref, plot_data_for_chatbot_st_ref,
17
- active_panel_action_state_ref, explored_plot_id_state_ref):
18
- self.components = analytics_components
19
- self.plot_configs = analytics_components['plot_configs']
20
- self.unique_ordered_sections = analytics_components['unique_ordered_sections']
21
- self.num_unique_sections = len(self.unique_ordered_sections)
22
- self.plot_ui_objects = analytics_components['plot_ui_objects'] # e.g. {'plot_id1': {'panel_component': gr.Plot, 'bomb_button': gr.Button, ...}}
23
- self.section_titles_map = analytics_components['section_titles_map'] # e.g. {'Section Name': gr.Markdown}
24
-
25
- # References to global states, these are gr.State objects themselves
26
- self.token_state = token_state_ref
27
- self.chat_histories_st = chat_histories_st_ref
28
- self.current_chat_plot_id_st = current_chat_plot_id_st_ref
29
- self.plot_data_for_chatbot_st = plot_data_for_chatbot_st_ref
30
- self.active_panel_action_state = active_panel_action_state_ref
31
- self.explored_plot_id_state = explored_plot_id_state_ref
32
-
33
- logging.info(f"AnalyticsHandlers initialized. {len(self.plot_configs)} plot configs, {self.num_unique_sections} unique sections.")
34
- if not self.plot_ui_objects:
35
- logging.warning("AnalyticsHandlers: plot_ui_objects is empty or not correctly passed.")
36
- if not self.section_titles_map:
37
- logging.warning("AnalyticsHandlers: section_titles_map is empty or not correctly passed.")
38
-
39
-
40
- def _get_graph_refresh_outputs_list(self):
41
- """Helper to construct the list of outputs for graph refresh actions."""
42
- outputs = [self.components['analytics_status_md']]
43
-
44
- # Plot components themselves
45
- for pc in self.plot_configs:
46
- plot_component = self.plot_ui_objects.get(pc["id"], {}).get("plot_component")
47
- if plot_component:
48
- outputs.append(plot_component)
49
- else:
50
- outputs.append(gr.update()) # Placeholder if not found
51
- logging.warning(f"Plot component for {pc['id']} not found in plot_ui_objects for refresh outputs.")
52
-
53
- # UI resets for action panel
54
- outputs.extend([
55
- self.components['global_actions_column_ui'],
56
- self.components['insights_chatbot_ui'], # For value reset
57
- self.components['insights_chat_input_ui'], # For value reset
58
- self.components['insights_suggestions_row_ui'],
59
- self.components['insights_suggestion_1_btn'],
60
- self.components['insights_suggestion_2_btn'],
61
- self.components['insights_suggestion_3_btn'],
62
- self.components['formula_display_markdown_ui'], # For value reset
63
- self.components['formula_close_hint_md']
64
- ])
65
-
66
- # State resets
67
- outputs.extend([
68
- self.active_panel_action_state,
69
- self.current_chat_plot_id_st,
70
- self.chat_histories_st,
71
- self.plot_data_for_chatbot_st
72
- ])
73
-
74
- # Button and panel visibility resets for each plot
75
- for pc in self.plot_configs:
76
- plot_id = pc["id"]
77
- ui_obj = self.plot_ui_objects.get(plot_id, {})
78
- outputs.extend([
79
- ui_obj.get("bomb_button", gr.update()),
80
- ui_obj.get("formula_button", gr.update()),
81
- ui_obj.get("explore_button", gr.update()),
82
- ui_obj.get("panel_component", gr.update()) # For visibility reset
83
- ])
84
-
85
- outputs.append(self.explored_plot_id_state) # Reset explored state
86
-
87
- # Section title visibility resets
88
- for s_name in self.unique_ordered_sections:
89
- outputs.append(self.section_titles_map.get(s_name, gr.update()))
90
-
91
- expected_len = 1 + len(self.plot_configs) + 9 + 4 + (4 * len(self.plot_configs)) + 1 + self.num_unique_sections
92
- # 1 (status) + N_plots (plots) + 9 (action panel UI) + 4 (states) + 4*N_plots (plot buttons/panels) + 1 (explored_id_state) + N_sections (titles)
93
- logging.debug(f"Graph refresh outputs list length: {len(outputs)}, Expected: {expected_len}")
94
- return outputs
95
-
96
- async def refresh_analytics_graphs_ui(self, current_token_state_val, date_filter_val,
97
- custom_start_val, custom_end_val):
98
- logging.info(f"Refreshing analytics graph UI. Filter: {date_filter_val}. Token set: {'yes' if current_token_state_val.get('token') else 'no'}")
99
- start_time = time.time()
100
-
101
- # Call the function that generates plot figures and summaries
102
- plot_gen_results = update_analytics_plots_figures(
103
- current_token_state_val,
104
- date_filter_val,
105
- custom_start_val,
106
- custom_end_val,
107
- self.plot_configs
108
- )
109
-
110
- # Debug: Log what we actually got back
111
- logging.debug(f"plot_gen_results type: {type(plot_gen_results)}, length: {len(plot_gen_results) if hasattr(plot_gen_results, '__len__') else 'N/A'}")
112
-
113
- # Handle different possible return formats
114
- if len(plot_gen_results) == 3:
115
- # Expected format: (status_msg, figures_list, summaries_dict)
116
- status_msg, gen_figs_result, new_summaries_dict = plot_gen_results
117
- else:
118
- # Fallback - try to parse based on old format
119
- # Old format seemed to be: status_msg, *figures, summaries_dict
120
- status_msg = plot_gen_results[0]
121
- new_summaries_dict = plot_gen_results[-1]
122
- gen_figs_result = plot_gen_results[1:-1] # Everything between first and last
123
-
124
- # Ensure we have a list of figures
125
- if isinstance(gen_figs_result, list):
126
- gen_figs_list = gen_figs_result
127
- else:
128
- # If it's a single figure, we need to check if it should be replicated
129
- # or if the function should return multiple figures
130
- logging.warning(f"Got single figure of type {type(gen_figs_result)}, expected list of {len(self.plot_configs)} figures")
131
-
132
- # Option 1: If the single figure should be used for all plots (unlikely)
133
- # gen_figs_list = [gen_figs_result] * len(self.plot_configs)
134
-
135
- # Option 2: If this indicates an error in the function (more likely)
136
- gen_figs_list = [gen_figs_result] # Keep as single item list for now
137
-
138
- all_updates = [gr.update(value=status_msg)] # For analytics_status_md
139
-
140
- # Update plot components with new figures
141
- if len(gen_figs_list) == len(self.plot_configs):
142
- # Perfect match - use figures as-is
143
- for fig in gen_figs_list:
144
- all_updates.append(fig)
145
- elif len(gen_figs_list) == 1 and len(self.plot_configs) > 1:
146
- # Special case: one figure for multiple plots - check if this is intentional
147
- logging.warning(f"Using single figure for {len(self.plot_configs)} plot components")
148
- # Use the single figure for the first plot, placeholders for the rest
149
- all_updates.append(gen_figs_list[0])
150
- for i in range(1, len(self.plot_configs)):
151
- all_updates.append(create_placeholder_plot("Info", f"Plot {i+1} - Data processing"))
152
- else:
153
- # Length mismatch - pad with placeholders
154
- logging.error(f"Figure list length mismatch: got {len(gen_figs_list)}, expected {len(self.plot_configs)}")
155
- for i in range(len(self.plot_configs)):
156
- if i < len(gen_figs_list):
157
- all_updates.append(gen_figs_list[i])
158
- else:
159
- all_updates.append(create_placeholder_plot("Error", "Figura mancante"))
160
-
161
- # Reset action panel UI elements (same as before)
162
- all_updates.extend([
163
- gr.update(visible=False), # global_actions_column_ui
164
- gr.update(value=[], visible=False), # insights_chatbot_ui
165
- gr.update(value="", visible=False), # insights_chat_input_ui
166
- gr.update(visible=False), # insights_suggestions_row_ui
167
- gr.update(value="S1"), # insights_suggestion_1_btn
168
- gr.update(value="S2"), # insights_suggestion_2_btn
169
- gr.update(value="S3"), # insights_suggestion_3_btn
170
- gr.update(value="Formula details here.", visible=False), # formula_display_markdown_ui
171
- gr.update(visible=False) # formula_close_hint_md
172
- ])
173
-
174
- # Reset states
175
- all_updates.extend([
176
- None, # active_panel_action_state
177
- None, # current_chat_plot_id_st
178
- {}, # chat_histories_st
179
- new_summaries_dict # plot_data_for_chatbot_st
180
- ])
181
-
182
- # Reset buttons and panel visibility for each plot
183
- for _ in self.plot_configs:
184
- all_updates.extend([
185
- gr.update(value=BOMB_ICON), # bomb_button
186
- gr.update(value=FORMULA_ICON), # formula_button
187
- gr.update(value=EXPLORE_ICON), # explore_button
188
- gr.update(visible=True) # panel_component
189
- ])
190
-
191
- all_updates.append(None) # explored_plot_id_state
192
-
193
- # Reset section title visibility
194
- for _ in self.unique_ordered_sections:
195
- all_updates.append(gr.update(visible=True))
196
-
197
- end_time = time.time()
198
- logging.info(f"Analytics graph refresh processing took {end_time - start_time:.2f} seconds.")
199
-
200
- expected_len = 1 + len(self.plot_configs) + 9 + 4 + (4 * len(self.plot_configs)) + 1 + self.num_unique_sections
201
- logging.info(f"Prepared {len(all_updates)} updates for graph refresh. Expected {expected_len}.")
202
-
203
- if len(all_updates) != expected_len:
204
- logging.error(f"Output length mismatch in refresh_analytics_graphs_ui. Got {len(all_updates)}, expected {expected_len}")
205
- # Pad with gr.update() to prevent Gradio errors
206
- while len(all_updates) < expected_len:
207
- all_updates.append(gr.update())
208
-
209
- return tuple(all_updates)
210
-
211
- def _get_action_panel_outputs_list(self):
212
- """Helper to construct the list of outputs for panel actions (insights, formula)."""
213
- outputs = [
214
- self.components['global_actions_column_ui'],
215
- self.components['insights_chatbot_ui'], # For visibility
216
- self.components['insights_chatbot_ui'], # For value
217
- self.components['insights_chat_input_ui'],
218
- self.components['insights_suggestions_row_ui'],
219
- self.components['insights_suggestion_1_btn'],
220
- self.components['insights_suggestion_2_btn'],
221
- self.components['insights_suggestion_3_btn'],
222
- self.components['formula_display_markdown_ui'], # For visibility
223
- self.components['formula_display_markdown_ui'], # For value
224
- self.components['formula_close_hint_md'],
225
- ]
226
- outputs.extend([
227
- self.active_panel_action_state,
228
- self.current_chat_plot_id_st,
229
- self.chat_histories_st,
230
- self.explored_plot_id_state
231
- ])
232
-
233
- for pc in self.plot_configs:
234
- ui_obj = self.plot_ui_objects.get(pc["id"], {})
235
- outputs.append(ui_obj.get("panel_component", gr.update())) # Plot panel visibility
236
- outputs.append(ui_obj.get("bomb_button", gr.update()))
237
- outputs.append(ui_obj.get("formula_button", gr.update()))
238
- outputs.append(ui_obj.get("explore_button", gr.update()))
239
-
240
- for s_name in self.unique_ordered_sections:
241
- outputs.append(self.section_titles_map.get(s_name, gr.update())) # Section title visibility
242
-
243
- expected_len = 11 + 4 + (4 * len(self.plot_configs)) + self.num_unique_sections
244
- logging.debug(f"Action panel outputs list length: {len(outputs)}, Expected: {expected_len}")
245
- return outputs
246
-
247
- async def handle_panel_action(self, plot_id_clicked: str, action_type: str,
248
- current_active_action_from_state: dict, # This is a direct value from gr.State
249
- current_chat_histories: dict, # This is a direct value
250
- current_chat_plot_id: str, # This is a direct value
251
- current_plot_data_for_chatbot: dict, # This is a direct value
252
- current_explored_plot_id: str # This is a direct value
253
- ):
254
- logging.info(f"Panel Action: '{action_type}' for plot '{plot_id_clicked}'. Active: {current_active_action_from_state}, Explored: {current_explored_plot_id}")
255
-
256
- clicked_plot_config = next((p for p in self.plot_configs if p["id"] == plot_id_clicked), None)
257
- if not clicked_plot_config:
258
- logging.error(f"Config not found for plot_id {plot_id_clicked}")
259
- num_outputs = len(self._get_action_panel_outputs_list())
260
- error_updates = [gr.update()] * num_outputs
261
- # Preserve existing state values if possible
262
- # Indices for states in action_panel_outputs_list:
263
- # active_panel_action_state is at index 11
264
- # current_chat_plot_id_st is at index 12
265
- # chat_histories_st is at index 13
266
- # explored_plot_id_state is at index 14
267
- error_updates[11] = current_active_action_from_state
268
- error_updates[12] = current_chat_plot_id
269
- error_updates[13] = current_chat_histories
270
- error_updates[14] = current_explored_plot_id
271
- yield tuple(error_updates) # Use yield instead of return <value>
272
- return # Explicitly return to end the generator function
273
-
274
- clicked_plot_label = clicked_plot_config["label"]
275
- clicked_plot_section = clicked_plot_config["section"]
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
- action_col_visible_update = gr.update(visible=False)
281
- insights_chatbot_visible_update = gr.update(visible=False)
282
- insights_chat_input_visible_update = gr.update(visible=False)
283
- insights_suggestions_row_visible_update = gr.update(visible=False)
284
- formula_display_visible_update = gr.update(visible=False)
285
- formula_close_hint_visible_update = gr.update(visible=False)
286
-
287
- chatbot_content_update = gr.update()
288
- s1_upd, s2_upd, s3_upd = gr.update(), gr.update(), gr.update()
289
- formula_content_update = gr.update()
290
-
291
- new_active_action_state_to_set = None # This will be the new value for the gr.State
292
- new_current_chat_plot_id = current_chat_plot_id # Default to existing
293
- updated_chat_histories = current_chat_histories # Default to existing
294
- new_explored_plot_id_to_set = current_explored_plot_id # Default to existing
295
-
296
- generated_panel_vis_updates = [] # For individual plot panels
297
- generated_bomb_btn_updates = []
298
- generated_formula_btn_updates = []
299
- generated_explore_btn_updates = []
300
- section_title_vis_updates = [gr.update()] * self.num_unique_sections
301
-
302
- if is_toggling_off:
303
- new_active_action_state_to_set = None
304
- action_col_visible_update = gr.update(visible=False)
305
- logging.info(f"Toggling OFF panel {action_type} for {plot_id_clicked}.")
306
-
307
- for _ in self.plot_configs:
308
- generated_bomb_btn_updates.append(gr.update(value=BOMB_ICON))
309
- generated_formula_btn_updates.append(gr.update(value=FORMULA_ICON))
310
-
311
- if current_explored_plot_id: # If an explore view is active, restore it
312
- explored_cfg = next((p for p in self.plot_configs if p["id"] == current_explored_plot_id), None)
313
- explored_sec = explored_cfg["section"] if explored_cfg else None
314
- for i, sec_name in enumerate(self.unique_ordered_sections):
315
- section_title_vis_updates[i] = gr.update(visible=(sec_name == explored_sec))
316
- for cfg in self.plot_configs:
317
- is_exp = (cfg["id"] == current_explored_plot_id)
318
- generated_panel_vis_updates.append(gr.update(visible=is_exp))
319
- generated_explore_btn_updates.append(gr.update(value=ACTIVE_ICON if is_exp else EXPLORE_ICON))
320
- else: # No explore view, all plots/sections visible
321
- for i in range(self.num_unique_sections):
322
- section_title_vis_updates[i] = gr.update(visible=True)
323
- for _ in self.plot_configs:
324
- generated_panel_vis_updates.append(gr.update(visible=True))
325
- generated_explore_btn_updates.append(gr.update(value=EXPLORE_ICON))
326
-
327
- if action_type == "insights":
328
- new_current_chat_plot_id = None # Clear chat context if insights panel is closed
329
-
330
- else: # Toggling ON a new action or switching actions
331
- new_active_action_state_to_set = hypothetical_new_active_state
332
- action_col_visible_update = gr.update(visible=True)
333
- new_explored_plot_id_to_set = None # Cancel any explore view
334
- logging.info(f"Toggling ON panel {action_type} for {plot_id_clicked}. Cancelling explore view if any.")
335
-
336
- # Show only the section of the clicked plot
337
- for i, sec_name in enumerate(self.unique_ordered_sections):
338
- section_title_vis_updates[i] = gr.update(visible=(sec_name == clicked_plot_section))
339
-
340
- # Show only the clicked plot's panel, update explore buttons to non-active
341
- for cfg in self.plot_configs:
342
- generated_panel_vis_updates.append(gr.update(visible=(cfg["id"] == plot_id_clicked)))
343
- generated_explore_btn_updates.append(gr.update(value=EXPLORE_ICON)) # Reset all explore to inactive
344
-
345
- # Update bomb and formula buttons based on the new active action
346
- for cfg_btn in self.plot_configs:
347
- is_active_insights = (new_active_action_state_to_set["plot_id"] == cfg_btn["id"] and new_active_action_state_to_set["type"] == "insights")
348
- is_active_formula = (new_active_action_state_to_set["plot_id"] == cfg_btn["id"] and new_active_action_state_to_set["type"] == "formula")
349
- generated_bomb_btn_updates.append(gr.update(value=ACTIVE_ICON if is_active_insights else BOMB_ICON))
350
- generated_formula_btn_updates.append(gr.update(value=ACTIVE_ICON if is_active_formula else FORMULA_ICON))
351
-
352
- if action_type == "insights":
353
- insights_chatbot_visible_update = gr.update(visible=True)
354
- insights_chat_input_visible_update = gr.update(visible=True)
355
- insights_suggestions_row_visible_update = gr.update(visible=True)
356
- new_current_chat_plot_id = plot_id_clicked # Set chat context
357
-
358
- history = current_chat_histories.get(plot_id_clicked, [])
359
- summary_for_plot = current_plot_data_for_chatbot.get(plot_id_clicked, f"Nessun sommario disponibile per '{clicked_plot_label}'.")
360
-
361
- if not history: # First time opening insights for this plot (or after a refresh)
362
- prompt, sugg = get_initial_insight_prompt_and_suggestions(plot_id_clicked, clicked_plot_label, summary_for_plot)
363
- llm_history_for_generation = [{"role": "user", "content": prompt}]
364
-
365
- # Display "Thinking..." or similar
366
- chatbot_content_update = gr.update(value=[[prompt, "Sto pensando..."]])
367
- # Yield intermediate update
368
- yield tuple(self._assemble_panel_action_updates(action_col_visible_update, insights_chatbot_visible_update, chatbot_content_update,
369
- insights_chat_input_visible_update, insights_suggestions_row_visible_update,
370
- s1_upd, s2_upd, s3_upd, formula_display_visible_update, formula_content_update,
371
- formula_close_hint_visible_update, new_active_action_state_to_set,
372
- new_current_chat_plot_id, updated_chat_histories, new_explored_plot_id_to_set,
373
- generated_panel_vis_updates, generated_bomb_btn_updates,
374
- generated_formula_btn_updates, generated_explore_btn_updates, section_title_vis_updates))
375
-
376
-
377
- resp_text = await generate_llm_response(prompt, plot_id_clicked, clicked_plot_label, llm_history_for_generation, summary_for_plot)
378
-
379
- new_gr_history_for_plot = [[prompt, resp_text]]
380
- new_internal_history_for_plot = [
381
- {"role": "user", "content": prompt},
382
- {"role": "assistant", "content": resp_text}
383
- ]
384
- updated_chat_histories = {**current_chat_histories, plot_id_clicked: new_internal_history_for_plot}
385
- chatbot_content_update = gr.update(value=new_gr_history_for_plot)
386
- else: # History exists, just display it
387
- _, sugg = get_initial_insight_prompt_and_suggestions(plot_id_clicked, clicked_plot_label, summary_for_plot) # Get fresh suggestions
388
- gr_history_to_display = self._convert_internal_to_gradio_chat_history(history)
389
- chatbot_content_update = gr.update(value=gr_history_to_display)
390
-
391
- s1_upd = gr.update(value=sugg[0] if sugg and len(sugg) > 0 else "N/A")
392
- s2_upd = gr.update(value=sugg[1] if sugg and len(sugg) > 1 else "N/A")
393
- s3_upd = gr.update(value=sugg[2] if sugg and len(sugg) > 2 else "N/A")
394
-
395
- elif action_type == "formula":
396
- formula_display_visible_update = gr.update(visible=True)
397
- formula_close_hint_visible_update = gr.update(visible=True)
398
- formula_key = PLOT_ID_TO_FORMULA_KEY_MAP.get(plot_id_clicked)
399
- formula_text = f"**Formula/Methodology for: {clicked_plot_label}** (ID: `{plot_id_clicked}`)\n\n"
400
- if formula_key and formula_key in PLOT_FORMULAS:
401
- formula_data = PLOT_FORMULAS[formula_key]
402
- formula_text += f"### {formula_data['title']}\n\n{formula_data['description']}\n\n"
403
- if 'calculation_steps' in formula_data and formula_data['calculation_steps']:
404
- formula_text += "**Calculation:**\n" + "\n".join([f"- {s}" for s in formula_data['calculation_steps']])
405
- else:
406
- formula_text += "(No detailed formula information found.)"
407
- formula_content_update = gr.update(value=formula_text)
408
- new_current_chat_plot_id = None # Clear chat context if formula panel is opened
409
-
410
- final_updates_tuple = self._assemble_panel_action_updates(
411
- action_col_visible_update, insights_chatbot_visible_update, chatbot_content_update,
412
- insights_chat_input_visible_update, insights_suggestions_row_visible_update,
413
- s1_upd, s2_upd, s3_upd, formula_display_visible_update, formula_content_update,
414
- formula_close_hint_visible_update, new_active_action_state_to_set,
415
- new_current_chat_plot_id, updated_chat_histories, new_explored_plot_id_to_set,
416
- generated_panel_vis_updates, generated_bomb_btn_updates,
417
- generated_formula_btn_updates, generated_explore_btn_updates, section_title_vis_updates
418
- )
419
- logging.debug(f"handle_panel_action yielding final updates. Count: {len(final_updates_tuple)}")
420
- yield final_updates_tuple
421
-
422
-
423
- def _assemble_panel_action_updates(self, action_col_visible_update, insights_chatbot_visible_update, chatbot_content_update,
424
- insights_chat_input_visible_update, insights_suggestions_row_visible_update,
425
- s1_upd, s2_upd, s3_upd, formula_display_visible_update, formula_content_update,
426
- formula_close_hint_visible_update, new_active_action_state_to_set,
427
- new_current_chat_plot_id, updated_chat_histories, new_explored_plot_id_to_set,
428
- generated_panel_vis_updates, generated_bomb_btn_updates,
429
- generated_formula_btn_updates, generated_explore_btn_updates, section_title_vis_updates):
430
- """Helper to assemble the final tuple of updates for handle_panel_action."""
431
- final_updates_list = [
432
- action_col_visible_update, # global_actions_column_ui (visibility)
433
- insights_chatbot_visible_update, # insights_chatbot_ui (visibility)
434
- chatbot_content_update, # insights_chatbot_ui (value)
435
- insights_chat_input_visible_update, # insights_chat_input_ui
436
- insights_suggestions_row_visible_update, # insights_suggestions_row_ui
437
- s1_upd, # insights_suggestion_1_btn
438
- s2_upd, # insights_suggestion_2_btn
439
- s3_upd, # insights_suggestion_3_btn
440
- formula_display_visible_update, # formula_display_markdown_ui (visibility)
441
- formula_content_update, # formula_display_markdown_ui (value)
442
- formula_close_hint_visible_update, # formula_close_hint_md
443
-
444
- # States
445
- new_active_action_state_to_set, # active_panel_action_state
446
- new_current_chat_plot_id, # current_chat_plot_id_st
447
- updated_chat_histories, # chat_histories_st
448
- new_explored_plot_id_to_set # explored_plot_id_state
449
- ]
450
- final_updates_list.extend(generated_panel_vis_updates)
451
- final_updates_list.extend(generated_bomb_btn_updates)
452
- final_updates_list.extend(generated_formula_btn_updates)
453
- final_updates_list.extend(generated_explore_btn_updates)
454
- final_updates_list.extend(section_title_vis_updates)
455
-
456
- expected_len = len(self._get_action_panel_outputs_list())
457
- if len(final_updates_list) != expected_len:
458
- logging.error(f"Output length mismatch in _assemble_panel_action_updates. Got {len(final_updates_list)}, expected {expected_len}")
459
- # Pad if necessary, though this is a bug indicator
460
- final_updates_list.extend([gr.update()] * (expected_len - len(final_updates_list)))
461
-
462
- return tuple(final_updates_list)
463
-
464
-
465
- async def handle_chat_message_submission(self, user_message: str, current_plot_id: str,
466
- chat_histories: dict, current_plot_data_for_chatbot: dict):
467
- if not current_plot_id or not user_message.strip():
468
- # Get current Gradio history for the plot_id to display
469
- internal_history_for_plot = chat_histories.get(current_plot_id, [])
470
- gr_history_display = self._convert_internal_to_gradio_chat_history(internal_history_for_plot)
471
- yield gr_history_display, gr.update(value=""), chat_histories
472
- return
473
-
474
- clicked_plot_config = next((p for p in self.plot_configs if p["id"] == current_plot_id), None)
475
- plot_label = clicked_plot_config["label"] if clicked_plot_config else "Selected Plot"
476
- summary_for_plot = current_plot_data_for_chatbot.get(current_plot_id, f"No summary for '{plot_label}'.")
477
-
478
- internal_history_for_plot = chat_histories.get(current_plot_id, []).copy() # Get a mutable copy
479
- internal_history_for_plot.append({"role": "user", "content": user_message})
480
-
481
- # Update Gradio chat display: User message + "Thinking..."
482
- gr_history_display_pending = self._convert_internal_to_gradio_chat_history(internal_history_for_plot, thinking=True)
483
- yield gr_history_display_pending, gr.update(value=""), chat_histories # Show user message immediately
484
-
485
- # Generate LLM response
486
- llm_response_text = await generate_llm_response(user_message, current_plot_id, plot_label, internal_history_for_plot, summary_for_plot)
487
-
488
- internal_history_for_plot.append({"role": "assistant", "content": llm_response_text})
489
-
490
- updated_chat_histories = {**chat_histories, current_plot_id: internal_history_for_plot}
491
-
492
- # Final Gradio chat display with LLM response
493
- final_gr_history_display = self._convert_internal_to_gradio_chat_history(internal_history_for_plot)
494
- yield final_gr_history_display, "", updated_chat_histories
495
-
496
- def _convert_internal_to_gradio_chat_history(self, internal_history, thinking=False):
497
- """Converts internal chat history format to Gradio's [[user, assistant], ...] format."""
498
- gradio_history = []
499
- temp_hist = internal_history[:] # Make a copy
500
- while temp_hist:
501
- user_msg_obj = temp_hist.pop(0)
502
- user_msg = user_msg_obj['content']
503
- assistant_msg = None
504
- if temp_hist and temp_hist[0]['role'] == 'assistant':
505
- assistant_msg_obj = temp_hist.pop(0)
506
- assistant_msg = assistant_msg_obj['content']
507
- gradio_history.append([user_msg, assistant_msg])
508
-
509
- if thinking and gradio_history and gradio_history[-1][1] is None: # If last message was user and we are in 'thinking' mode
510
- gradio_history[-1][1] = "Sto pensando..." # Replace None with "Thinking..."
511
- elif thinking and not gradio_history: # Should not happen if user_message was added
512
- pass # Or log an error, but it implies user_message wasn't added to internal_history_for_plot before calling
513
-
514
-
515
- return gradio_history
516
-
517
- async def handle_suggested_question_click(self, suggestion_text: str, current_plot_id: str,
518
- chat_histories: dict, current_plot_data_for_chatbot: dict):
519
- if not current_plot_id or not suggestion_text.strip() or suggestion_text == "N/A":
520
- internal_history_for_plot = chat_histories.get(current_plot_id, [])
521
- gr_history_display = self._convert_internal_to_gradio_chat_history(internal_history_for_plot)
522
- yield gr_history_display, gr.update(value=""), chat_histories
523
- return
524
-
525
- # Use the existing chat submission logic
526
- async for update_chunk in self.handle_chat_message_submission(suggestion_text, current_plot_id, chat_histories, current_plot_data_for_chatbot):
527
- yield update_chunk
528
-
529
- def _get_explore_outputs_list(self):
530
- """Helper to construct the list of outputs for explore actions."""
531
- outputs = [
532
- self.explored_plot_id_state,
533
- self.components['global_actions_column_ui'], # For visibility
534
- self.active_panel_action_state, # To potentially clear it
535
- self.components['formula_close_hint_md'] # For visibility
536
- ]
537
-
538
- for pc in self.plot_configs: # Plot panel visibility
539
- outputs.append(self.plot_ui_objects.get(pc["id"], {}).get("panel_component", gr.update()))
540
- for pc in self.plot_configs: # Explore button state
541
- outputs.append(self.plot_ui_objects.get(pc["id"], {}).get("explore_button", gr.update()))
542
- for pc in self.plot_configs: # Bomb button state (may need reset)
543
- outputs.append(self.plot_ui_objects.get(pc["id"], {}).get("bomb_button", gr.update()))
544
- for pc in self.plot_configs: # Formula button state (may need reset)
545
- outputs.append(self.plot_ui_objects.get(pc["id"], {}).get("formula_button", gr.update()))
546
-
547
- for s_name in self.unique_ordered_sections: # Section title visibility
548
- outputs.append(self.section_titles_map.get(s_name, gr.update()))
549
-
550
- expected_len = 4 + (4 * len(self.plot_configs)) + self.num_unique_sections
551
- logging.debug(f"Explore outputs list length: {len(outputs)}, Expected: {expected_len}")
552
- return outputs
553
-
554
- def handle_explore_click(self, plot_id_clicked: str, current_explored_plot_id_from_state: str,
555
- current_active_panel_action_state: dict):
556
- logging.info(f"Explore Click: Plot '{plot_id_clicked}'. Current Explored: {current_explored_plot_id_from_state}. Active Panel: {current_active_panel_action_state}")
557
-
558
- if not self.plot_ui_objects or not self.section_titles_map:
559
- logging.error("plot_ui_objects or section_titles_map not populated for handle_explore_click.")
560
- num_outputs = len(self._get_explore_outputs_list())
561
- error_updates = [gr.update()] * num_outputs
562
- error_updates[0] = current_explored_plot_id_from_state # Preserve explored_id_state
563
- error_updates[2] = current_active_panel_action_state # Preserve active_panel_state
564
- return tuple(error_updates)
565
-
566
- new_explored_id_to_set = None
567
- is_toggling_off_explore = (plot_id_clicked == current_explored_plot_id_from_state)
568
-
569
- action_col_upd = gr.update() # Default no change
570
- new_active_panel_state_upd = current_active_panel_action_state # Default no change
571
- formula_hint_upd = gr.update(visible=False) # Default hide
572
-
573
- panel_vis_updates = []
574
- explore_btns_updates = []
575
- bomb_btns_updates = [gr.update()] * len(self.plot_configs) # Default no change
576
- formula_btns_updates = [gr.update()] * len(self.plot_configs) # Default no change
577
- section_title_vis_updates = [gr.update()] * self.num_unique_sections
578
-
579
- clicked_cfg = next((p for p in self.plot_configs if p["id"] == plot_id_clicked), None)
580
- section_of_clicked_plot = clicked_cfg["section"] if clicked_cfg else None
581
-
582
- if is_toggling_off_explore:
583
- new_explored_id_to_set = None # Clear explore state
584
- logging.info(f"Stopping explore for {plot_id_clicked}. All plots/sections to be visible.")
585
- for i in range(self.num_unique_sections):
586
- section_title_vis_updates[i] = gr.update(visible=True)
587
- for _ in self.plot_configs:
588
- panel_vis_updates.append(gr.update(visible=True))
589
- explore_btns_updates.append(gr.update(value=EXPLORE_ICON))
590
- # Bomb and formula buttons remain as they were unless an action panel was closed (handled below if current_active_panel_action_state was set)
591
-
592
- else: # Starting explore or switching explored plot
593
- new_explored_id_to_set = plot_id_clicked
594
- logging.info(f"Exploring {plot_id_clicked}. Hiding other plots/sections.")
595
- for i, sec_name in enumerate(self.unique_ordered_sections):
596
- section_title_vis_updates[i] = gr.update(visible=(sec_name == section_of_clicked_plot))
597
- for cfg in self.plot_configs:
598
- is_target = (cfg["id"] == new_explored_id_to_set)
599
- panel_vis_updates.append(gr.update(visible=is_target))
600
- explore_btns_updates.append(gr.update(value=ACTIVE_ICON if is_target else EXPLORE_ICON))
601
-
602
- if current_active_panel_action_state: # If an action panel (insights/formula) is open, close it
603
- logging.info("Closing active insight/formula panel due to explore click.")
604
- action_col_upd = gr.update(visible=False)
605
- new_active_panel_state_upd = None # Clear active panel state
606
- formula_hint_upd = gr.update(visible=False) # Hide formula hint specifically
607
- # Reset bomb and formula buttons to their default icons
608
- bomb_btns_updates = [gr.update(value=BOMB_ICON) for _ in self.plot_configs]
609
- formula_btns_updates = [gr.update(value=FORMULA_ICON) for _ in self.plot_configs]
610
-
611
- final_explore_updates_list = [
612
- new_explored_id_to_set,
613
- action_col_upd,
614
- new_active_panel_state_upd,
615
- formula_hint_upd
616
- ]
617
- final_explore_updates_list.extend(panel_vis_updates)
618
- final_explore_updates_list.extend(explore_btns_updates)
619
- final_explore_updates_list.extend(bomb_btns_updates)
620
- final_explore_updates_list.extend(formula_btns_updates)
621
- final_explore_updates_list.extend(section_title_vis_updates)
622
-
623
- expected_len = len(self._get_explore_outputs_list())
624
- if len(final_explore_updates_list) != expected_len:
625
- logging.error(f"Output length mismatch in handle_explore_click. Got {len(final_explore_updates_list)}, expected {expected_len}")
626
- final_explore_updates_list.extend([gr.update()] * (expected_len - len(final_explore_updates_list)))
627
-
628
- return tuple(final_explore_updates_list)
629
-
630
- def setup_event_handlers(self):
631
- """Set up all event handlers for the analytics tab components."""
632
- logging.info("Setting up analytics event handlers.")
633
-
634
- # Apply filter button
635
- apply_filter_inputs = [
636
- self.token_state,
637
- self.components['date_filter_selector'],
638
- self.components['custom_start_date_picker'],
639
- self.components['custom_end_date_picker'],
640
- # self.chat_histories_st # Not directly an input to refresh_analytics_graphs_ui, it's accessed via self
641
- ]
642
- self.components['apply_filter_btn'].click(
643
- fn=self.refresh_analytics_graphs_ui,
644
- inputs=apply_filter_inputs,
645
- outputs=self._get_graph_refresh_outputs_list(), # Method returns the list of components
646
- show_progress="full",
647
- api_name="refresh_analytics_graphs"
648
- )
649
-
650
- # Plot action handlers (insights, formula, explore)
651
- action_click_inputs = [ # These are the gr.State objects themselves
652
- self.active_panel_action_state,
653
- self.chat_histories_st,
654
- self.current_chat_plot_id_st,
655
- self.plot_data_for_chatbot_st,
656
- self.explored_plot_id_state
657
- ]
658
-
659
- explore_click_inputs = [ # gr.State objects
660
- self.explored_plot_id_state,
661
- self.active_panel_action_state
662
- ]
663
-
664
- action_panel_outputs_list = self._get_action_panel_outputs_list()
665
- explore_outputs_list = self._get_explore_outputs_list()
666
-
667
- for config_item in self.plot_configs:
668
- plot_id = config_item["id"]
669
- if plot_id in self.plot_ui_objects:
670
- ui_obj = self.plot_ui_objects[plot_id]
671
-
672
- # Curry plot_id and action_type for the handler
673
- # The handler function itself (self.handle_panel_action) will receive the values from the gr.State inputs directly.
674
-
675
- if ui_obj.get("bomb_button"):
676
- ui_obj["bomb_button"].click(
677
- fn=lambda current_active, current_chats, current_chat_pid, current_plot_data, current_explored, p_id=plot_id: \
678
- self.handle_panel_action(p_id, "insights", current_active, current_chats, current_chat_pid, current_plot_data, current_explored),
679
- inputs=action_click_inputs, # Pass the list of gr.State objects
680
- outputs=action_panel_outputs_list,
681
- api_name=f"action_insights_{plot_id}"
682
- )
683
- if ui_obj.get("formula_button"):
684
- ui_obj["formula_button"].click(
685
- fn=lambda current_active, current_chats, current_chat_pid, current_plot_data, current_explored, p_id=plot_id: \
686
- self.handle_panel_action(p_id, "formula", current_active, current_chats, current_chat_pid, current_plot_data, current_explored),
687
- inputs=action_click_inputs,
688
- outputs=action_panel_outputs_list,
689
- api_name=f"action_formula_{plot_id}"
690
- )
691
- if ui_obj.get("explore_button"):
692
- ui_obj["explore_button"].click(
693
- fn=lambda current_explored_val, current_active_panel_val, p_id=plot_id: \
694
- self.handle_explore_click(p_id, current_explored_val, current_active_panel_val),
695
- inputs=explore_click_inputs, # Pass the list of gr.State objects
696
- outputs=explore_outputs_list,
697
- api_name=f"action_explore_{plot_id}"
698
- )
699
- else:
700
- logging.warning(f"UI object for plot_id '{plot_id}' not found for setting up click handlers.")
701
-
702
- # Chat submission handlers
703
- chat_submission_outputs = [
704
- self.components['insights_chatbot_ui'],
705
- self.components['insights_chat_input_ui'],
706
- self.chat_histories_st # This state will be updated
707
- ]
708
- chat_submission_inputs = [ # gr.Textbox, gr.State, gr.State, gr.State
709
- self.components['insights_chat_input_ui'],
710
- self.current_chat_plot_id_st,
711
- self.chat_histories_st,
712
- self.plot_data_for_chatbot_st
713
- ]
714
-
715
- self.components['insights_chat_input_ui'].submit(
716
- fn=self.handle_chat_message_submission,
717
- inputs=chat_submission_inputs,
718
- outputs=chat_submission_outputs,
719
- api_name="submit_chat_message"
720
- )
721
-
722
- suggestion_click_inputs_base = [ # gr.State, gr.State, gr.State
723
- self.current_chat_plot_id_st,
724
- self.chat_histories_st,
725
- self.plot_data_for_chatbot_st
726
- ]
727
-
728
- # For suggestion buttons, the first input is the button itself (to get its value)
729
- self.components['insights_suggestion_1_btn'].click(
730
- fn=self.handle_suggested_question_click,
731
- inputs=[self.components['insights_suggestion_1_btn']] + suggestion_click_inputs_base,
732
- outputs=chat_submission_outputs,
733
- api_name="click_suggestion_1"
734
- )
735
- self.components['insights_suggestion_2_btn'].click(
736
- fn=self.handle_suggested_question_click,
737
- inputs=[self.components['insights_suggestion_2_btn']] + suggestion_click_inputs_base,
738
- outputs=chat_submission_outputs,
739
- api_name="click_suggestion_2"
740
- )
741
- self.components['insights_suggestion_3_btn'].click(
742
- fn=self.handle_suggested_question_click,
743
- inputs=[self.components['insights_suggestion_3_btn']] + suggestion_click_inputs_base,
744
- outputs=chat_submission_outputs,
745
- api_name="click_suggestion_3"
746
- )
747
- logging.info("Analytics event handlers setup complete.")
748
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
services/analytics_tab_module.py ADDED
@@ -0,0 +1,674 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # analytics_tab_module.py
2
+ import gradio as gr
3
+ import pandas as pd
4
+ import logging
5
+ import time
6
+ from datetime import datetime, timedelta
7
+ import numpy as np
8
+ from collections import OrderedDict, defaultdict
9
+ import asyncio
10
+ import matplotlib # Keep this if create_placeholder_plot or other plot fns need it
11
+ matplotlib.use('Agg') # Keep if necessary for plotting functions
12
+ import matplotlib.pyplot as plt # Keep if necessary
13
+
14
+ # It's assumed that PLOT_CONFIGS is specific to this analytics tab.
15
+ # If it's used elsewhere, it might need to be passed in or imported from a central config.
16
+ PLOT_CONFIGS = [
17
+ {"label": "Numero di Follower nel Tempo", "id": "followers_count", "section": "Dinamiche dei Follower"},
18
+ {"label": "Tasso di Crescita Follower", "id": "followers_growth_rate", "section": "Dinamiche dei Follower"},
19
+ {"label": "Follower per Località", "id": "followers_by_location", "section": "Demografia Follower"},
20
+ {"label": "Follower per Ruolo (Funzione)", "id": "followers_by_role", "section": "Demografia Follower"},
21
+ {"label": "Follower per Settore", "id": "followers_by_industry", "section": "Demografia Follower"},
22
+ {"label": "Follower per Anzianità", "id": "followers_by_seniority", "section": "Demografia Follower"},
23
+ {"label": "Tasso di Engagement nel Tempo", "id": "engagement_rate", "section": "Approfondimenti Performance Post"},
24
+ {"label": "Copertura nel Tempo", "id": "reach_over_time", "section": "Approfondimenti Performance Post"},
25
+ {"label": "Visualizzazioni nel Tempo", "id": "impressions_over_time", "section": "Approfondimenti Performance Post"},
26
+ {"label": "Reazioni (Like) nel Tempo", "id": "likes_over_time", "section": "Approfondimenti Performance Post"},
27
+ {"label": "Click nel Tempo", "id": "clicks_over_time", "section": "Engagement Dettagliato Post nel Tempo"},
28
+ {"label": "Condivisioni nel Tempo", "id": "shares_over_time", "section": "Engagement Dettagliato Post nel Tempo"},
29
+ {"label": "Commenti nel Tempo", "id": "comments_over_time", "section": "Engagement Dettagliato Post nel Tempo"},
30
+ {"label": "Ripartizione Commenti per Sentiment", "id": "comments_sentiment", "section": "Engagement Dettagliato Post nel Tempo"},
31
+ {"label": "Frequenza Post", "id": "post_frequency_cs", "section": "Analisi Strategia Contenuti"},
32
+ {"label": "Ripartizione Contenuti per Formato", "id": "content_format_breakdown_cs", "section": "Analisi Strategia Contenuti"},
33
+ {"label": "Ripartizione Contenuti per Argomenti", "id": "content_topic_breakdown_cs", "section": "Analisi Strategia Contenuti"},
34
+ {"label": "Volume Menzioni nel Tempo (Dettaglio)", "id": "mention_analysis_volume", "section": "Analisi Menzioni (Dettaglio)"},
35
+ {"label": "Ripartizione Menzioni per Sentiment (Dettaglio)", "id": "mention_analysis_sentiment", "section": "Analisi Menzioni (Dettaglio)"}
36
+ ]
37
+ # IMPORTANT: Review if 'mention_analysis_volume' and 'mention_analysis_sentiment' plots
38
+ # can still be generated without the dedicated mentions data processing.
39
+ # If not, they should also be removed from plot_configs.
40
+ # For now, I am assuming they might draw from a general data pool in token_state.
41
+ assert len(PLOT_CONFIGS) == 19, "Mancata corrispondenza in PLOT_CONFIGS e grafici attesi. (If mentions plots were removed, adjust this number)"
42
+
43
+ UNIQUE_ORDERED_SECTIONS = list(OrderedDict.fromkeys(pc["section"] for pc in PLOT_CONFIGS))
44
+ NUM_UNIQUE_SECTIONS = len(UNIQUE_ORDERED_SECTIONS)
45
+
46
+ class AnalyticsTab:
47
+ def __init__(self, token_state, chat_histories_st, current_chat_plot_id_st, plot_data_for_chatbot_st,
48
+ # External dependencies (functions, data, icons)
49
+ plot_id_to_formula_map, plot_formulas_data, icons,
50
+ fn_build_plot_area, fn_update_plot_figures, fn_create_placeholder_plot,
51
+ fn_get_initial_insight, fn_generate_llm_response):
52
+
53
+ # Shared Gradio states passed from the main app
54
+ self.token_state = token_state
55
+ self.chat_histories_st = chat_histories_st
56
+ self.current_chat_plot_id_st = current_chat_plot_id_st
57
+ self.plot_data_for_chatbot_st = plot_data_for_chatbot_st
58
+
59
+ # Store external dependencies
60
+ self.PLOT_ID_TO_FORMULA_KEY_MAP = plot_id_to_formula_map
61
+ self.PLOT_FORMULAS = plot_formulas_data
62
+ self.BOMB_ICON = icons['bomb']
63
+ self.EXPLORE_ICON = icons['explore']
64
+ self.FORMULA_ICON = icons['formula']
65
+ self.ACTIVE_ICON = icons['active']
66
+ self.build_analytics_tab_plot_area = fn_build_plot_area
67
+ self.update_analytics_plots_figures = fn_update_plot_figures
68
+ self.create_placeholder_plot = fn_create_placeholder_plot
69
+ self.get_initial_insight_prompt_and_suggestions = fn_get_initial_insight
70
+ self.generate_llm_response = fn_generate_llm_response
71
+
72
+ # Internal Gradio states for this tab
73
+ self.active_panel_action_state = gr.State(None)
74
+ self.explored_plot_id_state = gr.State(None)
75
+
76
+ # To store UI objects created by build_analytics_tab_plot_area
77
+ self.plot_ui_objects = {}
78
+ self.section_titles_map = {}
79
+
80
+ # UI components that will be created in create_tab_ui
81
+ self.analytics_status_md = None
82
+ self.date_filter_selector = None
83
+ self.custom_start_date_picker = None
84
+ self.custom_end_date_picker = None
85
+ self.apply_filter_btn = None
86
+ self.plots_area_col = None
87
+ self.global_actions_column_ui = None
88
+ self.insights_chatbot_ui = None
89
+ self.insights_chat_input_ui = None
90
+ self.insights_suggestions_row_ui = None
91
+ self.insights_suggestion_1_btn = None
92
+ self.insights_suggestion_2_btn = None
93
+ self.insights_suggestion_3_btn = None
94
+ self.formula_display_markdown_ui = None
95
+ self.formula_close_hint_md = None
96
+
97
+ # Lists for Gradio callback outputs, will be populated after UI creation
98
+ self.graph_refresh_outputs_list = []
99
+ self.action_panel_outputs_list = []
100
+ self.explore_outputs_list = []
101
+
102
+ def _toggle_custom_date_pickers(self, selection):
103
+ is_custom = selection == "Intervallo Personalizzato"
104
+ return gr.update(visible=is_custom), gr.update(visible=is_custom)
105
+
106
+ async def _handle_panel_action(
107
+ self, plot_id_clicked: str, action_type: str, current_active_action_from_state: dict,
108
+ current_chat_histories: dict, current_chat_plot_id: str,
109
+ current_plot_data_for_chatbot: dict, current_explored_plot_id: str
110
+ ):
111
+ logging.info(f"Panel Action: '{action_type}' for plot '{plot_id_clicked}'. Active: {current_active_action_from_state}, Explored: {current_explored_plot_id}")
112
+ clicked_plot_config = next((p for p in PLOT_CONFIGS if p["id"] == plot_id_clicked), None)
113
+
114
+ if not clicked_plot_config:
115
+ logging.error(f"Config not found for plot_id {plot_id_clicked}")
116
+ num_plots = len(PLOT_CONFIGS)
117
+ error_list_len = 15 + (4 * num_plots) + NUM_UNIQUE_SECTIONS
118
+ error_list = [gr.update()] * error_list_len
119
+ # Fill specific indices if needed, matching the expected output structure
120
+ error_list[11] = current_active_action_from_state # active_panel_action_state
121
+ error_list[12] = current_chat_plot_id # current_chat_plot_id_st
122
+ error_list[13] = current_chat_histories # chat_histories_st
123
+ error_list[14] = current_explored_plot_id # explored_plot_id_state
124
+ return error_list
125
+
126
+ clicked_plot_label = clicked_plot_config["label"]
127
+ clicked_plot_section = clicked_plot_config["section"]
128
+ hypothetical_new_active_state = {"plot_id": plot_id_clicked, "type": action_type}
129
+ is_toggling_off = current_active_action_from_state == hypothetical_new_active_state
130
+
131
+ action_col_visible_update = gr.update(visible=False)
132
+ insights_chatbot_visible_update, insights_chat_input_visible_update, insights_suggestions_row_visible_update = gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
133
+ formula_display_visible_update = gr.update(visible=False)
134
+ formula_close_hint_visible_update = gr.update(visible=False)
135
+ chatbot_content_update, s1_upd, s2_upd, s3_upd, formula_content_update = gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
136
+
137
+ new_active_action_state_to_set = None
138
+ new_current_chat_plot_id = current_chat_plot_id # Preserve by default
139
+ updated_chat_histories = current_chat_histories # Preserve by default
140
+ new_explored_plot_id_to_set = current_explored_plot_id # Preserve by default
141
+
142
+ generated_panel_vis_updates = []
143
+ generated_bomb_btn_updates = []
144
+ generated_formula_btn_updates = []
145
+ generated_explore_btn_updates = []
146
+ section_title_vis_updates = [gr.update()] * NUM_UNIQUE_SECTIONS
147
+
148
+ if is_toggling_off:
149
+ new_active_action_state_to_set = None
150
+ action_col_visible_update = gr.update(visible=False)
151
+ logging.info(f"Toggling OFF panel {action_type} for {plot_id_clicked}.")
152
+
153
+ for _ in PLOT_CONFIGS:
154
+ generated_bomb_btn_updates.append(gr.update(value=self.BOMB_ICON))
155
+ generated_formula_btn_updates.append(gr.update(value=self.FORMULA_ICON))
156
+
157
+ if current_explored_plot_id: # If an explore view was active, restore it
158
+ explored_cfg = next((p for p in PLOT_CONFIGS if p["id"] == current_explored_plot_id), None)
159
+ explored_sec = explored_cfg["section"] if explored_cfg else None
160
+ for i, sec_name in enumerate(UNIQUE_ORDERED_SECTIONS):
161
+ section_title_vis_updates[i] = gr.update(visible=(sec_name == explored_sec))
162
+ for cfg in PLOT_CONFIGS:
163
+ is_exp = (cfg["id"] == current_explored_plot_id)
164
+ generated_panel_vis_updates.append(gr.update(visible=is_exp))
165
+ generated_explore_btn_updates.append(gr.update(value=self.ACTIVE_ICON if is_exp else self.EXPLORE_ICON))
166
+ else: # No explore view, all plots visible
167
+ for i in range(NUM_UNIQUE_SECTIONS):
168
+ section_title_vis_updates[i] = gr.update(visible=True)
169
+ for _ in PLOT_CONFIGS:
170
+ generated_panel_vis_updates.append(gr.update(visible=True))
171
+ generated_explore_btn_updates.append(gr.update(value=self.EXPLORE_ICON))
172
+
173
+ if action_type == "insights": # Specifically when closing insights chat
174
+ new_current_chat_plot_id = None # Clear the chat context
175
+
176
+ else: # Toggling ON a panel action
177
+ new_active_action_state_to_set = hypothetical_new_active_state
178
+ action_col_visible_update = gr.update(visible=True)
179
+ new_explored_plot_id_to_set = None # Cancel any active explore view
180
+ logging.info(f"Toggling ON panel {action_type} for {plot_id_clicked}. Cancelling explore view if any.")
181
+
182
+ # Show only the clicked plot and its section title
183
+ for i, sec_name in enumerate(UNIQUE_ORDERED_SECTIONS):
184
+ section_title_vis_updates[i] = gr.update(visible=(sec_name == clicked_plot_section))
185
+ for cfg in PLOT_CONFIGS:
186
+ generated_panel_vis_updates.append(gr.update(visible=(cfg["id"] == plot_id_clicked)))
187
+ generated_explore_btn_updates.append(gr.update(value=self.EXPLORE_ICON)) # Reset all explore buttons
188
+
189
+ # Update bomb and formula button icons based on the new active action
190
+ for cfg_btn in PLOT_CONFIGS:
191
+ is_active_insights = new_active_action_state_to_set == {"plot_id": cfg_btn["id"], "type": "insights"}
192
+ is_active_formula = new_active_action_state_to_set == {"plot_id": cfg_btn["id"], "type": "formula"}
193
+ generated_bomb_btn_updates.append(gr.update(value=self.ACTIVE_ICON if is_active_insights else self.BOMB_ICON))
194
+ generated_formula_btn_updates.append(gr.update(value=self.ACTIVE_ICON if is_active_formula else self.FORMULA_ICON))
195
+
196
+ if action_type == "insights":
197
+ insights_chatbot_visible_update = gr.update(visible=True)
198
+ insights_chat_input_visible_update = gr.update(visible=True)
199
+ insights_suggestions_row_visible_update = gr.update(visible=True)
200
+ new_current_chat_plot_id = plot_id_clicked # Set chat context to this plot
201
+
202
+ history = current_chat_histories.get(plot_id_clicked, [])
203
+ summary = current_plot_data_for_chatbot.get(plot_id_clicked, f"No summary available for '{clicked_plot_label}'.")
204
+
205
+ if not history: # First time opening chat for this plot
206
+ prompt, sugg = self.get_initial_insight_prompt_and_suggestions(plot_id_clicked, clicked_plot_label, summary)
207
+ llm_history_for_api = [{"role": "user", "content": prompt}] # API expects list of dicts
208
+ resp = await self.generate_llm_response(prompt, plot_id_clicked, clicked_plot_label, llm_history_for_api, summary)
209
+ history = [{"role": "assistant", "content": resp}] # Gradio expects list of tuples or specific dicts for Chatbot
210
+ updated_chat_histories = {**current_chat_histories, plot_id_clicked: history}
211
+ else: # Re-opening chat, just get new suggestions if any
212
+ _, sugg = self.get_initial_insight_prompt_and_suggestions(plot_id_clicked, clicked_plot_label, summary)
213
+
214
+ chatbot_content_update = gr.update(value=history)
215
+ s1_upd = gr.update(value=sugg[0] if sugg and len(sugg) > 0 else "N/A")
216
+ s2_upd = gr.update(value=sugg[1] if sugg and len(sugg) > 1 else "N/A")
217
+ s3_upd = gr.update(value=sugg[2] if sugg and len(sugg) > 2 else "N/A")
218
+
219
+ elif action_type == "formula":
220
+ formula_display_visible_update = gr.update(visible=True)
221
+ formula_close_hint_visible_update = gr.update(visible=True)
222
+ formula_key = self.PLOT_ID_TO_FORMULA_KEY_MAP.get(plot_id_clicked)
223
+ formula_text = f"**Formula/Methodology for: {clicked_plot_label}** (ID: `{plot_id_clicked}`)\n\n"
224
+ if formula_key and formula_key in self.PLOT_FORMULAS:
225
+ formula_data = self.PLOT_FORMULAS[formula_key]
226
+ formula_text += f"### {formula_data['title']}\n\n{formula_data['description']}\n\n**Calculation:**\n"
227
+ formula_text += "\n".join([f"- {step}" for step in formula_data['calculation_steps']])
228
+ else:
229
+ formula_text += "(No detailed formula information found.)"
230
+ formula_content_update = gr.update(value=formula_text)
231
+ new_current_chat_plot_id = None # Clear chat context if formula is opened
232
+
233
+ # Order of updates must match self.action_panel_outputs_list
234
+ final_updates = [
235
+ action_col_visible_update, # global_actions_column_ui
236
+ insights_chatbot_visible_update, # insights_chatbot_ui (visibility)
237
+ chatbot_content_update, # insights_chatbot_ui (content)
238
+ insights_chat_input_visible_update, # insights_chat_input_ui
239
+ insights_suggestions_row_visible_update, # insights_suggestions_row_ui
240
+ s1_upd, s2_upd, s3_upd, # suggestion buttons
241
+ formula_display_visible_update, # formula_display_markdown_ui (visibility)
242
+ formula_content_update, # formula_display_markdown_ui (content)
243
+ formula_close_hint_visible_update, # formula_close_hint_md
244
+ new_active_action_state_to_set, # active_panel_action_state
245
+ new_current_chat_plot_id, # current_chat_plot_id_st
246
+ updated_chat_histories, # chat_histories_st
247
+ new_explored_plot_id_to_set # explored_plot_id_state
248
+ ]
249
+ final_updates.extend(generated_panel_vis_updates) # Plot panel visibilities
250
+ final_updates.extend(generated_bomb_btn_updates) # Bomb button icons
251
+ final_updates.extend(generated_formula_btn_updates) # Formula button icons
252
+ final_updates.extend(generated_explore_btn_updates) # Explore button icons
253
+ final_updates.extend(section_title_vis_updates) # Section title visibilities
254
+
255
+ logging.debug(f"handle_panel_action returning {len(final_updates)} updates. Expected {15 + 4*len(PLOT_CONFIGS) + NUM_UNIQUE_SECTIONS}.")
256
+ return final_updates
257
+
258
+ async def _handle_chat_message_submission(self, user_message: str, current_plot_id: str, chat_histories: dict, current_plot_data_for_chatbot: dict):
259
+ if not current_plot_id or not user_message.strip():
260
+ current_history_for_plot = chat_histories.get(current_plot_id, [])
261
+ # Ensure history is in the format Gradio Chatbot expects (list of lists/tuples or specific dicts)
262
+ # Assuming it's already [{"role": "user/assistant", "content": "..."}]
263
+ yield current_history_for_plot, gr.update(value=""), chat_histories
264
+ return
265
+
266
+ clicked_plot_config = next((p for p in PLOT_CONFIGS if p["id"] == current_plot_id), None)
267
+ plot_label = clicked_plot_config["label"] if clicked_plot_config else "Selected Plot"
268
+ summary_data = current_plot_data_for_chatbot.get(current_plot_id, f"No summary available for '{plot_label}'.")
269
+
270
+ # Ensure history is a list of dicts {"role": ..., "content": ...} for the API
271
+ history_for_api = chat_histories.get(current_plot_id, []).copy() # Get a mutable copy
272
+ if not isinstance(history_for_api, list): history_for_api = [] # Should not happen if initialized correctly
273
+
274
+ history_for_api.append({"role": "user", "content": user_message})
275
+
276
+ # Update Gradio chatbot UI immediately with user message
277
+ # Gradio Chatbot expects list of (user_msg, assistant_msg) tuples or specific dicts.
278
+ # If current_chat_histories stores [{"role": "user", "content": "..."}], convert for display if needed,
279
+ # or ensure generate_llm_response and initial prompt also use this dict format.
280
+ # For simplicity, let's assume chat_histories_st stores a list of such dicts.
281
+ current_display_history = history_for_api.copy() # This is what will be displayed
282
+
283
+ yield current_display_history, gr.update(value=""), chat_histories # Update UI, clear input, pass original histories
284
+
285
+ assistant_response = await self.generate_llm_response(user_message, current_plot_id, plot_label, history_for_api, summary_data)
286
+ history_for_api.append({"role": "assistant", "content": assistant_response})
287
+
288
+ updated_chat_histories = {**chat_histories, current_plot_id: history_for_api}
289
+ current_display_history.append({"role": "assistant", "content": assistant_response})
290
+
291
+
292
+ yield current_display_history, "", updated_chat_histories
293
+
294
+
295
+ async def _handle_suggested_question_click(self, suggestion_text: str, current_plot_id: str, chat_histories: dict, current_plot_data_for_chatbot: dict):
296
+ if not current_plot_id or not suggestion_text.strip() or suggestion_text == "N/A":
297
+ current_history_for_plot = chat_histories.get(current_plot_id, [])
298
+ yield current_history_for_plot, gr.update(value=""), chat_histories
299
+ return
300
+
301
+ # Use async for to stream updates from _handle_chat_message_submission
302
+ async for update_chunk in self._handle_chat_message_submission(suggestion_text, current_plot_id, chat_histories, current_plot_data_for_chatbot):
303
+ yield update_chunk
304
+
305
+
306
+ def _handle_explore_click(self, plot_id_clicked, current_explored_plot_id_from_state, current_active_panel_action_state):
307
+ logging.info(f"Explore Click: Plot '{plot_id_clicked}'. Current Explored: {current_explored_plot_id_from_state}. Active Panel: {current_active_panel_action_state}")
308
+ num_plots = len(PLOT_CONFIGS)
309
+
310
+ if not self.plot_ui_objects: # Check if plot UI objects are populated
311
+ logging.error("plot_ui_objects not populated for handle_explore_click.")
312
+ # Prepare a list of gr.update() of the correct length
313
+ error_list_len = 4 + (4 * num_plots) + NUM_UNIQUE_SECTIONS
314
+ error_list = [gr.update()] * error_list_len
315
+ error_list[0] = current_explored_plot_id_from_state # explored_plot_id_state
316
+ error_list[2] = current_active_panel_action_state # active_panel_action_state
317
+ return error_list
318
+
319
+ new_explored_id_to_set = None
320
+ is_toggling_off_explore = (plot_id_clicked == current_explored_plot_id_from_state)
321
+
322
+ action_col_upd = gr.update() # Default to no change
323
+ new_active_panel_state_upd = current_active_panel_action_state # Default to no change
324
+ formula_hint_upd = gr.update(visible=False) # Hide by default, only shown by formula panel
325
+
326
+ panel_vis_updates = []
327
+ explore_btns_updates = []
328
+ bomb_btns_updates = [gr.update()] * num_plots # Default to no change for bomb/formula unless panel closes
329
+ formula_btns_updates = [gr.update()] * num_plots
330
+ section_title_vis_updates = [gr.update()] * NUM_UNIQUE_SECTIONS
331
+
332
+ clicked_cfg = next((p for p in PLOT_CONFIGS if p["id"] == plot_id_clicked), None)
333
+ section_of_clicked_plot = clicked_cfg["section"] if clicked_cfg else None
334
+
335
+ if is_toggling_off_explore:
336
+ new_explored_id_to_set = None # Clear explore state
337
+ logging.info(f"Stopping explore for {plot_id_clicked}. All plots/sections to be visible.")
338
+ for i in range(NUM_UNIQUE_SECTIONS):
339
+ section_title_vis_updates[i] = gr.update(visible=True)
340
+ for _ in PLOT_CONFIGS:
341
+ panel_vis_updates.append(gr.update(visible=True))
342
+ explore_btns_updates.append(gr.update(value=self.EXPLORE_ICON))
343
+ # If an action panel was open, it remains open. Its visibility is tied to active_panel_action_state.
344
+ # Bomb and formula buttons don't change unless an action panel is closed.
345
+ else: # Exploring a new plot or switching explore
346
+ new_explored_id_to_set = plot_id_clicked
347
+ logging.info(f"Exploring {plot_id_clicked}. Hiding other plots/sections.")
348
+
349
+ for i, sec_name in enumerate(UNIQUE_ORDERED_SECTIONS):
350
+ section_title_vis_updates[i] = gr.update(visible=(sec_name == section_of_clicked_plot))
351
+
352
+ for cfg in PLOT_CONFIGS:
353
+ is_target_plot = (cfg["id"] == new_explored_id_to_set)
354
+ panel_vis_updates.append(gr.update(visible=is_target_plot))
355
+ explore_btns_updates.append(gr.update(value=self.ACTIVE_ICON if is_target_plot else self.EXPLORE_ICON))
356
+
357
+ if current_active_panel_action_state: # If an insights/formula panel was open, close it
358
+ logging.info("Closing active insight/formula panel due to explore click.")
359
+ action_col_upd = gr.update(visible=False)
360
+ new_active_panel_state_upd = None # This will be set to the state
361
+ # Reset bomb and formula buttons to their default icons
362
+ bomb_btns_updates = [gr.update(value=self.BOMB_ICON) for _ in PLOT_CONFIGS]
363
+ formula_btns_updates = [gr.update(value=self.FORMULA_ICON) for _ in PLOT_CONFIGS]
364
+ formula_hint_upd = gr.update(visible=False) # Ensure hint is hidden
365
+
366
+ # Order of updates must match self.explore_outputs_list
367
+ final_explore_updates = [
368
+ new_explored_id_to_set, # explored_plot_id_state
369
+ action_col_upd, # global_actions_column_ui
370
+ new_active_panel_state_upd, # active_panel_action_state
371
+ formula_hint_upd # formula_close_hint_md
372
+ ]
373
+ final_explore_updates.extend(panel_vis_updates) # Plot panel visibilities
374
+ final_explore_updates.extend(explore_btns_updates) # Explore button icons
375
+ final_explore_updates.extend(bomb_btns_updates) # Bomb button icons
376
+ final_explore_updates.extend(formula_btns_updates) # Formula button icons
377
+ final_explore_updates.extend(section_title_vis_updates) # Section title visibilities
378
+
379
+ logging.debug(f"handle_explore_click returning {len(final_explore_updates)} updates. Expected {4 + 4*len(PLOT_CONFIGS) + NUM_UNIQUE_SECTIONS}.")
380
+ return final_explore_updates
381
+
382
+ def _create_panel_action_handler(self, p_id, action_type_str):
383
+ # This wrapper is needed because Gradio's .click() fn doesn't easily pass extra args fixed at definition time
384
+ # without using lambdas that might have late binding issues in loops, or functools.partial.
385
+ # An inner async def is a clean way for this specific pattern with async handlers.
386
+ async def _handler(curr_active_val, curr_chats_val, curr_chat_pid, curr_plot_data, curr_explored_id):
387
+ return await self._handle_panel_action(p_id, action_type_str, curr_active_val, curr_chats_val, curr_chat_pid, curr_plot_data, curr_explored_id)
388
+ return _handler
389
+
390
+ async def _refresh_analytics_graphs_ui(self, current_token_state_val, date_filter_val, custom_start_val, custom_end_val, current_chat_histories_val):
391
+ logging.info("Refreshing analytics graph UI elements and resetting actions/chat (within module).")
392
+ start_time = time.time()
393
+
394
+ plot_gen_results = self.update_analytics_plots_figures(current_token_state_val, date_filter_val, custom_start_val, custom_end_val, PLOT_CONFIGS)
395
+ status_msg, gen_figs, new_summaries_for_chatbot = plot_gen_results[0], plot_gen_results[1:-1], plot_gen_results[-1]
396
+
397
+ all_updates = []
398
+ # 1. Status Markdown
399
+ all_updates.append(status_msg) # For self.analytics_status_md
400
+
401
+ # 2. Plot components
402
+ if len(gen_figs) == len(PLOT_CONFIGS):
403
+ all_updates.extend(gen_figs)
404
+ else:
405
+ logging.error(f"Mismatch in generated figures ({len(gen_figs)}) and plot_configs ({len(PLOT_CONFIGS)})")
406
+ all_updates.extend([self.create_placeholder_plot("Error", f"Figura mancante {i}") for i in range(len(PLOT_CONFIGS))])
407
+
408
+ # 3. UI Resets for Action Panel (9 components)
409
+ all_updates.extend([
410
+ gr.update(visible=False), # global_actions_column_ui
411
+ gr.update(value=[], visible=False), # insights_chatbot_ui (content & visibility)
412
+ gr.update(value="", visible=False), # insights_chat_input_ui
413
+ gr.update(visible=False), # insights_suggestions_row_ui
414
+ gr.update(value="Suggerimento 1"), gr.update(value="Suggerimento 2"), gr.update(value="Suggerimento 3"), # suggestion_btns
415
+ gr.update(value="I dettagli sulla formula/metodologia appariranno qui.", visible=False), # formula_display_markdown_ui
416
+ gr.update(visible=False) # formula_close_hint_md
417
+ ])
418
+
419
+ # 4. State Resets (4 states)
420
+ all_updates.extend([
421
+ None, # active_panel_action_state (reset)
422
+ None, # current_chat_plot_id_st (reset)
423
+ current_chat_histories_val, # chat_histories_st (pass through, or {} to reset all chats) - original code implies reset with {}
424
+ new_summaries_for_chatbot # plot_data_for_chatbot_st (update with new summaries)
425
+ ])
426
+ # If chat_histories_st should be fully reset on graph refresh:
427
+ # all_updates[-2] = {} # This would reset all chat histories
428
+
429
+ # 5. Plot-specific UI Resets (4 components per plot)
430
+ for _ in PLOT_CONFIGS:
431
+ all_updates.extend([
432
+ gr.update(value=self.BOMB_ICON), # bomb_button
433
+ gr.update(value=self.FORMULA_ICON), # formula_button
434
+ gr.update(value=self.EXPLORE_ICON), # explore_button
435
+ gr.update(visible=True) # panel_component (plot visibility itself)
436
+ ])
437
+
438
+ # 6. Explored Plot ID State Reset (1 state)
439
+ all_updates.append(None) # explored_plot_id_state (reset)
440
+
441
+ # 7. Section Title Visibilities
442
+ all_updates.extend([gr.update(visible=True)] * NUM_UNIQUE_SECTIONS)
443
+
444
+ end_time = time.time()
445
+ logging.info(f"Analytics graph refresh (module) took {end_time - start_time:.2f} seconds.")
446
+ expected_len = 1 + len(PLOT_CONFIGS) + 9 + 4 + (4 * len(PLOT_CONFIGS)) + 1 + NUM_UNIQUE_SECTIONS
447
+ logging.info(f"Prepared {len(all_updates)} updates for graph refresh. Expected {expected_len}.")
448
+ if len(all_updates) != expected_len:
449
+ logging.error(f"Output length mismatch in _refresh_analytics_graphs_ui: got {len(all_updates)}, expected {expected_len}")
450
+ return tuple(all_updates)
451
+
452
+ def _define_callback_outputs(self):
453
+ # This method populates the output lists for various callbacks.
454
+ # It MUST be called after all UI components of this tab are created.
455
+
456
+ # Outputs for _refresh_analytics_graphs_ui
457
+ self.graph_refresh_outputs_list.append(self.analytics_status_md)
458
+ self.graph_refresh_outputs_list.extend([self.plot_ui_objects.get(pc["id"], {}).get("plot_component", gr.update()) for pc in PLOT_CONFIGS])
459
+ self.graph_refresh_outputs_list.extend([
460
+ self.global_actions_column_ui, self.insights_chatbot_ui, self.insights_chat_input_ui,
461
+ self.insights_suggestions_row_ui, self.insights_suggestion_1_btn, self.insights_suggestion_2_btn,
462
+ self.insights_suggestion_3_btn, self.formula_display_markdown_ui, self.formula_close_hint_md
463
+ ])
464
+ self.graph_refresh_outputs_list.extend([
465
+ self.active_panel_action_state, self.current_chat_plot_id_st,
466
+ self.chat_histories_st, self.plot_data_for_chatbot_st
467
+ ])
468
+ for pc in PLOT_CONFIGS:
469
+ pid = pc["id"]
470
+ self.graph_refresh_outputs_list.extend([
471
+ self.plot_ui_objects.get(pid, {}).get("bomb_button", gr.update()),
472
+ self.plot_ui_objects.get(pid, {}).get("formula_button", gr.update()),
473
+ self.plot_ui_objects.get(pid, {}).get("explore_button", gr.update()),
474
+ self.plot_ui_objects.get(pid, {}).get("panel_component", gr.update())
475
+ ])
476
+ self.graph_refresh_outputs_list.append(self.explored_plot_id_state)
477
+ self.graph_refresh_outputs_list.extend([self.section_titles_map.get(s_name, gr.update()) for s_name in UNIQUE_ORDERED_SECTIONS])
478
+
479
+ # Outputs for _handle_panel_action
480
+ self.action_panel_outputs_list.extend([
481
+ self.global_actions_column_ui, self.insights_chatbot_ui, self.insights_chatbot_ui, # Chatbot visibility and content
482
+ self.insights_chat_input_ui, self.insights_suggestions_row_ui,
483
+ self.insights_suggestion_1_btn, self.insights_suggestion_2_btn, self.insights_suggestion_3_btn,
484
+ self.formula_display_markdown_ui, self.formula_display_markdown_ui, # Formula visibility and content
485
+ self.formula_close_hint_md
486
+ ])
487
+ self.action_panel_outputs_list.extend([
488
+ self.active_panel_action_state, self.current_chat_plot_id_st,
489
+ self.chat_histories_st, self.explored_plot_id_state
490
+ ])
491
+ self.action_panel_outputs_list.extend([self.plot_ui_objects.get(pc["id"], {}).get("panel_component", gr.update()) for pc in PLOT_CONFIGS])
492
+ self.action_panel_outputs_list.extend([self.plot_ui_objects.get(pc["id"], {}).get("bomb_button", gr.update()) for pc in PLOT_CONFIGS])
493
+ self.action_panel_outputs_list.extend([self.plot_ui_objects.get(pc["id"], {}).get("formula_button", gr.update()) for pc in PLOT_CONFIGS])
494
+ self.action_panel_outputs_list.extend([self.plot_ui_objects.get(pc["id"], {}).get("explore_button", gr.update()) for pc in PLOT_CONFIGS])
495
+ self.action_panel_outputs_list.extend([self.section_titles_map.get(s_name, gr.update()) for s_name in UNIQUE_ORDERED_SECTIONS])
496
+
497
+ # Outputs for _handle_explore_click
498
+ self.explore_outputs_list.extend([
499
+ self.explored_plot_id_state, self.global_actions_column_ui,
500
+ self.active_panel_action_state, self.formula_close_hint_md
501
+ ])
502
+ self.explore_outputs_list.extend([self.plot_ui_objects.get(pc["id"], {}).get("panel_component", gr.update()) for pc in PLOT_CONFIGS])
503
+ self.explore_outputs_list.extend([self.plot_ui_objects.get(pc["id"], {}).get("explore_button", gr.update()) for pc in PLOT_CONFIGS])
504
+ self.explore_outputs_list.extend([self.plot_ui_objects.get(pc["id"], {}).get("bomb_button", gr.update()) for pc in PLOT_CONFIGS]) # For resetting if panel closes
505
+ self.explore_outputs_list.extend([self.plot_ui_objects.get(pc["id"], {}).get("formula_button", gr.update()) for pc in PLOT_CONFIGS])# For resetting if panel closes
506
+ self.explore_outputs_list.extend([self.section_titles_map.get(s_name, gr.update()) for s_name in UNIQUE_ORDERED_SECTIONS])
507
+
508
+
509
+ def _setup_callbacks(self):
510
+ # Apply filter button
511
+ graph_refresh_inputs = [
512
+ self.token_state, self.date_filter_selector,
513
+ self.custom_start_date_picker, self.custom_end_date_picker,
514
+ self.chat_histories_st # Pass the state object itself
515
+ ]
516
+ self.apply_filter_btn.click(
517
+ fn=self._refresh_analytics_graphs_ui,
518
+ inputs=graph_refresh_inputs,
519
+ outputs=self.graph_refresh_outputs_list,
520
+ show_progress="full",
521
+ api_name="refresh_analytics_graphs_module"
522
+ )
523
+
524
+ # Panel action buttons (bomb, formula, explore)
525
+ action_click_inputs = [
526
+ self.active_panel_action_state, self.chat_histories_st,
527
+ self.current_chat_plot_id_st, self.plot_data_for_chatbot_st,
528
+ self.explored_plot_id_state
529
+ ]
530
+ explore_click_inputs = [self.explored_plot_id_state, self.active_panel_action_state]
531
+
532
+ for config_item in PLOT_CONFIGS:
533
+ plot_id = config_item["id"]
534
+ if plot_id in self.plot_ui_objects:
535
+ ui_obj = self.plot_ui_objects[plot_id]
536
+ if ui_obj.get("bomb_button"):
537
+ ui_obj["bomb_button"].click(
538
+ fn=self._create_panel_action_handler(plot_id, "insights"),
539
+ inputs=action_click_inputs,
540
+ outputs=self.action_panel_outputs_list,
541
+ api_name=f"action_insights_{plot_id}_module"
542
+ )
543
+ if ui_obj.get("formula_button"):
544
+ ui_obj["formula_button"].click(
545
+ fn=self._create_panel_action_handler(plot_id, "formula"),
546
+ inputs=action_click_inputs,
547
+ outputs=self.action_panel_outputs_list,
548
+ api_name=f"action_formula_{plot_id}_module"
549
+ )
550
+ if ui_obj.get("explore_button"):
551
+ # Lambda is okay here as p_id is captured correctly due to loop variable usage
552
+ ui_obj["explore_button"].click(
553
+ fn=lambda current_explored_val, current_active_panel_val, p_id=plot_id: self._handle_explore_click(p_id, current_explored_val, current_active_panel_val),
554
+ inputs=explore_click_inputs,
555
+ outputs=self.explore_outputs_list,
556
+ api_name=f"action_explore_{plot_id}_module"
557
+ )
558
+ else:
559
+ logging.warning(f"UI object for plot_id '{plot_id}' not found for click handlers in module.")
560
+
561
+ # Chat submission
562
+ chat_submission_outputs = [self.insights_chatbot_ui, self.insights_chat_input_ui, self.chat_histories_st]
563
+ chat_submission_inputs = [
564
+ self.insights_chat_input_ui, self.current_chat_plot_id_st,
565
+ self.chat_histories_st, self.plot_data_for_chatbot_st
566
+ ]
567
+ self.insights_chat_input_ui.submit(
568
+ fn=self._handle_chat_message_submission,
569
+ inputs=chat_submission_inputs,
570
+ outputs=chat_submission_outputs,
571
+ api_name="submit_chat_message_module"
572
+ )
573
+
574
+ # Suggested questions
575
+ suggestion_click_inputs_base = [
576
+ self.current_chat_plot_id_st, self.chat_histories_st, self.plot_data_for_chatbot_st
577
+ ]
578
+ self.insights_suggestion_1_btn.click(
579
+ fn=self._handle_suggested_question_click,
580
+ inputs=[self.insights_suggestion_1_btn] + suggestion_click_inputs_base, # Pass the button itself for its value
581
+ outputs=chat_submission_outputs,
582
+ api_name="click_suggestion_1_module"
583
+ )
584
+ self.insights_suggestion_2_btn.click(
585
+ fn=self._handle_suggested_question_click,
586
+ inputs=[self.insights_suggestion_2_btn] + suggestion_click_inputs_base,
587
+ outputs=chat_submission_outputs,
588
+ api_name="click_suggestion_2_module"
589
+ )
590
+ self.insights_suggestion_3_btn.click(
591
+ fn=self._handle_suggested_question_click,
592
+ inputs=[self.insights_suggestion_3_btn] + suggestion_click_inputs_base,
593
+ outputs=chat_submission_outputs,
594
+ api_name="click_suggestion_3_module"
595
+ )
596
+
597
+ def create_tab_ui(self):
598
+ # This method is called by the main app to build the UI for this tab
599
+ with gr.TabItem("2️⃣ Analisi Grafici", id="tab_analytics_module"): # Changed id to avoid conflict if old tab exists temporarily
600
+ gr.Markdown("## 📈 Analisi Performance LinkedIn")
601
+ gr.Markdown("Seleziona un intervallo di date per i grafici. Clicca i pulsanti (💣 Insights, ƒ Formula, 🧭 Esplora) su un grafico per azioni.")
602
+ self.analytics_status_md = gr.Markdown("Stato analisi grafici...")
603
+
604
+ with gr.Row():
605
+ self.date_filter_selector = gr.Radio(
606
+ ["Sempre", "Ultimi 7 Giorni", "Ultimi 30 Giorni", "Intervallo Personalizzato"],
607
+ label="Seleziona Intervallo Date per Grafici", value="Sempre", scale=3
608
+ )
609
+ with gr.Column(scale=2):
610
+ self.custom_start_date_picker = gr.DateTime(label="Data Inizio", visible=False, include_time=False, type="datetime")
611
+ self.custom_end_date_picker = gr.DateTime(label="Data Fine", visible=False, include_time=False, type="datetime")
612
+
613
+ self.apply_filter_btn = gr.Button("🔍 Applica Filtro & Aggiorna Grafici", variant="primary")
614
+
615
+ self.date_filter_selector.change(
616
+ fn=self._toggle_custom_date_pickers,
617
+ inputs=[self.date_filter_selector],
618
+ outputs=[self.custom_start_date_picker, self.custom_end_date_picker]
619
+ )
620
+
621
+ # Plot area and actions column
622
+ with gr.Row(equal_height=False):
623
+ with gr.Column(scale=8) as self.plots_area_col:
624
+ # Dynamically build plot UI objects and section titles
625
+ ui_elements_tuple = self.build_analytics_tab_plot_area(PLOT_CONFIGS) # Call injected function
626
+ if isinstance(ui_elements_tuple, tuple) and len(ui_elements_tuple) == 2:
627
+ self.plot_ui_objects, self.section_titles_map = ui_elements_tuple
628
+ # Verify section_titles_map completeness (optional, good for debugging)
629
+ if not all(sec_name in self.section_titles_map for sec_name in UNIQUE_ORDERED_SECTIONS):
630
+ logging.error("section_titles_map from build_analytics_tab_plot_area is incomplete in module.")
631
+ # Create placeholders if missing, to prevent errors with output lists
632
+ for sec_name in UNIQUE_ORDERED_SECTIONS:
633
+ if sec_name not in self.section_titles_map:
634
+ logging.warning(f"Creating fallback Markdown for missing section title: {sec_name}")
635
+ self.section_titles_map[sec_name] = gr.Markdown(f"### {sec_name} (Error Placeholder)")
636
+ else:
637
+ logging.error("build_analytics_tab_plot_area did not return a tuple of (plot_ui_objects, section_titles_map). Using fallback.")
638
+ # Fallback: try to use the result if it's a dict, otherwise empty
639
+ self.plot_ui_objects = ui_elements_tuple if isinstance(ui_elements_tuple, dict) else {}
640
+ for sec_name in UNIQUE_ORDERED_SECTIONS: # Ensure section_titles_map has entries
641
+ self.section_titles_map[sec_name] = gr.Markdown(f"### {sec_name} (Error Placeholder)")
642
+
643
+
644
+ with gr.Column(scale=4, visible=False) as self.global_actions_column_ui:
645
+ gr.Markdown("### 💡 Azioni Contestuali Grafico")
646
+ self.insights_chatbot_ui = gr.Chatbot(
647
+ label="Chat Insights", type="messages", height=450,
648
+ bubble_full_width=False, visible=False, show_label=False,
649
+ placeholder="L'analisi AI del grafico apparirà qui. Fai domande di approfondimento!"
650
+ )
651
+ self.insights_chat_input_ui = gr.Textbox(
652
+ label="La tua domanda:", placeholder="Chiedi all'AI riguardo a questo grafico...",
653
+ lines=2, visible=False, show_label=False
654
+ )
655
+ with gr.Row(visible=False) as self.insights_suggestions_row_ui:
656
+ self.insights_suggestion_1_btn = gr.Button(value="Suggerimento 1", size="sm", min_width=50)
657
+ self.insights_suggestion_2_btn = gr.Button(value="Suggerimento 2", size="sm", min_width=50)
658
+ self.insights_suggestion_3_btn = gr.Button(value="Suggerimento 3", size="sm", min_width=50)
659
+
660
+ self.formula_display_markdown_ui = gr.Markdown(
661
+ "I dettagli sulla formula/metodologia appariranno qui.", visible=False
662
+ )
663
+ self.formula_close_hint_md = gr.Markdown(
664
+ "<p style='font-size:0.9em; text-align:center; margin-top:10px;'><em>Click the active ƒ button on the plot again to close this panel.</em></p>",
665
+ visible=False
666
+ )
667
+
668
+ # After all UI components are defined, populate the output lists
669
+ self._define_callback_outputs()
670
+ # Then, set up the callbacks that use these components and output lists
671
+ self._setup_callbacks()
672
+
673
+ # The method doesn't need to return anything as it modifies the Gradio app context directly.
674
+ # The main app will access necessary components via the instance attributes if needed (e.g., for .then() inputs).