GuglielmoTor commited on
Commit
e5163d2
·
verified ·
1 Parent(s): 9cccb2a

Create analytics_handlers.py

Browse files
Files changed (1) hide show
  1. services/analytics_handlers.py +736 -0
services/analytics_handlers.py ADDED
@@ -0,0 +1,736 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # chat_histories_st is a state, its value will be accessed via self.chat_histories_st.value
99
+ ):
100
+ logging.info(f"Refreshing analytics graph UI. Filter: {date_filter_val}. Token set: {'yes' if current_token_state_val.get('token') else 'no'}")
101
+ start_time = time.time()
102
+
103
+ # Call the function that generates plot figures and summaries
104
+ # Ensure update_analytics_plots_figures is adapted to return:
105
+ # status_msg (str), figures_dict (dict: {plot_id: fig}), summaries_dict (dict: {plot_id: summary_text})
106
+ plot_gen_results = update_analytics_plots_figures(
107
+ current_token_state_val,
108
+ date_filter_val,
109
+ custom_start_val,
110
+ custom_end_val,
111
+ self.plot_configs # Pass plot_configs to it
112
+ )
113
+ # Expected: status_msg, list_of_figures, dict_of_plot_summaries
114
+ # Original: status_msg, gen_figs (list), new_summaries (dict)
115
+
116
+ status_msg = plot_gen_results[0]
117
+ gen_figs_list = plot_gen_results[1] # This should be a list of figures in order of plot_configs
118
+ new_summaries_dict = plot_gen_results[2] # This should be a dict {plot_id: summary}
119
+
120
+ all_updates = [gr.update(value=status_msg)] # For analytics_status_md
121
+
122
+ # Update plot components with new figures
123
+ if len(gen_figs_list) == len(self.plot_configs):
124
+ for fig in gen_figs_list:
125
+ all_updates.append(fig) # fig itself is the update for gr.Plot
126
+ else:
127
+ logging.error(f"Figure list length mismatch: got {len(gen_figs_list)}, expected {len(self.plot_configs)}")
128
+ for _ in self.plot_configs:
129
+ all_updates.append(create_placeholder_plot("Error", "Figura mancante"))
130
+
131
+ # Reset action panel UI elements
132
+ all_updates.extend([
133
+ gr.update(visible=False), # global_actions_column_ui
134
+ gr.update(value=[], visible=False), # insights_chatbot_ui (value and visibility)
135
+ gr.update(value="", visible=False), # insights_chat_input_ui (value and visibility)
136
+ gr.update(visible=False), # insights_suggestions_row_ui
137
+ gr.update(value="S1"), # insights_suggestion_1_btn
138
+ gr.update(value="S2"), # insights_suggestion_2_btn
139
+ gr.update(value="S3"), # insights_suggestion_3_btn
140
+ gr.update(value="Formula details here.", visible=False), # formula_display_markdown_ui
141
+ gr.update(visible=False) # formula_close_hint_md
142
+ ])
143
+
144
+ # Reset states
145
+ all_updates.extend([
146
+ None, # active_panel_action_state
147
+ None, # current_chat_plot_id_st
148
+ {}, # chat_histories_st (reset to empty dict)
149
+ new_summaries_dict # plot_data_for_chatbot_st (update with new summaries)
150
+ ])
151
+
152
+ # Reset buttons and panel visibility for each plot
153
+ for _ in self.plot_configs:
154
+ all_updates.extend([
155
+ gr.update(value=BOMB_ICON), # bomb_button
156
+ gr.update(value=FORMULA_ICON), # formula_button
157
+ gr.update(value=EXPLORE_ICON), # explore_button
158
+ gr.update(visible=True) # panel_component (plot visibility)
159
+ ])
160
+
161
+ all_updates.append(None) # explored_plot_id_state (reset)
162
+
163
+ # Reset section title visibility
164
+ for _ in self.unique_ordered_sections:
165
+ all_updates.append(gr.update(visible=True))
166
+
167
+ end_time = time.time()
168
+ logging.info(f"Analytics graph refresh processing took {end_time - start_time:.2f} seconds.")
169
+
170
+ expected_len = 1 + len(self.plot_configs) + 9 + 4 + (4 * len(self.plot_configs)) + 1 + self.num_unique_sections
171
+ logging.info(f"Prepared {len(all_updates)} updates for graph refresh. Expected {expected_len}.")
172
+ if len(all_updates) != expected_len:
173
+ logging.error(f"Output length mismatch in refresh_analytics_graphs_ui. Got {len(all_updates)}, expected {expected_len}")
174
+ # Pad with gr.update() if lengths don't match, to avoid Gradio errors, though this indicates a logic flaw.
175
+ all_updates.extend([gr.update()] * (expected_len - len(all_updates)))
176
+
177
+ return tuple(all_updates)
178
+
179
+ def _get_action_panel_outputs_list(self):
180
+ """Helper to construct the list of outputs for panel actions (insights, formula)."""
181
+ outputs = [
182
+ self.components['global_actions_column_ui'],
183
+ self.components['insights_chatbot_ui'], # For visibility
184
+ self.components['insights_chatbot_ui'], # For value
185
+ self.components['insights_chat_input_ui'],
186
+ self.components['insights_suggestions_row_ui'],
187
+ self.components['insights_suggestion_1_btn'],
188
+ self.components['insights_suggestion_2_btn'],
189
+ self.components['insights_suggestion_3_btn'],
190
+ self.components['formula_display_markdown_ui'], # For visibility
191
+ self.components['formula_display_markdown_ui'], # For value
192
+ self.components['formula_close_hint_md'],
193
+ ]
194
+ outputs.extend([
195
+ self.active_panel_action_state,
196
+ self.current_chat_plot_id_st,
197
+ self.chat_histories_st,
198
+ self.explored_plot_id_state
199
+ ])
200
+
201
+ for pc in self.plot_configs:
202
+ ui_obj = self.plot_ui_objects.get(pc["id"], {})
203
+ outputs.append(ui_obj.get("panel_component", gr.update())) # Plot panel visibility
204
+ outputs.append(ui_obj.get("bomb_button", gr.update()))
205
+ outputs.append(ui_obj.get("formula_button", gr.update()))
206
+ outputs.append(ui_obj.get("explore_button", gr.update()))
207
+
208
+ for s_name in self.unique_ordered_sections:
209
+ outputs.append(self.section_titles_map.get(s_name, gr.update())) # Section title visibility
210
+
211
+ expected_len = 11 + 4 + (4 * len(self.plot_configs)) + self.num_unique_sections
212
+ logging.debug(f"Action panel outputs list length: {len(outputs)}, Expected: {expected_len}")
213
+ return outputs
214
+
215
+ async def handle_panel_action(self, plot_id_clicked: str, action_type: str,
216
+ current_active_action_from_state: dict, # This is a direct value from gr.State
217
+ current_chat_histories: dict, # This is a direct value
218
+ current_chat_plot_id: str, # This is a direct value
219
+ current_plot_data_for_chatbot: dict, # This is a direct value
220
+ current_explored_plot_id: str # This is a direct value
221
+ ):
222
+ logging.info(f"Panel Action: '{action_type}' for plot '{plot_id_clicked}'. Active: {current_active_action_from_state}, Explored: {current_explored_plot_id}")
223
+
224
+ clicked_plot_config = next((p for p in self.plot_configs if p["id"] == plot_id_clicked), None)
225
+ if not clicked_plot_config:
226
+ logging.error(f"Config not found for plot_id {plot_id_clicked}")
227
+ # Construct a list of gr.update() of the correct length
228
+ num_outputs = len(self._get_action_panel_outputs_list())
229
+ error_updates = [gr.update()] * num_outputs
230
+ # Try to preserve existing state values if possible by updating specific indices
231
+ # This part is tricky without knowing the exact order and meaning of each output.
232
+ # For simplicity, returning all gr.update() might be safer if an error occurs early.
233
+ # Or, more robustly, identify which states need to be passed through.
234
+ # Indices for states in action_panel_outputs_list:
235
+ # active_panel_action_state is at index 11
236
+ # current_chat_plot_id_st is at index 12
237
+ # chat_histories_st is at index 13
238
+ # explored_plot_id_state is at index 14
239
+ error_updates[11] = current_active_action_from_state
240
+ error_updates[12] = current_chat_plot_id
241
+ error_updates[13] = current_chat_histories
242
+ error_updates[14] = current_explored_plot_id
243
+ return tuple(error_updates)
244
+
245
+ clicked_plot_label = clicked_plot_config["label"]
246
+ clicked_plot_section = clicked_plot_config["section"]
247
+
248
+ hypothetical_new_active_state = {"plot_id": plot_id_clicked, "type": action_type}
249
+ is_toggling_off = current_active_action_from_state == hypothetical_new_active_state
250
+
251
+ action_col_visible_update = gr.update(visible=False)
252
+ insights_chatbot_visible_update = gr.update(visible=False)
253
+ insights_chat_input_visible_update = gr.update(visible=False)
254
+ insights_suggestions_row_visible_update = gr.update(visible=False)
255
+ formula_display_visible_update = gr.update(visible=False)
256
+ formula_close_hint_visible_update = gr.update(visible=False)
257
+
258
+ chatbot_content_update = gr.update()
259
+ s1_upd, s2_upd, s3_upd = gr.update(), gr.update(), gr.update()
260
+ formula_content_update = gr.update()
261
+
262
+ new_active_action_state_to_set = None # This will be the new value for the gr.State
263
+ new_current_chat_plot_id = current_chat_plot_id # Default to existing
264
+ updated_chat_histories = current_chat_histories # Default to existing
265
+ new_explored_plot_id_to_set = current_explored_plot_id # Default to existing
266
+
267
+ generated_panel_vis_updates = [] # For individual plot panels
268
+ generated_bomb_btn_updates = []
269
+ generated_formula_btn_updates = []
270
+ generated_explore_btn_updates = []
271
+ section_title_vis_updates = [gr.update()] * self.num_unique_sections
272
+
273
+ if is_toggling_off:
274
+ new_active_action_state_to_set = None
275
+ action_col_visible_update = gr.update(visible=False)
276
+ logging.info(f"Toggling OFF panel {action_type} for {plot_id_clicked}.")
277
+
278
+ for _ in self.plot_configs:
279
+ generated_bomb_btn_updates.append(gr.update(value=BOMB_ICON))
280
+ generated_formula_btn_updates.append(gr.update(value=FORMULA_ICON))
281
+
282
+ if current_explored_plot_id: # If an explore view is active, restore it
283
+ explored_cfg = next((p for p in self.plot_configs if p["id"] == current_explored_plot_id), None)
284
+ explored_sec = explored_cfg["section"] if explored_cfg else None
285
+ for i, sec_name in enumerate(self.unique_ordered_sections):
286
+ section_title_vis_updates[i] = gr.update(visible=(sec_name == explored_sec))
287
+ for cfg in self.plot_configs:
288
+ is_exp = (cfg["id"] == current_explored_plot_id)
289
+ generated_panel_vis_updates.append(gr.update(visible=is_exp))
290
+ generated_explore_btn_updates.append(gr.update(value=ACTIVE_ICON if is_exp else EXPLORE_ICON))
291
+ else: # No explore view, all plots/sections visible
292
+ for i in range(self.num_unique_sections):
293
+ section_title_vis_updates[i] = gr.update(visible=True)
294
+ for _ in self.plot_configs:
295
+ generated_panel_vis_updates.append(gr.update(visible=True))
296
+ generated_explore_btn_updates.append(gr.update(value=EXPLORE_ICON))
297
+
298
+ if action_type == "insights":
299
+ new_current_chat_plot_id = None # Clear chat context if insights panel is closed
300
+
301
+ else: # Toggling ON a new action or switching actions
302
+ new_active_action_state_to_set = hypothetical_new_active_state
303
+ action_col_visible_update = gr.update(visible=True)
304
+ new_explored_plot_id_to_set = None # Cancel any explore view
305
+ logging.info(f"Toggling ON panel {action_type} for {plot_id_clicked}. Cancelling explore view if any.")
306
+
307
+ # Show only the section of the clicked plot
308
+ for i, sec_name in enumerate(self.unique_ordered_sections):
309
+ section_title_vis_updates[i] = gr.update(visible=(sec_name == clicked_plot_section))
310
+
311
+ # Show only the clicked plot's panel, update explore buttons to non-active
312
+ for cfg in self.plot_configs:
313
+ generated_panel_vis_updates.append(gr.update(visible=(cfg["id"] == plot_id_clicked)))
314
+ generated_explore_btn_updates.append(gr.update(value=EXPLORE_ICON)) # Reset all explore to inactive
315
+
316
+ # Update bomb and formula buttons based on the new active action
317
+ for cfg_btn in self.plot_configs:
318
+ is_active_insights = (new_active_action_state_to_set["plot_id"] == cfg_btn["id"] and new_active_action_state_to_set["type"] == "insights")
319
+ is_active_formula = (new_active_action_state_to_set["plot_id"] == cfg_btn["id"] and new_active_action_state_to_set["type"] == "formula")
320
+ generated_bomb_btn_updates.append(gr.update(value=ACTIVE_ICON if is_active_insights else BOMB_ICON))
321
+ generated_formula_btn_updates.append(gr.update(value=ACTIVE_ICON if is_active_formula else FORMULA_ICON))
322
+
323
+ if action_type == "insights":
324
+ insights_chatbot_visible_update = gr.update(visible=True)
325
+ insights_chat_input_visible_update = gr.update(visible=True)
326
+ insights_suggestions_row_visible_update = gr.update(visible=True)
327
+ new_current_chat_plot_id = plot_id_clicked # Set chat context
328
+
329
+ history = current_chat_histories.get(plot_id_clicked, [])
330
+ summary_for_plot = current_plot_data_for_chatbot.get(plot_id_clicked, f"Nessun sommario disponibile per '{clicked_plot_label}'.")
331
+
332
+ if not history: # First time opening insights for this plot (or after a refresh)
333
+ prompt, sugg = get_initial_insight_prompt_and_suggestions(plot_id_clicked, clicked_plot_label, summary_for_plot)
334
+ # Gradio's chatbot expects a list of lists/tuples: [[user_msg, None], [None, assistant_msg]]
335
+ # Our generate_llm_response and history uses: [{"role": "user", "content": prompt}, {"role": "assistant", "content": resp}]
336
+ # We need to adapt. For now, let's assume generate_llm_response takes our format and returns a string.
337
+ # The history for Gradio Chatbot component needs to be [[user_msg, assistant_msg], ...]
338
+ # Let's build history for LLM first
339
+ llm_history_for_generation = [{"role": "user", "content": prompt}]
340
+
341
+ # Display "Thinking..." or similar
342
+ chatbot_content_update = gr.update(value=[[prompt, "Sto pensando..."]])
343
+ yield tuple(self._assemble_panel_action_updates(action_col_visible_update, insights_chatbot_visible_update, chatbot_content_update,
344
+ insights_chat_input_visible_update, insights_suggestions_row_visible_update,
345
+ s1_upd, s2_upd, s3_upd, formula_display_visible_update, formula_content_update,
346
+ formula_close_hint_visible_update, new_active_action_state_to_set,
347
+ new_current_chat_plot_id, updated_chat_histories, new_explored_plot_id_to_set,
348
+ generated_panel_vis_updates, generated_bomb_btn_updates,
349
+ generated_formula_btn_updates, generated_explore_btn_updates, section_title_vis_updates))
350
+
351
+
352
+ resp_text = await generate_llm_response(prompt, plot_id_clicked, clicked_plot_label, llm_history_for_generation, summary_for_plot)
353
+
354
+ # Gradio chatbot history format
355
+ new_gr_history_for_plot = [[prompt, resp_text]]
356
+ # Internal history format for re-sending to LLM
357
+ new_internal_history_for_plot = [
358
+ {"role": "user", "content": prompt},
359
+ {"role": "assistant", "content": resp_text}
360
+ ]
361
+ updated_chat_histories = {**current_chat_histories, plot_id_clicked: new_internal_history_for_plot}
362
+ chatbot_content_update = gr.update(value=new_gr_history_for_plot)
363
+ else: # History exists, just display it
364
+ _, sugg = get_initial_insight_prompt_and_suggestions(plot_id_clicked, clicked_plot_label, summary_for_plot) # Get fresh suggestions
365
+ # Convert internal history to Gradio format for display
366
+ gr_history_to_display = []
367
+ # Assuming history is [{"role":"user", "content":"..."}, {"role":"assistant", "content":"..."}]
368
+ # We need to pair them up. If an odd number, the last user message might not have a pair yet.
369
+ temp_hist = history[:] # Make a copy
370
+ while temp_hist:
371
+ user_turn = temp_hist.pop(0)
372
+ assistant_turn = None
373
+ if temp_hist and temp_hist[0]["role"] == "assistant":
374
+ assistant_turn = temp_hist.pop(0)
375
+ gr_history_to_display.append([user_turn["content"], assistant_turn["content"] if assistant_turn else None])
376
+
377
+ chatbot_content_update = gr.update(value=gr_history_to_display)
378
+
379
+ s1_upd = gr.update(value=sugg[0] if sugg and len(sugg) > 0 else "N/A")
380
+ s2_upd = gr.update(value=sugg[1] if sugg and len(sugg) > 1 else "N/A")
381
+ s3_upd = gr.update(value=sugg[2] if sugg and len(sugg) > 2 else "N/A")
382
+
383
+ elif action_type == "formula":
384
+ formula_display_visible_update = gr.update(visible=True)
385
+ formula_close_hint_visible_update = gr.update(visible=True)
386
+ formula_key = PLOT_ID_TO_FORMULA_KEY_MAP.get(plot_id_clicked)
387
+ formula_text = f"**Formula/Methodology for: {clicked_plot_label}** (ID: `{plot_id_clicked}`)\n\n"
388
+ if formula_key and formula_key in PLOT_FORMULAS:
389
+ formula_data = PLOT_FORMULAS[formula_key]
390
+ formula_text += f"### {formula_data['title']}\n\n{formula_data['description']}\n\n"
391
+ if 'calculation_steps' in formula_data and formula_data['calculation_steps']:
392
+ formula_text += "**Calculation:**\n" + "\n".join([f"- {s}" for s in formula_data['calculation_steps']])
393
+ else:
394
+ formula_text += "(No detailed formula information found.)"
395
+ formula_content_update = gr.update(value=formula_text)
396
+ new_current_chat_plot_id = None # Clear chat context if formula panel is opened
397
+
398
+ final_updates_tuple = self._assemble_panel_action_updates(
399
+ action_col_visible_update, insights_chatbot_visible_update, chatbot_content_update,
400
+ insights_chat_input_visible_update, insights_suggestions_row_visible_update,
401
+ s1_upd, s2_upd, s3_upd, formula_display_visible_update, formula_content_update,
402
+ formula_close_hint_visible_update, new_active_action_state_to_set,
403
+ new_current_chat_plot_id, updated_chat_histories, new_explored_plot_id_to_set,
404
+ generated_panel_vis_updates, generated_bomb_btn_updates,
405
+ generated_formula_btn_updates, generated_explore_btn_updates, section_title_vis_updates
406
+ )
407
+ logging.debug(f"handle_panel_action returning {len(final_updates_tuple)} updates.")
408
+ yield final_updates_tuple
409
+
410
+
411
+ def _assemble_panel_action_updates(self, 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
+ """Helper to assemble the final tuple of updates for handle_panel_action."""
419
+ final_updates_list = [
420
+ action_col_visible_update, # global_actions_column_ui (visibility)
421
+ insights_chatbot_visible_update, # insights_chatbot_ui (visibility)
422
+ chatbot_content_update, # insights_chatbot_ui (value)
423
+ insights_chat_input_visible_update, # insights_chat_input_ui
424
+ insights_suggestions_row_visible_update, # insights_suggestions_row_ui
425
+ s1_upd, # insights_suggestion_1_btn
426
+ s2_upd, # insights_suggestion_2_btn
427
+ s3_upd, # insights_suggestion_3_btn
428
+ formula_display_visible_update, # formula_display_markdown_ui (visibility)
429
+ formula_content_update, # formula_display_markdown_ui (value)
430
+ formula_close_hint_visible_update, # formula_close_hint_md
431
+
432
+ # States
433
+ new_active_action_state_to_set, # active_panel_action_state
434
+ new_current_chat_plot_id, # current_chat_plot_id_st
435
+ updated_chat_histories, # chat_histories_st
436
+ new_explored_plot_id_to_set # explored_plot_id_state
437
+ ]
438
+ final_updates_list.extend(generated_panel_vis_updates)
439
+ final_updates_list.extend(generated_bomb_btn_updates)
440
+ final_updates_list.extend(generated_formula_btn_updates)
441
+ final_updates_list.extend(generated_explore_btn_updates)
442
+ final_updates_list.extend(section_title_vis_updates)
443
+
444
+ expected_len = len(self._get_action_panel_outputs_list())
445
+ if len(final_updates_list) != expected_len:
446
+ logging.error(f"Output length mismatch in _assemble_panel_action_updates. Got {len(final_updates_list)}, expected {expected_len}")
447
+ # Pad if necessary, though this is a bug indicator
448
+ final_updates_list.extend([gr.update()] * (expected_len - len(final_updates_list)))
449
+
450
+ return tuple(final_updates_list)
451
+
452
+
453
+ async def handle_chat_message_submission(self, user_message: str, current_plot_id: str,
454
+ chat_histories: dict, current_plot_data_for_chatbot: dict):
455
+ if not current_plot_id or not user_message.strip():
456
+ # Get current Gradio history for the plot_id to display
457
+ internal_history_for_plot = chat_histories.get(current_plot_id, [])
458
+ gr_history_display = self._convert_internal_to_gradio_chat_history(internal_history_for_plot)
459
+ yield gr_history_display, gr.update(value=""), chat_histories
460
+ return
461
+
462
+ clicked_plot_config = next((p for p in self.plot_configs if p["id"] == current_plot_id), None)
463
+ plot_label = clicked_plot_config["label"] if clicked_plot_config else "Selected Plot"
464
+ summary_for_plot = current_plot_data_for_chatbot.get(current_plot_id, f"No summary for '{plot_label}'.")
465
+
466
+ internal_history_for_plot = chat_histories.get(current_plot_id, []).copy() # Get a mutable copy
467
+ internal_history_for_plot.append({"role": "user", "content": user_message})
468
+
469
+ # Update Gradio chat display: User message + "Thinking..."
470
+ gr_history_display_pending = self._convert_internal_to_gradio_chat_history(internal_history_for_plot, thinking=True)
471
+ yield gr_history_display_pending, gr.update(value=""), chat_histories # Show user message immediately
472
+
473
+ # Generate LLM response
474
+ llm_response_text = await generate_llm_response(user_message, current_plot_id, plot_label, internal_history_for_plot, summary_for_plot)
475
+
476
+ internal_history_for_plot.append({"role": "assistant", "content": llm_response_text})
477
+
478
+ updated_chat_histories = {**chat_histories, current_plot_id: internal_history_for_plot}
479
+
480
+ # Final Gradio chat display with LLM response
481
+ final_gr_history_display = self._convert_internal_to_gradio_chat_history(internal_history_for_plot)
482
+ yield final_gr_history_display, "", updated_chat_histories
483
+
484
+ def _convert_internal_to_gradio_chat_history(self, internal_history, thinking=False):
485
+ """Converts internal chat history format to Gradio's [[user, assistant], ...] format."""
486
+ gradio_history = []
487
+ temp_hist = internal_history[:] # Make a copy
488
+ while temp_hist:
489
+ user_msg_obj = temp_hist.pop(0)
490
+ user_msg = user_msg_obj['content']
491
+ assistant_msg = None
492
+ if temp_hist and temp_hist[0]['role'] == 'assistant':
493
+ assistant_msg_obj = temp_hist.pop(0)
494
+ assistant_msg = assistant_msg_obj['content']
495
+ gradio_history.append([user_msg, assistant_msg])
496
+
497
+ if thinking and gradio_history and gradio_history[-1][1] is None: # If last message was user and we are in 'thinking' mode
498
+ gradio_history[-1][1] = "Sto pensando..." # Replace None with "Thinking..."
499
+ elif thinking and not gradio_history: # Should not happen if user_message was added
500
+ pass
501
+
502
+
503
+ return gradio_history
504
+
505
+ async def handle_suggested_question_click(self, suggestion_text: str, current_plot_id: str,
506
+ chat_histories: dict, current_plot_data_for_chatbot: dict):
507
+ if not current_plot_id or not suggestion_text.strip() or suggestion_text == "N/A":
508
+ internal_history_for_plot = chat_histories.get(current_plot_id, [])
509
+ gr_history_display = self._convert_internal_to_gradio_chat_history(internal_history_for_plot)
510
+ yield gr_history_display, gr.update(value=""), chat_histories
511
+ return
512
+
513
+ # Use the existing chat submission logic
514
+ async for update_chunk in self.handle_chat_message_submission(suggestion_text, current_plot_id, chat_histories, current_plot_data_for_chatbot):
515
+ yield update_chunk
516
+
517
+ def _get_explore_outputs_list(self):
518
+ """Helper to construct the list of outputs for explore actions."""
519
+ outputs = [
520
+ self.explored_plot_id_state,
521
+ self.components['global_actions_column_ui'], # For visibility
522
+ self.active_panel_action_state, # To potentially clear it
523
+ self.components['formula_close_hint_md'] # For visibility
524
+ ]
525
+
526
+ for pc in self.plot_configs: # Plot panel visibility
527
+ outputs.append(self.plot_ui_objects.get(pc["id"], {}).get("panel_component", gr.update()))
528
+ for pc in self.plot_configs: # Explore button state
529
+ outputs.append(self.plot_ui_objects.get(pc["id"], {}).get("explore_button", gr.update()))
530
+ for pc in self.plot_configs: # Bomb button state (may need reset)
531
+ outputs.append(self.plot_ui_objects.get(pc["id"], {}).get("bomb_button", gr.update()))
532
+ for pc in self.plot_configs: # Formula button state (may need reset)
533
+ outputs.append(self.plot_ui_objects.get(pc["id"], {}).get("formula_button", gr.update()))
534
+
535
+ for s_name in self.unique_ordered_sections: # Section title visibility
536
+ outputs.append(self.section_titles_map.get(s_name, gr.update()))
537
+
538
+ expected_len = 4 + (4 * len(self.plot_configs)) + self.num_unique_sections
539
+ logging.debug(f"Explore outputs list length: {len(outputs)}, Expected: {expected_len}")
540
+ return outputs
541
+
542
+ def handle_explore_click(self, plot_id_clicked: str, current_explored_plot_id_from_state: str,
543
+ current_active_panel_action_state: dict):
544
+ logging.info(f"Explore Click: Plot '{plot_id_clicked}'. Current Explored: {current_explored_plot_id_from_state}. Active Panel: {current_active_panel_action_state}")
545
+
546
+ if not self.plot_ui_objects or not self.section_titles_map:
547
+ logging.error("plot_ui_objects or section_titles_map not populated for handle_explore_click.")
548
+ num_outputs = len(self._get_explore_outputs_list())
549
+ error_updates = [gr.update()] * num_outputs
550
+ error_updates[0] = current_explored_plot_id_from_state # Preserve explored_id_state
551
+ error_updates[2] = current_active_panel_action_state # Preserve active_panel_state
552
+ return tuple(error_updates)
553
+
554
+ new_explored_id_to_set = None
555
+ is_toggling_off_explore = (plot_id_clicked == current_explored_plot_id_from_state)
556
+
557
+ action_col_upd = gr.update() # Default no change
558
+ new_active_panel_state_upd = current_active_panel_action_state # Default no change
559
+ formula_hint_upd = gr.update(visible=False) # Default hide
560
+
561
+ panel_vis_updates = []
562
+ explore_btns_updates = []
563
+ bomb_btns_updates = [gr.update()] * len(self.plot_configs) # Default no change
564
+ formula_btns_updates = [gr.update()] * len(self.plot_configs) # Default no change
565
+ section_title_vis_updates = [gr.update()] * self.num_unique_sections
566
+
567
+ clicked_cfg = next((p for p in self.plot_configs if p["id"] == plot_id_clicked), None)
568
+ section_of_clicked_plot = clicked_cfg["section"] if clicked_cfg else None
569
+
570
+ if is_toggling_off_explore:
571
+ new_explored_id_to_set = None # Clear explore state
572
+ logging.info(f"Stopping explore for {plot_id_clicked}. All plots/sections to be visible.")
573
+ for i in range(self.num_unique_sections):
574
+ section_title_vis_updates[i] = gr.update(visible=True)
575
+ for _ in self.plot_configs:
576
+ panel_vis_updates.append(gr.update(visible=True))
577
+ explore_btns_updates.append(gr.update(value=EXPLORE_ICON))
578
+ # Bomb and formula buttons remain as they were unless an action panel was closed (handled below if current_active_panel_action_state was set)
579
+
580
+ else: # Starting explore or switching explored plot
581
+ new_explored_id_to_set = plot_id_clicked
582
+ logging.info(f"Exploring {plot_id_clicked}. Hiding other plots/sections.")
583
+ for i, sec_name in enumerate(self.unique_ordered_sections):
584
+ section_title_vis_updates[i] = gr.update(visible=(sec_name == section_of_clicked_plot))
585
+ for cfg in self.plot_configs:
586
+ is_target = (cfg["id"] == new_explored_id_to_set)
587
+ panel_vis_updates.append(gr.update(visible=is_target))
588
+ explore_btns_updates.append(gr.update(value=ACTIVE_ICON if is_target else EXPLORE_ICON))
589
+
590
+ if current_active_panel_action_state: # If an action panel (insights/formula) is open, close it
591
+ logging.info("Closing active insight/formula panel due to explore click.")
592
+ action_col_upd = gr.update(visible=False)
593
+ new_active_panel_state_upd = None # Clear active panel state
594
+ formula_hint_upd = gr.update(visible=False) # Hide formula hint specifically
595
+ # Reset bomb and formula buttons to their default icons
596
+ bomb_btns_updates = [gr.update(value=BOMB_ICON) for _ in self.plot_configs]
597
+ formula_btns_updates = [gr.update(value=FORMULA_ICON) for _ in self.plot_configs]
598
+
599
+ final_explore_updates_list = [
600
+ new_explored_id_to_set,
601
+ action_col_upd,
602
+ new_active_panel_state_upd,
603
+ formula_hint_upd
604
+ ]
605
+ final_explore_updates_list.extend(panel_vis_updates)
606
+ final_explore_updates_list.extend(explore_btns_updates)
607
+ final_explore_updates_list.extend(bomb_btns_updates)
608
+ final_explore_updates_list.extend(formula_btns_updates)
609
+ final_explore_updates_list.extend(section_title_vis_updates)
610
+
611
+ expected_len = len(self._get_explore_outputs_list())
612
+ if len(final_explore_updates_list) != expected_len:
613
+ logging.error(f"Output length mismatch in handle_explore_click. Got {len(final_explore_updates_list)}, expected {expected_len}")
614
+ final_explore_updates_list.extend([gr.update()] * (expected_len - len(final_explore_updates_list)))
615
+
616
+ return tuple(final_explore_updates_list)
617
+
618
+ def setup_event_handlers(self):
619
+ """Set up all event handlers for the analytics tab components."""
620
+ logging.info("Setting up analytics event handlers.")
621
+
622
+ # Apply filter button
623
+ apply_filter_inputs = [
624
+ self.token_state,
625
+ self.components['date_filter_selector'],
626
+ self.components['custom_start_date_picker'],
627
+ self.components['custom_end_date_picker'],
628
+ # self.chat_histories_st # Not directly an input to refresh_analytics_graphs_ui, it's accessed via self
629
+ ]
630
+ self.components['apply_filter_btn'].click(
631
+ fn=self.refresh_analytics_graphs_ui,
632
+ inputs=apply_filter_inputs,
633
+ outputs=self._get_graph_refresh_outputs_list(), # Method returns the list of components
634
+ show_progress="full",
635
+ api_name="refresh_analytics_graphs"
636
+ )
637
+
638
+ # Plot action handlers (insights, formula, explore)
639
+ action_click_inputs = [ # These are the gr.State objects themselves
640
+ self.active_panel_action_state,
641
+ self.chat_histories_st,
642
+ self.current_chat_plot_id_st,
643
+ self.plot_data_for_chatbot_st,
644
+ self.explored_plot_id_state
645
+ ]
646
+
647
+ explore_click_inputs = [ # gr.State objects
648
+ self.explored_plot_id_state,
649
+ self.active_panel_action_state
650
+ ]
651
+
652
+ action_panel_outputs_list = self._get_action_panel_outputs_list()
653
+ explore_outputs_list = self._get_explore_outputs_list()
654
+
655
+ for config_item in self.plot_configs:
656
+ plot_id = config_item["id"]
657
+ if plot_id in self.plot_ui_objects:
658
+ ui_obj = self.plot_ui_objects[plot_id]
659
+
660
+ # Curry plot_id and action_type for the handler
661
+ # The handler function itself (self.handle_panel_action) will receive the values from the gr.State inputs directly.
662
+
663
+ if ui_obj.get("bomb_button"):
664
+ ui_obj["bomb_button"].click(
665
+ fn=lambda current_active, current_chats, current_chat_pid, current_plot_data, current_explored, p_id=plot_id: \
666
+ self.handle_panel_action(p_id, "insights", current_active, current_chats, current_chat_pid, current_plot_data, current_explored),
667
+ inputs=action_click_inputs, # Pass the list of gr.State objects
668
+ outputs=action_panel_outputs_list,
669
+ api_name=f"action_insights_{plot_id}"
670
+ )
671
+ if ui_obj.get("formula_button"):
672
+ ui_obj["formula_button"].click(
673
+ fn=lambda current_active, current_chats, current_chat_pid, current_plot_data, current_explored, p_id=plot_id: \
674
+ self.handle_panel_action(p_id, "formula", current_active, current_chats, current_chat_pid, current_plot_data, current_explored),
675
+ inputs=action_click_inputs,
676
+ outputs=action_panel_outputs_list,
677
+ api_name=f"action_formula_{plot_id}"
678
+ )
679
+ if ui_obj.get("explore_button"):
680
+ ui_obj["explore_button"].click(
681
+ fn=lambda current_explored_val, current_active_panel_val, p_id=plot_id: \
682
+ self.handle_explore_click(p_id, current_explored_val, current_active_panel_val),
683
+ inputs=explore_click_inputs, # Pass the list of gr.State objects
684
+ outputs=explore_outputs_list,
685
+ api_name=f"action_explore_{plot_id}"
686
+ )
687
+ else:
688
+ logging.warning(f"UI object for plot_id '{plot_id}' not found for setting up click handlers.")
689
+
690
+ # Chat submission handlers
691
+ chat_submission_outputs = [
692
+ self.components['insights_chatbot_ui'],
693
+ self.components['insights_chat_input_ui'],
694
+ self.chat_histories_st # This state will be updated
695
+ ]
696
+ chat_submission_inputs = [ # gr.Textbox, gr.State, gr.State, gr.State
697
+ self.components['insights_chat_input_ui'],
698
+ self.current_chat_plot_id_st,
699
+ self.chat_histories_st,
700
+ self.plot_data_for_chatbot_st
701
+ ]
702
+
703
+ self.components['insights_chat_input_ui'].submit(
704
+ fn=self.handle_chat_message_submission,
705
+ inputs=chat_submission_inputs,
706
+ outputs=chat_submission_outputs,
707
+ api_name="submit_chat_message"
708
+ )
709
+
710
+ suggestion_click_inputs_base = [ # gr.State, gr.State, gr.State
711
+ self.current_chat_plot_id_st,
712
+ self.chat_histories_st,
713
+ self.plot_data_for_chatbot_st
714
+ ]
715
+
716
+ # For suggestion buttons, the first input is the button itself (to get its value)
717
+ self.components['insights_suggestion_1_btn'].click(
718
+ fn=self.handle_suggested_question_click,
719
+ inputs=[self.components['insights_suggestion_1_btn']] + suggestion_click_inputs_base,
720
+ outputs=chat_submission_outputs,
721
+ api_name="click_suggestion_1"
722
+ )
723
+ self.components['insights_suggestion_2_btn'].click(
724
+ fn=self.handle_suggested_question_click,
725
+ inputs=[self.components['insights_suggestion_2_btn']] + suggestion_click_inputs_base,
726
+ outputs=chat_submission_outputs,
727
+ api_name="click_suggestion_2"
728
+ )
729
+ self.components['insights_suggestion_3_btn'].click(
730
+ fn=self.handle_suggested_question_click,
731
+ inputs=[self.components['insights_suggestion_3_btn']] + suggestion_click_inputs_base,
732
+ outputs=chat_submission_outputs,
733
+ api_name="click_suggestion_3"
734
+ )
735
+ logging.info("Analytics event handlers setup complete.")
736
+