File size: 7,002 Bytes
d1943e0
12fa967
 
 
 
 
 
 
 
 
d1943e0
 
 
 
 
 
 
 
12fa967
d1943e0
12fa967
 
 
 
 
d1943e0
 
12fa967
 
 
 
 
 
 
d1943e0
 
12fa967
 
 
 
 
 
d1943e0
 
12fa967
 
 
 
 
d1943e0
12fa967
 
 
 
 
 
d1943e0
 
12fa967
 
d1943e0
 
 
12fa967
 
d1943e0
 
 
 
12fa967
d1943e0
 
 
 
 
 
12fa967
 
 
 
 
 
 
 
 
 
 
d1943e0
12fa967
d1943e0
 
 
 
 
 
 
 
 
 
 
 
 
 
12fa967
d1943e0
12fa967
 
 
 
 
 
d1943e0
12fa967
 
d1943e0
 
 
 
 
12fa967
 
 
d1943e0
12fa967
 
 
d1943e0
 
12fa967
d1943e0
 
 
 
 
 
 
 
 
 
12fa967
 
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
# ui/callbacks.py

# -*- coding: utf-8 -*-
#
# PROJECT:      CognitiveEDA v5.0 - The QuantumLeap Intelligence Platform
#
# DESCRIPTION:  The "Controller" of the application. This module contains all
#               the Gradio event handlers (callbacks) that connect the UI (view)
#               to the core analysis engine (model).

import gradio as gr
import pandas as pd
import logging
from threading import Thread

from core.analyzer import DataAnalyzer
from core.llm import GeminiNarrativeGenerator
from core.config import settings
from core.exceptions import DataProcessingError
from modules.clustering import perform_clustering
from modules.text import generate_word_cloud
from modules.timeseries import analyze_time_series
# NOTE: The UI layout file would need to be updated to pass all the individual component
# references, which is done in the create_main_layout function. This callback assumes
# a dictionary of components is passed to it.

def register_callbacks(components):
    """
    Binds all event handlers (callbacks) to the UI components.

    Args:
        components (dict): A dictionary mapping component names to their
                           Gradio component objects.
    """

    # --- Main Analysis Trigger ---
    def run_full_analysis(file_obj, progress=gr.Progress(track_tqdm=True)):
        """
        The primary orchestration function triggered by the user. It loads data,
        runs the standard analysis, and spawns a thread for the AI report.
        """
        # 1. Input Validation (File)
        if file_obj is None:
            raise gr.Error("No file uploaded. Please upload a CSV or Excel file.")

        # 2. Runtime Configuration Validation (API Key)
        # This is the critical fix. We check for the key here, at the point of use,
        # rather than letting it crash the app at startup.
        progress(0, desc="Validating configuration...")
        if not settings.GOOGLE_API_KEY:
            logging.error("Analysis attempted without GOOGLE_API_KEY set.")
            raise gr.Error(
                "CRITICAL: GOOGLE_API_KEY is not configured. "
                "The AI Strategy Report cannot be generated. Please add the key to your "
                ".env file (for local development) or as a platform secret (for deployed apps) and restart."
            )

        try:
            # 3. Data Loading & Core Analysis
            progress(0.1, desc="Loading and parsing data...")
            df = pd.read_csv(file_obj.name) if file_obj.name.endswith('.csv') else pd.read_excel(file_obj.name)
            if len(df) > settings.MAX_UI_ROWS:
                df = df.sample(n=settings.MAX_UI_ROWS, random_state=42)

            progress(0.3, desc="Instantiating analysis engine...")
            analyzer = DataAnalyzer(df)
            meta = analyzer.metadata

            # 4. Asynchronous AI Narrative Generation
            ai_report_queue = [""]  # Use a mutable list to pass string by reference
            def generate_ai_report_threaded(analyzer_instance):
                narrative_generator = GeminiNarrativeGenerator(api_key=settings.GOOGLE_API_KEY)
                ai_report_queue[0] = narrative_generator.generate_narrative(analyzer_instance)

            thread = Thread(target=generate_ai_report_threaded, args=(analyzer,))
            thread.start()

            # 5. Generate Standard Reports and Visuals (runs immediately)
            progress(0.5, desc="Generating data profiles...")
            missing_df, num_df, cat_df = analyzer.get_profiling_reports()

            progress(0.7, desc="Creating overview visualizations...")
            fig_types, fig_missing, fig_corr = analyzer.get_overview_visuals()

            # 6. Prepare and yield initial UI updates
            progress(0.9, desc="Building initial dashboard...")
            initial_updates = {
                components["state_analyzer"]: analyzer,
                components["ai_report_output"]: gr.update(value="⏳ Generating AI-powered report in the background... The main dashboard is ready now."),
                components["profile_missing_df"]: gr.update(value=missing_df),
                components["profile_numeric_df"]: gr.update(value=num_df),
                components["profile_categorical_df"]: gr.update(value=cat_df),
                components["plot_types"]: gr.update(value=fig_types),
                components["plot_missing"]: gr.update(value=fig_missing),
                components["plot_correlation"]: gr.update(value=fig_corr),
                components["dd_hist_col"]: gr.update(choices=meta['numeric_cols'], value=meta['numeric_cols'][0] if meta['numeric_cols'] else None),
                components["dd_scatter_x"]: gr.update(choices=meta['numeric_cols'], value=meta['numeric_cols'][0] if meta['numeric_cols'] else None),
                components["dd_scatter_y"]: gr.update(choices=meta['numeric_cols'], value=meta['numeric_cols'][1] if len(meta['numeric_cols']) > 1 else None),
                components["dd_scatter_color"]: gr.update(choices=meta['columns']),
                components["tab_timeseries"]: gr.update(visible=bool(meta['datetime_cols'])),
                components["tab_text"]: gr.update(visible=bool(meta['text_cols'])),
                components["tab_cluster"]: gr.update(visible=len(meta['numeric_cols']) > 1),
            }
            yield initial_updates

            # 7. Wait for AI thread and yield final update
            thread.join()
            progress(1.0, desc="AI Report complete!")
            final_updates = initial_updates.copy()
            final_updates[components["ai_report_output"]] = ai_report_queue[0]
            yield final_updates

        except DataProcessingError as e:
            logging.error(f"User-facing data processing error: {e}", exc_info=True)
            raise gr.Error(str(e))
        except Exception as e:
            logging.error(f"A critical unhandled error occurred: {e}", exc_info=True)
            raise gr.Error(f"Analysis Failed! An unexpected error occurred: {str(e)}")

    # Bind the main analysis function
    # Note: `outputs` must be a list of all components being updated.
    output_component_list = list(components.values())
    components["analyze_button"].click(
        fn=run_full_analysis,
        inputs=[components["upload_button"]],
        outputs=output_component_list
    )

    # --- Other Interactive Callbacks ---
    def update_clustering(analyzer, k):
        if not analyzer: return gr.update(), gr.update(), gr.update()
        fig_cluster, fig_elbow, summary = perform_clustering(analyzer.df, analyzer.metadata['numeric_cols'], k)
        return fig_cluster, fig_elbow, summary

    components["num_clusters"].change(
        fn=update_clustering,
        inputs=[components["state_analyzer"], components["num_clusters"]],
        outputs=[components["plot_cluster"], components["plot_elbow"], components["md_cluster_summary"]]
    )

    # (Imagine other callbacks for scatter, histogram, etc., are registered here)