File size: 44,036 Bytes
e5163d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
# handlers/analytics_handlers.py
import gradio as gr
import logging
import time
from ui.analytics_plot_generator import update_analytics_plots_figures, create_placeholder_plot
from ui.ui_generators import BOMB_ICON, EXPLORE_ICON, FORMULA_ICON, ACTIVE_ICON # Make sure these are accessible
from features.chatbot.chatbot_prompts import get_initial_insight_prompt_and_suggestions
from features.chatbot.chatbot_handler import generate_llm_response
from config import PLOT_ID_TO_FORMULA_KEY_MAP # Ensure this is correctly imported from your config
from formulas import PLOT_FORMULAS # Ensure this is correctly imported

class AnalyticsHandlers:
    """Handles all analytics tab events and interactions."""
    
    def __init__(self, analytics_components, token_state_ref, chat_histories_st_ref, 
                 current_chat_plot_id_st_ref, plot_data_for_chatbot_st_ref,
                 active_panel_action_state_ref, explored_plot_id_state_ref):
        self.components = analytics_components
        self.plot_configs = analytics_components['plot_configs']
        self.unique_ordered_sections = analytics_components['unique_ordered_sections']
        self.num_unique_sections = len(self.unique_ordered_sections)
        self.plot_ui_objects = analytics_components['plot_ui_objects'] # e.g. {'plot_id1': {'panel_component': gr.Plot, 'bomb_button': gr.Button, ...}}
        self.section_titles_map = analytics_components['section_titles_map'] # e.g. {'Section Name': gr.Markdown}

        # References to global states, these are gr.State objects themselves
        self.token_state = token_state_ref
        self.chat_histories_st = chat_histories_st_ref
        self.current_chat_plot_id_st = current_chat_plot_id_st_ref
        self.plot_data_for_chatbot_st = plot_data_for_chatbot_st_ref
        self.active_panel_action_state = active_panel_action_state_ref
        self.explored_plot_id_state = explored_plot_id_state_ref
        
        logging.info(f"AnalyticsHandlers initialized. {len(self.plot_configs)} plot configs, {self.num_unique_sections} unique sections.")
        if not self.plot_ui_objects:
            logging.warning("AnalyticsHandlers: plot_ui_objects is empty or not correctly passed.")
        if not self.section_titles_map:
            logging.warning("AnalyticsHandlers: section_titles_map is empty or not correctly passed.")


    def _get_graph_refresh_outputs_list(self):
        """Helper to construct the list of outputs for graph refresh actions."""
        outputs = [self.components['analytics_status_md']]
        
        # Plot components themselves
        for pc in self.plot_configs:
            plot_component = self.plot_ui_objects.get(pc["id"], {}).get("plot_component")
            if plot_component:
                outputs.append(plot_component)
            else:
                outputs.append(gr.update()) # Placeholder if not found
                logging.warning(f"Plot component for {pc['id']} not found in plot_ui_objects for refresh outputs.")

        # UI resets for action panel
        outputs.extend([
            self.components['global_actions_column_ui'],
            self.components['insights_chatbot_ui'], # For value reset
            self.components['insights_chat_input_ui'], # For value reset
            self.components['insights_suggestions_row_ui'],
            self.components['insights_suggestion_1_btn'],
            self.components['insights_suggestion_2_btn'],
            self.components['insights_suggestion_3_btn'],
            self.components['formula_display_markdown_ui'], # For value reset
            self.components['formula_close_hint_md']
        ])
        
        # State resets
        outputs.extend([
            self.active_panel_action_state,
            self.current_chat_plot_id_st,
            self.chat_histories_st,
            self.plot_data_for_chatbot_st 
        ])
        
        # Button and panel visibility resets for each plot
        for pc in self.plot_configs:
            plot_id = pc["id"]
            ui_obj = self.plot_ui_objects.get(plot_id, {})
            outputs.extend([
                ui_obj.get("bomb_button", gr.update()),
                ui_obj.get("formula_button", gr.update()),
                ui_obj.get("explore_button", gr.update()),
                ui_obj.get("panel_component", gr.update()) # For visibility reset
            ])
            
        outputs.append(self.explored_plot_id_state) # Reset explored state

        # Section title visibility resets
        for s_name in self.unique_ordered_sections:
            outputs.append(self.section_titles_map.get(s_name, gr.update()))
            
        expected_len = 1 + len(self.plot_configs) + 9 + 4 + (4 * len(self.plot_configs)) + 1 + self.num_unique_sections
        # 1 (status) + N_plots (plots) + 9 (action panel UI) + 4 (states) + 4*N_plots (plot buttons/panels) + 1 (explored_id_state) + N_sections (titles)
        logging.debug(f"Graph refresh outputs list length: {len(outputs)}, Expected: {expected_len}")
        return outputs

    async def refresh_analytics_graphs_ui(self, current_token_state_val, date_filter_val, 
                                          custom_start_val, custom_end_val, 
                                          # chat_histories_st is a state, its value will be accessed via self.chat_histories_st.value
                                          ):
        logging.info(f"Refreshing analytics graph UI. Filter: {date_filter_val}. Token set: {'yes' if current_token_state_val.get('token') else 'no'}")
        start_time = time.time()

        # Call the function that generates plot figures and summaries
        # Ensure update_analytics_plots_figures is adapted to return:
        # status_msg (str), figures_dict (dict: {plot_id: fig}), summaries_dict (dict: {plot_id: summary_text})
        plot_gen_results = update_analytics_plots_figures(
            current_token_state_val, 
            date_filter_val, 
            custom_start_val, 
            custom_end_val, 
            self.plot_configs # Pass plot_configs to it
        )
        # Expected: status_msg, list_of_figures, dict_of_plot_summaries
        # Original: status_msg, gen_figs (list), new_summaries (dict)

        status_msg = plot_gen_results[0]
        gen_figs_list = plot_gen_results[1] # This should be a list of figures in order of plot_configs
        new_summaries_dict = plot_gen_results[2] # This should be a dict {plot_id: summary}

        all_updates = [gr.update(value=status_msg)] # For analytics_status_md

        # Update plot components with new figures
        if len(gen_figs_list) == len(self.plot_configs):
            for fig in gen_figs_list:
                all_updates.append(fig) # fig itself is the update for gr.Plot
        else:
            logging.error(f"Figure list length mismatch: got {len(gen_figs_list)}, expected {len(self.plot_configs)}")
            for _ in self.plot_configs:
                all_updates.append(create_placeholder_plot("Error", "Figura mancante"))

        # Reset action panel UI elements
        all_updates.extend([
            gr.update(visible=False),  # global_actions_column_ui
            gr.update(value=[], visible=False),  # insights_chatbot_ui (value and visibility)
            gr.update(value="", visible=False),  # insights_chat_input_ui (value and visibility)
            gr.update(visible=False),  # insights_suggestions_row_ui
            gr.update(value="S1"),  # insights_suggestion_1_btn
            gr.update(value="S2"),  # insights_suggestion_2_btn
            gr.update(value="S3"),  # insights_suggestion_3_btn
            gr.update(value="Formula details here.", visible=False),  # formula_display_markdown_ui
            gr.update(visible=False)  # formula_close_hint_md
        ])
        
        # Reset states
        all_updates.extend([
            None,  # active_panel_action_state
            None,  # current_chat_plot_id_st
            {},    # chat_histories_st (reset to empty dict)
            new_summaries_dict # plot_data_for_chatbot_st (update with new summaries)
        ])
        
        # Reset buttons and panel visibility for each plot
        for _ in self.plot_configs:
            all_updates.extend([
                gr.update(value=BOMB_ICON),      # bomb_button
                gr.update(value=FORMULA_ICON),   # formula_button
                gr.update(value=EXPLORE_ICON),   # explore_button
                gr.update(visible=True)          # panel_component (plot visibility)
            ])
            
        all_updates.append(None) # explored_plot_id_state (reset)

        # Reset section title visibility
        for _ in self.unique_ordered_sections:
            all_updates.append(gr.update(visible=True))
            
        end_time = time.time()
        logging.info(f"Analytics graph refresh processing took {end_time - start_time:.2f} seconds.")
        
        expected_len = 1 + len(self.plot_configs) + 9 + 4 + (4 * len(self.plot_configs)) + 1 + self.num_unique_sections
        logging.info(f"Prepared {len(all_updates)} updates for graph refresh. Expected {expected_len}.")
        if len(all_updates) != expected_len:
            logging.error(f"Output length mismatch in refresh_analytics_graphs_ui. Got {len(all_updates)}, expected {expected_len}")
            # Pad with gr.update() if lengths don't match, to avoid Gradio errors, though this indicates a logic flaw.
            all_updates.extend([gr.update()] * (expected_len - len(all_updates)))

        return tuple(all_updates)

    def _get_action_panel_outputs_list(self):
        """Helper to construct the list of outputs for panel actions (insights, formula)."""
        outputs = [
            self.components['global_actions_column_ui'],
            self.components['insights_chatbot_ui'], # For visibility
            self.components['insights_chatbot_ui'], # For value
            self.components['insights_chat_input_ui'],
            self.components['insights_suggestions_row_ui'],
            self.components['insights_suggestion_1_btn'],
            self.components['insights_suggestion_2_btn'],
            self.components['insights_suggestion_3_btn'],
            self.components['formula_display_markdown_ui'], # For visibility
            self.components['formula_display_markdown_ui'], # For value
            self.components['formula_close_hint_md'],
        ]
        outputs.extend([
            self.active_panel_action_state,
            self.current_chat_plot_id_st,
            self.chat_histories_st,
            self.explored_plot_id_state 
        ])
        
        for pc in self.plot_configs:
            ui_obj = self.plot_ui_objects.get(pc["id"], {})
            outputs.append(ui_obj.get("panel_component", gr.update())) # Plot panel visibility
            outputs.append(ui_obj.get("bomb_button", gr.update()))
            outputs.append(ui_obj.get("formula_button", gr.update()))
            outputs.append(ui_obj.get("explore_button", gr.update()))

        for s_name in self.unique_ordered_sections:
            outputs.append(self.section_titles_map.get(s_name, gr.update())) # Section title visibility

        expected_len = 11 + 4 + (4 * len(self.plot_configs)) + self.num_unique_sections
        logging.debug(f"Action panel outputs list length: {len(outputs)}, Expected: {expected_len}")
        return outputs

    async def handle_panel_action(self, plot_id_clicked: str, action_type: str, 
                                  current_active_action_from_state: dict, # This is a direct value from gr.State
                                  current_chat_histories: dict,          # This is a direct value
                                  current_chat_plot_id: str,             # This is a direct value
                                  current_plot_data_for_chatbot: dict,   # This is a direct value
                                  current_explored_plot_id: str          # This is a direct value
                                 ):
        logging.info(f"Panel Action: '{action_type}' for plot '{plot_id_clicked}'. Active: {current_active_action_from_state}, Explored: {current_explored_plot_id}")
        
        clicked_plot_config = next((p for p in self.plot_configs if p["id"] == plot_id_clicked), None)
        if not clicked_plot_config:
            logging.error(f"Config not found for plot_id {plot_id_clicked}")
            # Construct a list of gr.update() of the correct length
            num_outputs = len(self._get_action_panel_outputs_list())
            error_updates = [gr.update()] * num_outputs
            # Try to preserve existing state values if possible by updating specific indices
            # This part is tricky without knowing the exact order and meaning of each output.
            # For simplicity, returning all gr.update() might be safer if an error occurs early.
            # Or, more robustly, identify which states need to be passed through.
            # Indices for states in action_panel_outputs_list:
            # active_panel_action_state is at index 11
            # current_chat_plot_id_st is at index 12
            # chat_histories_st is at index 13
            # explored_plot_id_state is at index 14
            error_updates[11] = current_active_action_from_state
            error_updates[12] = current_chat_plot_id
            error_updates[13] = current_chat_histories
            error_updates[14] = current_explored_plot_id
            return tuple(error_updates)

        clicked_plot_label = clicked_plot_config["label"]
        clicked_plot_section = clicked_plot_config["section"]
        
        hypothetical_new_active_state = {"plot_id": plot_id_clicked, "type": action_type}
        is_toggling_off = current_active_action_from_state == hypothetical_new_active_state
        
        action_col_visible_update = gr.update(visible=False)
        insights_chatbot_visible_update = gr.update(visible=False)
        insights_chat_input_visible_update = gr.update(visible=False)
        insights_suggestions_row_visible_update = gr.update(visible=False)
        formula_display_visible_update = gr.update(visible=False)
        formula_close_hint_visible_update = gr.update(visible=False)
        
        chatbot_content_update = gr.update()
        s1_upd, s2_upd, s3_upd = gr.update(), gr.update(), gr.update()
        formula_content_update = gr.update()
        
        new_active_action_state_to_set = None # This will be the new value for the gr.State
        new_current_chat_plot_id = current_chat_plot_id # Default to existing
        updated_chat_histories = current_chat_histories # Default to existing
        new_explored_plot_id_to_set = current_explored_plot_id # Default to existing

        generated_panel_vis_updates = [] # For individual plot panels
        generated_bomb_btn_updates = []
        generated_formula_btn_updates = []
        generated_explore_btn_updates = []
        section_title_vis_updates = [gr.update()] * self.num_unique_sections

        if is_toggling_off:
            new_active_action_state_to_set = None
            action_col_visible_update = gr.update(visible=False)
            logging.info(f"Toggling OFF panel {action_type} for {plot_id_clicked}.")

            for _ in self.plot_configs:
                generated_bomb_btn_updates.append(gr.update(value=BOMB_ICON))
                generated_formula_btn_updates.append(gr.update(value=FORMULA_ICON))
            
            if current_explored_plot_id: # If an explore view is active, restore it
                explored_cfg = next((p for p in self.plot_configs if p["id"] == current_explored_plot_id), None)
                explored_sec = explored_cfg["section"] if explored_cfg else None
                for i, sec_name in enumerate(self.unique_ordered_sections):
                    section_title_vis_updates[i] = gr.update(visible=(sec_name == explored_sec))
                for cfg in self.plot_configs:
                    is_exp = (cfg["id"] == current_explored_plot_id)
                    generated_panel_vis_updates.append(gr.update(visible=is_exp))
                    generated_explore_btn_updates.append(gr.update(value=ACTIVE_ICON if is_exp else EXPLORE_ICON))
            else: # No explore view, all plots/sections visible
                for i in range(self.num_unique_sections):
                    section_title_vis_updates[i] = gr.update(visible=True)
                for _ in self.plot_configs:
                    generated_panel_vis_updates.append(gr.update(visible=True))
                    generated_explore_btn_updates.append(gr.update(value=EXPLORE_ICON))
            
            if action_type == "insights":
                new_current_chat_plot_id = None # Clear chat context if insights panel is closed
        
        else: # Toggling ON a new action or switching actions
            new_active_action_state_to_set = hypothetical_new_active_state
            action_col_visible_update = gr.update(visible=True)
            new_explored_plot_id_to_set = None # Cancel any explore view
            logging.info(f"Toggling ON panel {action_type} for {plot_id_clicked}. Cancelling explore view if any.")

            # Show only the section of the clicked plot
            for i, sec_name in enumerate(self.unique_ordered_sections):
                section_title_vis_updates[i] = gr.update(visible=(sec_name == clicked_plot_section))
            
            # Show only the clicked plot's panel, update explore buttons to non-active
            for cfg in self.plot_configs:
                generated_panel_vis_updates.append(gr.update(visible=(cfg["id"] == plot_id_clicked)))
                generated_explore_btn_updates.append(gr.update(value=EXPLORE_ICON)) # Reset all explore to inactive

            # Update bomb and formula buttons based on the new active action
            for cfg_btn in self.plot_configs:
                is_active_insights = (new_active_action_state_to_set["plot_id"] == cfg_btn["id"] and new_active_action_state_to_set["type"] == "insights")
                is_active_formula = (new_active_action_state_to_set["plot_id"] == cfg_btn["id"] and new_active_action_state_to_set["type"] == "formula")
                generated_bomb_btn_updates.append(gr.update(value=ACTIVE_ICON if is_active_insights else BOMB_ICON))
                generated_formula_btn_updates.append(gr.update(value=ACTIVE_ICON if is_active_formula else FORMULA_ICON))

            if action_type == "insights":
                insights_chatbot_visible_update = gr.update(visible=True)
                insights_chat_input_visible_update = gr.update(visible=True)
                insights_suggestions_row_visible_update = gr.update(visible=True)
                new_current_chat_plot_id = plot_id_clicked # Set chat context
                
                history = current_chat_histories.get(plot_id_clicked, [])
                summary_for_plot = current_plot_data_for_chatbot.get(plot_id_clicked, f"Nessun sommario disponibile per '{clicked_plot_label}'.")

                if not history: # First time opening insights for this plot (or after a refresh)
                    prompt, sugg = get_initial_insight_prompt_and_suggestions(plot_id_clicked, clicked_plot_label, summary_for_plot)
                    # Gradio's chatbot expects a list of lists/tuples: [[user_msg, None], [None, assistant_msg]]
                    # Our generate_llm_response and history uses: [{"role": "user", "content": prompt}, {"role": "assistant", "content": resp}]
                    # We need to adapt. For now, let's assume generate_llm_response takes our format and returns a string.
                    # The history for Gradio Chatbot component needs to be [[user_msg, assistant_msg], ...]
                    # Let's build history for LLM first
                    llm_history_for_generation = [{"role": "user", "content": prompt}]
                    
                    # Display "Thinking..." or similar
                    chatbot_content_update = gr.update(value=[[prompt, "Sto pensando..."]])
                    yield tuple(self._assemble_panel_action_updates(action_col_visible_update, insights_chatbot_visible_update, chatbot_content_update, 
                                                              insights_chat_input_visible_update, insights_suggestions_row_visible_update, 
                                                              s1_upd, s2_upd, s3_upd, formula_display_visible_update, formula_content_update, 
                                                              formula_close_hint_visible_update, new_active_action_state_to_set, 
                                                              new_current_chat_plot_id, updated_chat_histories, new_explored_plot_id_to_set, 
                                                              generated_panel_vis_updates, generated_bomb_btn_updates, 
                                                              generated_formula_btn_updates, generated_explore_btn_updates, section_title_vis_updates))


                    resp_text = await generate_llm_response(prompt, plot_id_clicked, clicked_plot_label, llm_history_for_generation, summary_for_plot)
                    
                    # Gradio chatbot history format
                    new_gr_history_for_plot = [[prompt, resp_text]]
                    # Internal history format for re-sending to LLM
                    new_internal_history_for_plot = [
                        {"role": "user", "content": prompt},
                        {"role": "assistant", "content": resp_text}
                    ]
                    updated_chat_histories = {**current_chat_histories, plot_id_clicked: new_internal_history_for_plot}
                    chatbot_content_update = gr.update(value=new_gr_history_for_plot)
                else: # History exists, just display it
                    _, sugg = get_initial_insight_prompt_and_suggestions(plot_id_clicked, clicked_plot_label, summary_for_plot) # Get fresh suggestions
                    # Convert internal history to Gradio format for display
                    gr_history_to_display = []
                    # Assuming history is [{"role":"user", "content":"..."}, {"role":"assistant", "content":"..."}]
                    # We need to pair them up. If an odd number, the last user message might not have a pair yet.
                    temp_hist = history[:] # Make a copy
                    while temp_hist:
                        user_turn = temp_hist.pop(0)
                        assistant_turn = None
                        if temp_hist and temp_hist[0]["role"] == "assistant":
                            assistant_turn = temp_hist.pop(0)
                        gr_history_to_display.append([user_turn["content"], assistant_turn["content"] if assistant_turn else None])

                    chatbot_content_update = gr.update(value=gr_history_to_display)

                s1_upd = gr.update(value=sugg[0] if sugg and len(sugg) > 0 else "N/A")
                s2_upd = gr.update(value=sugg[1] if sugg and len(sugg) > 1 else "N/A")
                s3_upd = gr.update(value=sugg[2] if sugg and len(sugg) > 2 else "N/A")

            elif action_type == "formula":
                formula_display_visible_update = gr.update(visible=True)
                formula_close_hint_visible_update = gr.update(visible=True)
                formula_key = PLOT_ID_TO_FORMULA_KEY_MAP.get(plot_id_clicked)
                formula_text = f"**Formula/Methodology for: {clicked_plot_label}** (ID: `{plot_id_clicked}`)\n\n"
                if formula_key and formula_key in PLOT_FORMULAS:
                    formula_data = PLOT_FORMULAS[formula_key]
                    formula_text += f"### {formula_data['title']}\n\n{formula_data['description']}\n\n"
                    if 'calculation_steps' in formula_data and formula_data['calculation_steps']:
                         formula_text += "**Calculation:**\n" + "\n".join([f"- {s}" for s in formula_data['calculation_steps']])
                else:
                    formula_text += "(No detailed formula information found.)"
                formula_content_update = gr.update(value=formula_text)
                new_current_chat_plot_id = None # Clear chat context if formula panel is opened
        
        final_updates_tuple = self._assemble_panel_action_updates(
            action_col_visible_update, insights_chatbot_visible_update, chatbot_content_update, 
            insights_chat_input_visible_update, insights_suggestions_row_visible_update, 
            s1_upd, s2_upd, s3_upd, formula_display_visible_update, formula_content_update, 
            formula_close_hint_visible_update, new_active_action_state_to_set, 
            new_current_chat_plot_id, updated_chat_histories, new_explored_plot_id_to_set, 
            generated_panel_vis_updates, generated_bomb_btn_updates, 
            generated_formula_btn_updates, generated_explore_btn_updates, section_title_vis_updates
        )
        logging.debug(f"handle_panel_action returning {len(final_updates_tuple)} updates.")
        yield final_updates_tuple


    def _assemble_panel_action_updates(self, action_col_visible_update, insights_chatbot_visible_update, chatbot_content_update, 
                               insights_chat_input_visible_update, insights_suggestions_row_visible_update, 
                               s1_upd, s2_upd, s3_upd, formula_display_visible_update, formula_content_update, 
                               formula_close_hint_visible_update, new_active_action_state_to_set, 
                               new_current_chat_plot_id, updated_chat_histories, new_explored_plot_id_to_set, 
                               generated_panel_vis_updates, generated_bomb_btn_updates, 
                               generated_formula_btn_updates, generated_explore_btn_updates, section_title_vis_updates):
        """Helper to assemble the final tuple of updates for handle_panel_action."""
        final_updates_list = [
            action_col_visible_update,          # global_actions_column_ui (visibility)
            insights_chatbot_visible_update,    # insights_chatbot_ui (visibility)
            chatbot_content_update,             # insights_chatbot_ui (value)
            insights_chat_input_visible_update, # insights_chat_input_ui
            insights_suggestions_row_visible_update, # insights_suggestions_row_ui
            s1_upd,                             # insights_suggestion_1_btn
            s2_upd,                             # insights_suggestion_2_btn
            s3_upd,                             # insights_suggestion_3_btn
            formula_display_visible_update,     # formula_display_markdown_ui (visibility)
            formula_content_update,             # formula_display_markdown_ui (value)
            formula_close_hint_visible_update,  # formula_close_hint_md
            
            # States
            new_active_action_state_to_set,     # active_panel_action_state
            new_current_chat_plot_id,           # current_chat_plot_id_st
            updated_chat_histories,             # chat_histories_st
            new_explored_plot_id_to_set         # explored_plot_id_state
        ]
        final_updates_list.extend(generated_panel_vis_updates)
        final_updates_list.extend(generated_bomb_btn_updates)
        final_updates_list.extend(generated_formula_btn_updates)
        final_updates_list.extend(generated_explore_btn_updates)
        final_updates_list.extend(section_title_vis_updates)
        
        expected_len = len(self._get_action_panel_outputs_list())
        if len(final_updates_list) != expected_len:
            logging.error(f"Output length mismatch in _assemble_panel_action_updates. Got {len(final_updates_list)}, expected {expected_len}")
            # Pad if necessary, though this is a bug indicator
            final_updates_list.extend([gr.update()] * (expected_len - len(final_updates_list)))
            
        return tuple(final_updates_list)


    async def handle_chat_message_submission(self, user_message: str, current_plot_id: str, 
                                             chat_histories: dict, current_plot_data_for_chatbot: dict):
        if not current_plot_id or not user_message.strip():
            # Get current Gradio history for the plot_id to display
            internal_history_for_plot = chat_histories.get(current_plot_id, [])
            gr_history_display = self._convert_internal_to_gradio_chat_history(internal_history_for_plot)
            yield gr_history_display, gr.update(value=""), chat_histories
            return

        clicked_plot_config = next((p for p in self.plot_configs if p["id"] == current_plot_id), None)
        plot_label = clicked_plot_config["label"] if clicked_plot_config else "Selected Plot"
        summary_for_plot = current_plot_data_for_chatbot.get(current_plot_id, f"No summary for '{plot_label}'.")
        
        internal_history_for_plot = chat_histories.get(current_plot_id, []).copy() # Get a mutable copy
        internal_history_for_plot.append({"role": "user", "content": user_message})
        
        # Update Gradio chat display: User message + "Thinking..."
        gr_history_display_pending = self._convert_internal_to_gradio_chat_history(internal_history_for_plot, thinking=True)
        yield gr_history_display_pending, gr.update(value=""), chat_histories # Show user message immediately

        # Generate LLM response
        llm_response_text = await generate_llm_response(user_message, current_plot_id, plot_label, internal_history_for_plot, summary_for_plot)
        
        internal_history_for_plot.append({"role": "assistant", "content": llm_response_text})
        
        updated_chat_histories = {**chat_histories, current_plot_id: internal_history_for_plot}
        
        # Final Gradio chat display with LLM response
        final_gr_history_display = self._convert_internal_to_gradio_chat_history(internal_history_for_plot)
        yield final_gr_history_display, "", updated_chat_histories

    def _convert_internal_to_gradio_chat_history(self, internal_history, thinking=False):
        """Converts internal chat history format to Gradio's [[user, assistant], ...] format."""
        gradio_history = []
        temp_hist = internal_history[:] # Make a copy
        while temp_hist:
            user_msg_obj = temp_hist.pop(0)
            user_msg = user_msg_obj['content']
            assistant_msg = None
            if temp_hist and temp_hist[0]['role'] == 'assistant':
                assistant_msg_obj = temp_hist.pop(0)
                assistant_msg = assistant_msg_obj['content']
            gradio_history.append([user_msg, assistant_msg])
        
        if thinking and gradio_history and gradio_history[-1][1] is None: # If last message was user and we are in 'thinking' mode
             gradio_history[-1][1] = "Sto pensando..." # Replace None with "Thinking..."
        elif thinking and not gradio_history: # Should not happen if user_message was added
            pass


        return gradio_history

    async def handle_suggested_question_click(self, suggestion_text: str, current_plot_id: str, 
                                              chat_histories: dict, current_plot_data_for_chatbot: dict):
        if not current_plot_id or not suggestion_text.strip() or suggestion_text == "N/A":
            internal_history_for_plot = chat_histories.get(current_plot_id, [])
            gr_history_display = self._convert_internal_to_gradio_chat_history(internal_history_for_plot)
            yield gr_history_display, gr.update(value=""), chat_histories
            return
        
        # Use the existing chat submission logic
        async for update_chunk in self.handle_chat_message_submission(suggestion_text, current_plot_id, chat_histories, current_plot_data_for_chatbot):
            yield update_chunk
            
    def _get_explore_outputs_list(self):
        """Helper to construct the list of outputs for explore actions."""
        outputs = [
            self.explored_plot_id_state,
            self.components['global_actions_column_ui'], # For visibility
            self.active_panel_action_state, # To potentially clear it
            self.components['formula_close_hint_md'] # For visibility
        ]
        
        for pc in self.plot_configs: # Plot panel visibility
            outputs.append(self.plot_ui_objects.get(pc["id"], {}).get("panel_component", gr.update()))
        for pc in self.plot_configs: # Explore button state
            outputs.append(self.plot_ui_objects.get(pc["id"], {}).get("explore_button", gr.update()))
        for pc in self.plot_configs: # Bomb button state (may need reset)
            outputs.append(self.plot_ui_objects.get(pc["id"], {}).get("bomb_button", gr.update()))
        for pc in self.plot_configs: # Formula button state (may need reset)
            outputs.append(self.plot_ui_objects.get(pc["id"], {}).get("formula_button", gr.update()))
            
        for s_name in self.unique_ordered_sections: # Section title visibility
            outputs.append(self.section_titles_map.get(s_name, gr.update()))

        expected_len = 4 + (4 * len(self.plot_configs)) + self.num_unique_sections
        logging.debug(f"Explore outputs list length: {len(outputs)}, Expected: {expected_len}")
        return outputs

    def handle_explore_click(self, plot_id_clicked: str, current_explored_plot_id_from_state: str, 
                             current_active_panel_action_state: dict):
        logging.info(f"Explore Click: Plot '{plot_id_clicked}'. Current Explored: {current_explored_plot_id_from_state}. Active Panel: {current_active_panel_action_state}")

        if not self.plot_ui_objects or not self.section_titles_map:
            logging.error("plot_ui_objects or section_titles_map not populated for handle_explore_click.")
            num_outputs = len(self._get_explore_outputs_list())
            error_updates = [gr.update()] * num_outputs
            error_updates[0] = current_explored_plot_id_from_state # Preserve explored_id_state
            error_updates[2] = current_active_panel_action_state # Preserve active_panel_state
            return tuple(error_updates)

        new_explored_id_to_set = None
        is_toggling_off_explore = (plot_id_clicked == current_explored_plot_id_from_state)
        
        action_col_upd = gr.update() # Default no change
        new_active_panel_state_upd = current_active_panel_action_state # Default no change
        formula_hint_upd = gr.update(visible=False) # Default hide

        panel_vis_updates = []
        explore_btns_updates = []
        bomb_btns_updates = [gr.update()] * len(self.plot_configs) # Default no change
        formula_btns_updates = [gr.update()] * len(self.plot_configs) # Default no change
        section_title_vis_updates = [gr.update()] * self.num_unique_sections

        clicked_cfg = next((p for p in self.plot_configs if p["id"] == plot_id_clicked), None)
        section_of_clicked_plot = clicked_cfg["section"] if clicked_cfg else None

        if is_toggling_off_explore:
            new_explored_id_to_set = None # Clear explore state
            logging.info(f"Stopping explore for {plot_id_clicked}. All plots/sections to be visible.")
            for i in range(self.num_unique_sections):
                section_title_vis_updates[i] = gr.update(visible=True)
            for _ in self.plot_configs:
                panel_vis_updates.append(gr.update(visible=True))
                explore_btns_updates.append(gr.update(value=EXPLORE_ICON))
            # Bomb and formula buttons remain as they were unless an action panel was closed (handled below if current_active_panel_action_state was set)
        
        else: # Starting explore or switching explored plot
            new_explored_id_to_set = plot_id_clicked
            logging.info(f"Exploring {plot_id_clicked}. Hiding other plots/sections.")
            for i, sec_name in enumerate(self.unique_ordered_sections):
                section_title_vis_updates[i] = gr.update(visible=(sec_name == section_of_clicked_plot))
            for cfg in self.plot_configs:
                is_target = (cfg["id"] == new_explored_id_to_set)
                panel_vis_updates.append(gr.update(visible=is_target))
                explore_btns_updates.append(gr.update(value=ACTIVE_ICON if is_target else EXPLORE_ICON))

            if current_active_panel_action_state: # If an action panel (insights/formula) is open, close it
                logging.info("Closing active insight/formula panel due to explore click.")
                action_col_upd = gr.update(visible=False)
                new_active_panel_state_upd = None # Clear active panel state
                formula_hint_upd = gr.update(visible=False) # Hide formula hint specifically
                # Reset bomb and formula buttons to their default icons
                bomb_btns_updates = [gr.update(value=BOMB_ICON) for _ in self.plot_configs]
                formula_btns_updates = [gr.update(value=FORMULA_ICON) for _ in self.plot_configs]
        
        final_explore_updates_list = [
            new_explored_id_to_set, 
            action_col_upd, 
            new_active_panel_state_upd, 
            formula_hint_upd
        ]
        final_explore_updates_list.extend(panel_vis_updates)
        final_explore_updates_list.extend(explore_btns_updates)
        final_explore_updates_list.extend(bomb_btns_updates)
        final_explore_updates_list.extend(formula_btns_updates)
        final_explore_updates_list.extend(section_title_vis_updates)
        
        expected_len = len(self._get_explore_outputs_list())
        if len(final_explore_updates_list) != expected_len:
            logging.error(f"Output length mismatch in handle_explore_click. Got {len(final_explore_updates_list)}, expected {expected_len}")
            final_explore_updates_list.extend([gr.update()] * (expected_len - len(final_explore_updates_list)))

        return tuple(final_explore_updates_list)

    def setup_event_handlers(self):
        """Set up all event handlers for the analytics tab components."""
        logging.info("Setting up analytics event handlers.")

        # Apply filter button
        apply_filter_inputs = [
            self.token_state, 
            self.components['date_filter_selector'], 
            self.components['custom_start_date_picker'], 
            self.components['custom_end_date_picker'],
            # self.chat_histories_st # Not directly an input to refresh_analytics_graphs_ui, it's accessed via self
        ]
        self.components['apply_filter_btn'].click(
            fn=self.refresh_analytics_graphs_ui,
            inputs=apply_filter_inputs,
            outputs=self._get_graph_refresh_outputs_list(), # Method returns the list of components
            show_progress="full",
            api_name="refresh_analytics_graphs"
        )
        
        # Plot action handlers (insights, formula, explore)
        action_click_inputs = [ # These are the gr.State objects themselves
            self.active_panel_action_state, 
            self.chat_histories_st, 
            self.current_chat_plot_id_st, 
            self.plot_data_for_chatbot_st, 
            self.explored_plot_id_state
        ]
        
        explore_click_inputs = [ # gr.State objects
            self.explored_plot_id_state, 
            self.active_panel_action_state
        ]
        
        action_panel_outputs_list = self._get_action_panel_outputs_list()
        explore_outputs_list = self._get_explore_outputs_list()

        for config_item in self.plot_configs:
            plot_id = config_item["id"]
            if plot_id in self.plot_ui_objects:
                ui_obj = self.plot_ui_objects[plot_id]
                
                # Curry plot_id and action_type for the handler
                # The handler function itself (self.handle_panel_action) will receive the values from the gr.State inputs directly.
                
                if ui_obj.get("bomb_button"):
                    ui_obj["bomb_button"].click(
                        fn=lambda current_active, current_chats, current_chat_pid, current_plot_data, current_explored, p_id=plot_id: \
                             self.handle_panel_action(p_id, "insights", current_active, current_chats, current_chat_pid, current_plot_data, current_explored),
                        inputs=action_click_inputs, # Pass the list of gr.State objects
                        outputs=action_panel_outputs_list,
                        api_name=f"action_insights_{plot_id}"
                    )
                if ui_obj.get("formula_button"):
                    ui_obj["formula_button"].click(
                        fn=lambda current_active, current_chats, current_chat_pid, current_plot_data, current_explored, p_id=plot_id: \
                             self.handle_panel_action(p_id, "formula", current_active, current_chats, current_chat_pid, current_plot_data, current_explored),
                        inputs=action_click_inputs,
                        outputs=action_panel_outputs_list,
                        api_name=f"action_formula_{plot_id}"
                    )
                if ui_obj.get("explore_button"):
                    ui_obj["explore_button"].click(
                        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),
                        inputs=explore_click_inputs, # Pass the list of gr.State objects
                        outputs=explore_outputs_list,
                        api_name=f"action_explore_{plot_id}"
                    )
            else:
                logging.warning(f"UI object for plot_id '{plot_id}' not found for setting up click handlers.")

        # Chat submission handlers
        chat_submission_outputs = [
            self.components['insights_chatbot_ui'], 
            self.components['insights_chat_input_ui'], 
            self.chat_histories_st # This state will be updated
        ]
        chat_submission_inputs = [ # gr.Textbox, gr.State, gr.State, gr.State
            self.components['insights_chat_input_ui'], 
            self.current_chat_plot_id_st, 
            self.chat_histories_st, 
            self.plot_data_for_chatbot_st
        ]
        
        self.components['insights_chat_input_ui'].submit(
            fn=self.handle_chat_message_submission, 
            inputs=chat_submission_inputs, 
            outputs=chat_submission_outputs, 
            api_name="submit_chat_message"
        )
        
        suggestion_click_inputs_base = [ # gr.State, gr.State, gr.State
            self.current_chat_plot_id_st, 
            self.chat_histories_st, 
            self.plot_data_for_chatbot_st
        ]
        
        # For suggestion buttons, the first input is the button itself (to get its value)
        self.components['insights_suggestion_1_btn'].click(
            fn=self.handle_suggested_question_click, 
            inputs=[self.components['insights_suggestion_1_btn']] + suggestion_click_inputs_base, 
            outputs=chat_submission_outputs, 
            api_name="click_suggestion_1"
        )
        self.components['insights_suggestion_2_btn'].click(
            fn=self.handle_suggested_question_click, 
            inputs=[self.components['insights_suggestion_2_btn']] + suggestion_click_inputs_base, 
            outputs=chat_submission_outputs, 
            api_name="click_suggestion_2"
        )
        self.components['insights_suggestion_3_btn'].click(
            fn=self.handle_suggested_question_click, 
            inputs=[self.components['insights_suggestion_3_btn']] + suggestion_click_inputs_base, 
            outputs=chat_submission_outputs, 
            api_name="click_suggestion_3"
        )
        logging.info("Analytics event handlers setup complete.")