mgbam commited on
Commit
c8e629f
Β·
verified Β·
1 Parent(s): 06fc0da

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +64 -54
app.py CHANGED
@@ -11,10 +11,9 @@ from typing import List, Dict, Any, Tuple, Optional
11
 
12
  # --- Configuration & Constants ---
13
  warnings.filterwarnings('ignore')
14
-
15
  CSS = """
16
- /* --- Phoenix UI Professional Dark CSS --- */
17
- #app-title { text-align: center; font-weight: 800; font-size: 2.5rem; color: #f9fafb; }
18
  .stat-card { border-radius: 12px !important; padding: 20px !important; background: #1f2937 !important; border: 1px solid #374151 !important; text-align: center; transition: all 0.3s ease; }
19
  .stat-card:hover { transform: translateY(-5px); box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05); }
20
  .stat-card-title { font-size: 16px; font-weight: 500; color: #9ca3af !important; margin-bottom: 8px; }
@@ -25,7 +24,6 @@ CSS = """
25
  .sidebar .gr-button.selected { background-color: #4f46e5 !important; font-weight: 600 !important; color: white !important; }
26
  .explanation-block { background-color: #1e3a8a !important; border-left: 4px solid #3b82f6 !important; padding: 12px; color: #e5e7eb !important; border-radius: 4px; }
27
  """
28
- MAX_DASHBOARD_PLOTS = 10
29
 
30
  class DataExplorerApp:
31
  """A professional-grade, AI-powered data exploration application."""
@@ -37,28 +35,37 @@ class DataExplorerApp:
37
  with gr.Blocks(theme=gr.themes.Glass(primary_hue="indigo", secondary_hue="blue"), css=CSS, title="AI Data Explorer Pro") as demo:
38
  state_var = gr.State({})
39
 
40
- # --- Component Definition ---
41
- cockpit_btn, deep_dive_btn, copilot_btn = [gr.Button(elem_id=id) for id in ["cockpit", "deep_dive", "co-pilot"]]
42
- file_input, status_output = gr.File(label="πŸ“ Upload CSV File", file_types=[".csv"]), gr.Markdown("Status: Awaiting data...")
 
 
 
 
 
 
 
43
  api_key_input = gr.Textbox(label="πŸ”‘ Gemini API Key", type="password", placeholder="Enter key to enable AI...")
44
  suggestion_btn = gr.Button("Get Smart Suggestions", variant="secondary", interactive=False)
45
 
46
- rows_stat, cols_stat = gr.Textbox("0", interactive=False, elem_classes="stat-card-value", show_label=False), gr.Textbox("0", interactive=False, elem_classes="stat-card-value", show_label=False)
47
- quality_stat, time_cols_stat = gr.Textbox("0%", interactive=False, elem_classes="stat-card-value", show_label=False), gr.Textbox("0", interactive=False, elem_classes="stat-card-value", show_label=False)
 
48
  suggestion_buttons = [gr.Button(visible=False) for _ in range(5)]
49
 
 
50
  plot_type_dd = gr.Dropdown(['histogram', 'bar', 'scatter', 'box'], label="Plot Type", value='histogram')
51
  x_col_dd = gr.Dropdown([], label="X-Axis / Column", interactive=False)
52
  y_col_dd = gr.Dropdown([], label="Y-Axis (for Scatter/Box)", visible=False, interactive=False)
53
- add_plot_btn, clear_plots_btn = gr.Button("Add to Dashboard", variant="primary", interactive=False), gr.Button("Clear Dashboard")
54
-
55
- # CORRECTED: Use a dynamic set of Plot components, not Gallery
56
  dashboard_plots = [gr.Plot(visible=False) for _ in range(MAX_DASHBOARD_PLOTS)]
57
 
58
- chatbot, chat_input, chat_submit_btn = gr.Chatbot(height=500, label="Conversation", show_copy_button=True), gr.Textbox(label="Your Question", placeholder="e.g., 'What is the relationship between age and salary?'", scale=4), gr.Button("Ask AI", variant="primary")
 
59
  copilot_explanation, copilot_code = gr.Markdown(visible=False, elem_classes="explanation-block"), gr.Code(language="python", visible=False, label="Executed Code")
60
  copilot_plot, copilot_table = gr.Plot(visible=False, label="Generated Visualization"), gr.Dataframe(visible=False, label="Generated Table", wrap=True)
61
-
 
62
  # --- Layout Arrangement ---
63
  with gr.Row():
64
  with gr.Column(scale=1, elem_classes="sidebar"):
@@ -66,7 +73,7 @@ class DataExplorerApp:
66
  file_input; status_output; gr.Markdown("---"); api_key_input; suggestion_btn
67
  with gr.Column(scale=4):
68
  welcome_page, cockpit_page, deep_dive_page, copilot_page = [gr.Column(visible=i==0) for i in range(4)]
69
- with welcome_page: gr.Markdown("# Welcome to the AI Data Explorer Pro\n> Please **upload a CSV file** and **enter your Gemini API key** to begin.")
70
  with cockpit_page:
71
  gr.Markdown("## πŸ“Š Data Cockpit: At-a-Glance Overview")
72
  with gr.Row():
@@ -79,7 +86,7 @@ class DataExplorerApp:
79
  gr.Markdown("## πŸ” Deep Dive: Manual Dashboard Builder"); gr.Markdown("Construct visualizations to investigate specific relationships.")
80
  with gr.Row(): plot_type_dd; x_col_dd; y_col_dd
81
  with gr.Row(): add_plot_btn; clear_plots_btn
82
- with gr.Column(): [plot for plot in dashboard_plots] # Place the plot holders
83
  with copilot_page:
84
  gr.Markdown("## πŸ€– Chief Data Scientist: Your AI Partner"); chatbot
85
  with gr.Accordion("AI's Detailed Response", open=True): copilot_explanation; copilot_code; copilot_plot; copilot_table
@@ -92,16 +99,18 @@ class DataExplorerApp:
92
  lambda i=i: [gr.update(elem_classes="selected" if j==i else "") for j in range(len(nav_buttons))], outputs=nav_buttons)
93
 
94
  file_input.upload(self.load_and_process_file, inputs=[file_input], outputs=[
95
- state_var, status_output, welcome_page, cockpit_page, rows_stat, cols_stat, quality_stat, time_cols_stat,
96
  x_col_dd, y_col_dd, add_plot_btn]).then(lambda: self._switch_page("cockpit", pages), outputs=pages).then(
97
  lambda: [gr.update(elem_classes="selected"), gr.update(elem_classes=""), gr.update(elem_classes="")], outputs=nav_buttons)
98
 
99
  api_key_input.change(lambda x: gr.update(interactive=bool(x)), inputs=[api_key_input], outputs=[suggestion_btn])
 
 
100
  plot_type_dd.change(self._update_plot_controls, inputs=[plot_type_dd], outputs=[y_col_dd])
101
- add_plot_btn.click(self.add_plot_to_dashboard, inputs=[state_var, x_col_dd, y_col_dd, plot_type_dd], outputs=[state_var, *dashboard_plots])
102
- clear_plots_btn.click(self.clear_dashboard, inputs=[state_var], outputs=[state_var, *dashboard_plots])
103
- suggestion_btn.click(self.get_ai_suggestions, inputs=[state_var, api_key_input], outputs=suggestion_buttons)
104
 
 
105
  for btn in suggestion_buttons:
106
  btn.click(self.handle_suggestion_click, inputs=[btn], outputs=[*pages, chat_input]).then(
107
  lambda: self._switch_page("co-pilot", pages), outputs=pages).then(
@@ -113,8 +122,9 @@ class DataExplorerApp:
113
 
114
  def launch(self): self.demo.launch(debug=True)
115
 
 
116
  def _switch_page(self, page_id: str, all_pages: List) -> List[gr.update]:
117
- visibility = {"welcome":0, "cockpit":1, "deep_dive":2, "co-pilot":3}
118
  return [gr.update(visible=i == visibility.get(page_id, 0)) for i in range(len(all_pages))]
119
 
120
  def _update_plot_controls(self, plot_type: str) -> gr.update:
@@ -126,11 +136,12 @@ class DataExplorerApp:
126
  metadata = self._extract_dataset_metadata(df)
127
  state = {'df': df, 'metadata': metadata, 'dashboard_plots': []}
128
  rows, cols, quality = metadata['shape'][0], metadata['shape'][1], metadata['data_quality']
129
- return (state, f"βœ… **{os.path.basename(file_obj.name)}** loaded.", gr.update(visible=False), gr.update(visible=True),
130
- f"{rows:,}", f"{cols}", f"{quality}%", f"{len(metadata['datetime_cols'])}",
131
- gr.update(choices=metadata['columns'], interactive=True), gr.update(choices=metadata['columns'], interactive=True), gr.update(interactive=True))
 
132
  except Exception as e:
133
- gr.Error(f"File Load Error: {e}"); return {}, f"❌ Error: {e}", gr.update(visible=True), gr.update(visible=False), "0", "0", "0%", "0", gr.update(choices=[], interactive=False), gr.update(choices=[], interactive=False), gr.update(interactive=False)
134
 
135
  def _extract_dataset_metadata(self, df: pd.DataFrame) -> Dict[str, Any]:
136
  rows, cols = df.shape
@@ -141,9 +152,10 @@ class DataExplorerApp:
141
 
142
  def add_plot_to_dashboard(self, state: Dict, x_col: str, y_col: Optional[str], plot_type: str) -> List[Any]:
143
  if len(state.get('dashboard_plots', [])) >= MAX_DASHBOARD_PLOTS:
144
- gr.Warning(f"Dashboard is full. Max {MAX_DASHBOARD_PLOTS} plots allowed."); return [state, *self._get_plot_updates(state)]
145
- if not x_col: gr.Warning("Please select at least an X-axis column."); return [state, *self._get_plot_updates(state)]
146
- df, title = state['df'], f"{plot_type.capitalize()}: {y_col} by {x_col}" if y_col and plot_type in ['box', 'scatter'] else f"Distribution of {x_col}"
 
147
  try:
148
  if plot_type == 'histogram': fig = px.histogram(df, x=x_col, title=title)
149
  elif plot_type == 'box': fig = px.box(df, x=x_col, y=y_col, title=title)
@@ -153,62 +165,48 @@ class DataExplorerApp:
153
  fig = px.bar(counts, x=counts.index, y=counts.values, title=f"Top 20 Categories for {x_col}", labels={'index': x_col, 'y': 'Count'})
154
  if fig:
155
  fig.update_layout(template="plotly_dark"); state['dashboard_plots'].append(fig); gr.Info(f"Added '{title}' to dashboard.")
156
- return [state, *self._get_plot_updates(state)]
157
- except Exception as e: gr.Error(f"Plotting Error: {e}"); return [state, *self._get_plot_updates(state)]
158
 
159
  def _get_plot_updates(self, state: Dict) -> List[gr.update]:
160
  plots = state.get('dashboard_plots', [])
161
- updates = []
162
- for i in range(MAX_DASHBOARD_PLOTS):
163
- if i < len(plots): updates.append(gr.update(value=plots[i], visible=True))
164
- else: updates.append(gr.update(value=None, visible=False))
165
- return updates
166
 
167
  def clear_dashboard(self, state: Dict) -> List[Any]:
168
- state['dashboard_plots'] = []; gr.Info("Dashboard cleared."); return [state, *self._get_plot_updates(state)]
169
 
170
  def get_ai_suggestions(self, state: Dict, api_key: str) -> List[gr.update]:
171
  if not api_key: gr.Warning("API Key is required."); return [gr.update(visible=False)]*5
172
  if not state: gr.Warning("Please load data first."); return [gr.update(visible=False)]*5
173
- # CORRECTED: metadata assignment
174
- metadata = state['metadata']
175
- prompt = f"From columns {metadata['columns']}, generate 4 impactful analytical questions. Return ONLY a JSON list of strings."
176
  try:
177
  genai.configure(api_key=api_key); suggestions = json.loads(genai.GenerativeModel('gemini-1.5-flash').generate_content(prompt).text)
178
  return [gr.Button(s, visible=True) for s in suggestions] + [gr.Button(visible=False)] * (5 - len(suggestions))
179
  except Exception as e: gr.Error(f"AI Suggestion Error: {e}"); return [gr.update(visible=False)]*5
180
 
181
  def handle_suggestion_click(self, question: str) -> Tuple[gr.update, ...]:
182
- return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), question
183
 
184
  def respond_to_chat(self, state: Dict, api_key: str, user_message: str, history: List) -> Any:
185
- if not user_message.strip(): gr.Warning("Message is empty."); return history, *[gr.update()]*4
186
  if not api_key or not state:
187
  msg = "I need a Gemini API key and a dataset to work."; history.append((user_message, msg)); return history, *[gr.update(visible=False)]*4
188
 
189
  history.append((user_message, "Thinking... πŸ€”")); yield history, *[gr.update(visible=False)]*4
190
 
191
  metadata, prompt = state['metadata'], f"""You are 'Chief Data Scientist', an expert AI analyst...
192
- **Instructions:**
193
- 1. **Analyze:** Understand user intent. Infer best plot type.
194
- 2. **Plan:** Briefly explain your plan.
195
- 3. **Code:** Write Python code. Use `fig` for plots (`template='plotly_dark'`) and `result_df` for tables.
196
- 4. **Insight:** Provide a one-sentence business insight.
197
- 5. **Respond ONLY with a single JSON object with keys: "plan", "code", "insight".**
198
- **Metadata:** {metadata['dtypes_head']}
199
- **User Question:** "{user_message}"
200
  """
201
  try:
202
- genai.configure(api_key=api_key)
203
- response_json = json.loads(genai.GenerativeModel('gemini-1.5-flash').generate_content(prompt).text.strip().replace("```json", "").replace("```", ""))
204
  plan, code, insight = response_json.get("plan"), response_json.get("code"), response_json.get("insight")
205
  stdout, fig, df_result, error = self._safe_exec(code, {'df': state['df'], 'px': px, 'pd': pd})
206
-
207
  history[-1] = (user_message, f"**Plan:** {plan}")
208
  explanation = f"**Insight:** {insight}"
209
  if stdout: explanation += f"\n\n**Console Output:**\n```\n{stdout}\n```"
210
  if error: gr.Error(f"AI Code Execution Failed: {error}")
211
-
212
  yield (history, gr.update(visible=bool(explanation), value=explanation), gr.update(visible=bool(code), value=code),
213
  gr.update(visible=bool(fig), value=fig), gr.update(visible=bool(df_result is not None), value=df_result))
214
  except Exception as e:
@@ -216,12 +214,24 @@ class DataExplorerApp:
216
  yield history, *[gr.update(visible=False)]*4
217
 
218
  def _safe_exec(self, code_string: str, local_vars: Dict) -> Tuple[Any, ...]:
219
- output_buffer = io.StringIO()
220
  try:
 
221
  with redirect_stdout(output_buffer): exec(code_string, globals(), local_vars)
222
  return output_buffer.getvalue(), local_vars.get('fig'), local_vars.get('result_df'), None
223
  except Exception as e: return None, None, None, str(e)
224
 
225
  if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
226
  app = DataExplorerApp()
227
  app.launch()
 
11
 
12
  # --- Configuration & Constants ---
13
  warnings.filterwarnings('ignore')
14
+ MAX_DASHBOARD_PLOTS = 10
15
  CSS = """
16
+ #app-title { text-align: center; font-weight: 800; font-size: 2.5rem; color: #f9fafb; padding-top: 10px; }
 
17
  .stat-card { border-radius: 12px !important; padding: 20px !important; background: #1f2937 !important; border: 1px solid #374151 !important; text-align: center; transition: all 0.3s ease; }
18
  .stat-card:hover { transform: translateY(-5px); box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05); }
19
  .stat-card-title { font-size: 16px; font-weight: 500; color: #9ca3af !important; margin-bottom: 8px; }
 
24
  .sidebar .gr-button.selected { background-color: #4f46e5 !important; font-weight: 600 !important; color: white !important; }
25
  .explanation-block { background-color: #1e3a8a !important; border-left: 4px solid #3b82f6 !important; padding: 12px; color: #e5e7eb !important; border-radius: 4px; }
26
  """
 
27
 
28
  class DataExplorerApp:
29
  """A professional-grade, AI-powered data exploration application."""
 
35
  with gr.Blocks(theme=gr.themes.Glass(primary_hue="indigo", secondary_hue="blue"), css=CSS, title="AI Data Explorer Pro") as demo:
36
  state_var = gr.State({})
37
 
38
+ # --- Component Definition (Define-Then-Place Pattern) ---
39
+ # Sidebar
40
+ ### CRITICAL FIX HERE ###
41
+ # Buttons are defined individually with their value (text) to fix the "Run" bug.
42
+ cockpit_btn = gr.Button("πŸ“Š Data Cockpit", elem_classes="selected", elem_id="cockpit")
43
+ deep_dive_btn = gr.Button("πŸ” Deep Dive Builder", elem_id="deep_dive")
44
+ copilot_btn = gr.Button("πŸ€– Chief Data Scientist", elem_id="co-pilot")
45
+
46
+ file_input = gr.File(label="πŸ“ Upload CSV File", file_types=[".csv"])
47
+ status_output = gr.Markdown("Status: Awaiting data...")
48
  api_key_input = gr.Textbox(label="πŸ”‘ Gemini API Key", type="password", placeholder="Enter key to enable AI...")
49
  suggestion_btn = gr.Button("Get Smart Suggestions", variant="secondary", interactive=False)
50
 
51
+ # Cockpit
52
+ rows_stat, cols_stat = gr.Textbox("0", interactive=False, show_label=False, elem_classes="stat-card-value"), gr.Textbox("0", interactive=False, show_label=False, elem_classes="stat-card-value")
53
+ quality_stat, time_cols_stat = gr.Textbox("0%", interactive=False, show_label=False, elem_classes="stat-card-value"), gr.Textbox("0", interactive=False, show_label=False, elem_classes="stat-card-value")
54
  suggestion_buttons = [gr.Button(visible=False) for _ in range(5)]
55
 
56
+ # Deep Dive
57
  plot_type_dd = gr.Dropdown(['histogram', 'bar', 'scatter', 'box'], label="Plot Type", value='histogram')
58
  x_col_dd = gr.Dropdown([], label="X-Axis / Column", interactive=False)
59
  y_col_dd = gr.Dropdown([], label="Y-Axis (for Scatter/Box)", visible=False, interactive=False)
60
+ add_plot_btn, clear_plots_btn = gr.Button("Add to Dashboard", variant="primary", interactive=False), gr.Button("Clear Dashboard", interactive=False)
 
 
61
  dashboard_plots = [gr.Plot(visible=False) for _ in range(MAX_DASHBOARD_PLOTS)]
62
 
63
+ # Co-pilot
64
+ chatbot = gr.Chatbot(height=500, label="Conversation", show_copy_button=True, avatar_images=(None, "bot.png"))
65
  copilot_explanation, copilot_code = gr.Markdown(visible=False, elem_classes="explanation-block"), gr.Code(language="python", visible=False, label="Executed Code")
66
  copilot_plot, copilot_table = gr.Plot(visible=False, label="Generated Visualization"), gr.Dataframe(visible=False, label="Generated Table", wrap=True)
67
+ chat_input, chat_submit_btn = gr.Textbox(label="Your Question", placeholder="e.g., 'What is the relationship between age and salary?'", scale=4), gr.Button("Ask AI", variant="primary", interactive=False)
68
+
69
  # --- Layout Arrangement ---
70
  with gr.Row():
71
  with gr.Column(scale=1, elem_classes="sidebar"):
 
73
  file_input; status_output; gr.Markdown("---"); api_key_input; suggestion_btn
74
  with gr.Column(scale=4):
75
  welcome_page, cockpit_page, deep_dive_page, copilot_page = [gr.Column(visible=i==0) for i in range(4)]
76
+ with welcome_page: gr.Markdown("# Welcome to the AI Data Explorer Pro\n> Please **upload a CSV file** and **enter your Gemini API key** to begin your analysis.")
77
  with cockpit_page:
78
  gr.Markdown("## πŸ“Š Data Cockpit: At-a-Glance Overview")
79
  with gr.Row():
 
86
  gr.Markdown("## πŸ” Deep Dive: Manual Dashboard Builder"); gr.Markdown("Construct visualizations to investigate specific relationships.")
87
  with gr.Row(): plot_type_dd; x_col_dd; y_col_dd
88
  with gr.Row(): add_plot_btn; clear_plots_btn
89
+ with gr.Column(): [plot for plot in dashboard_plots]
90
  with copilot_page:
91
  gr.Markdown("## πŸ€– Chief Data Scientist: Your AI Partner"); chatbot
92
  with gr.Accordion("AI's Detailed Response", open=True): copilot_explanation; copilot_code; copilot_plot; copilot_table
 
99
  lambda i=i: [gr.update(elem_classes="selected" if j==i else "") for j in range(len(nav_buttons))], outputs=nav_buttons)
100
 
101
  file_input.upload(self.load_and_process_file, inputs=[file_input], outputs=[
102
+ state_var, status_output, *pages, rows_stat, cols_stat, quality_stat, time_cols_stat,
103
  x_col_dd, y_col_dd, add_plot_btn]).then(lambda: self._switch_page("cockpit", pages), outputs=pages).then(
104
  lambda: [gr.update(elem_classes="selected"), gr.update(elem_classes=""), gr.update(elem_classes="")], outputs=nav_buttons)
105
 
106
  api_key_input.change(lambda x: gr.update(interactive=bool(x)), inputs=[api_key_input], outputs=[suggestion_btn])
107
+ chat_input.change(lambda x: gr.update(interactive=bool(x.strip())), inputs=[chat_input], outputs=[chat_submit_btn])
108
+
109
  plot_type_dd.change(self._update_plot_controls, inputs=[plot_type_dd], outputs=[y_col_dd])
110
+ add_plot_btn.click(self.add_plot_to_dashboard, inputs=[state_var, x_col_dd, y_col_dd, plot_type_dd], outputs=[state_var, clear_plots_btn, *dashboard_plots])
111
+ clear_plots_btn.click(self.clear_dashboard, inputs=[state_var], outputs=[state_var, clear_plots_btn, *dashboard_plots])
 
112
 
113
+ suggestion_btn.click(self.get_ai_suggestions, inputs=[state_var, api_key_input], outputs=suggestion_buttons)
114
  for btn in suggestion_buttons:
115
  btn.click(self.handle_suggestion_click, inputs=[btn], outputs=[*pages, chat_input]).then(
116
  lambda: self._switch_page("co-pilot", pages), outputs=pages).then(
 
122
 
123
  def launch(self): self.demo.launch(debug=True)
124
 
125
+ # --- Backend Logic Methods ---
126
  def _switch_page(self, page_id: str, all_pages: List) -> List[gr.update]:
127
+ visibility = {"cockpit": 1, "deep_dive": 2, "co-pilot": 3}
128
  return [gr.update(visible=i == visibility.get(page_id, 0)) for i in range(len(all_pages))]
129
 
130
  def _update_plot_controls(self, plot_type: str) -> gr.update:
 
136
  metadata = self._extract_dataset_metadata(df)
137
  state = {'df': df, 'metadata': metadata, 'dashboard_plots': []}
138
  rows, cols, quality = metadata['shape'][0], metadata['shape'][1], metadata['data_quality']
139
+ updates = (state, f"βœ… **{os.path.basename(file_obj.name)}** loaded.", *self._switch_page("cockpit", [0,1,2,3]),
140
+ f"{rows:,}", f"{cols}", f"{quality}%", f"{len(metadata['datetime_cols'])}",
141
+ gr.update(choices=metadata['columns'], interactive=True), gr.update(choices=metadata['columns'], interactive=True), gr.update(interactive=True))
142
+ return updates
143
  except Exception as e:
144
+ gr.Error(f"File Load Error: {e}"); return {}, f"❌ Error: {e}", *self._switch_page("welcome", [0,1,2,3]), "0", "0", "0%", "0", gr.update(choices=[], interactive=False), gr.update(choices=[], interactive=False), gr.update(interactive=False)
145
 
146
  def _extract_dataset_metadata(self, df: pd.DataFrame) -> Dict[str, Any]:
147
  rows, cols = df.shape
 
152
 
153
  def add_plot_to_dashboard(self, state: Dict, x_col: str, y_col: Optional[str], plot_type: str) -> List[Any]:
154
  if len(state.get('dashboard_plots', [])) >= MAX_DASHBOARD_PLOTS:
155
+ gr.Warning(f"Dashboard is full. Max {MAX_DASHBOARD_PLOTS} plots."); return [state, gr.update(), *self._get_plot_updates(state)]
156
+ if not x_col: gr.Warning("Please select an X-axis column."); return [state, gr.update(), *self._get_plot_updates(state)]
157
+ df = state['df']
158
+ title = f"{plot_type.capitalize()}: {y_col} by {x_col}" if y_col and plot_type in ['box', 'scatter'] else f"Distribution of {x_col}"
159
  try:
160
  if plot_type == 'histogram': fig = px.histogram(df, x=x_col, title=title)
161
  elif plot_type == 'box': fig = px.box(df, x=x_col, y=y_col, title=title)
 
165
  fig = px.bar(counts, x=counts.index, y=counts.values, title=f"Top 20 Categories for {x_col}", labels={'index': x_col, 'y': 'Count'})
166
  if fig:
167
  fig.update_layout(template="plotly_dark"); state['dashboard_plots'].append(fig); gr.Info(f"Added '{title}' to dashboard.")
168
+ return [state, gr.update(interactive=True), *self._get_plot_updates(state)]
169
+ except Exception as e: gr.Error(f"Plotting Error: {e}"); return [state, gr.update(), *self._get_plot_updates(state)]
170
 
171
  def _get_plot_updates(self, state: Dict) -> List[gr.update]:
172
  plots = state.get('dashboard_plots', [])
173
+ return [gr.update(value=plots[i] if i < len(plots) else None, visible=i < len(plots)) for i in range(MAX_DASHBOARD_PLOTS)]
 
 
 
 
174
 
175
  def clear_dashboard(self, state: Dict) -> List[Any]:
176
+ state['dashboard_plots'] = []; gr.Info("Dashboard cleared."); return [state, gr.update(interactive=False), *self._get_plot_updates(state)]
177
 
178
  def get_ai_suggestions(self, state: Dict, api_key: str) -> List[gr.update]:
179
  if not api_key: gr.Warning("API Key is required."); return [gr.update(visible=False)]*5
180
  if not state: gr.Warning("Please load data first."); return [gr.update(visible=False)]*5
181
+ metadata = state['metadata']; prompt = f"From columns {metadata['columns']}, generate 4 impactful analytical questions. Return ONLY a JSON list of strings."
 
 
182
  try:
183
  genai.configure(api_key=api_key); suggestions = json.loads(genai.GenerativeModel('gemini-1.5-flash').generate_content(prompt).text)
184
  return [gr.Button(s, visible=True) for s in suggestions] + [gr.Button(visible=False)] * (5 - len(suggestions))
185
  except Exception as e: gr.Error(f"AI Suggestion Error: {e}"); return [gr.update(visible=False)]*5
186
 
187
  def handle_suggestion_click(self, question: str) -> Tuple[gr.update, ...]:
188
+ return *self._switch_page("co-pilot", [0,1,2,3]), question
189
 
190
  def respond_to_chat(self, state: Dict, api_key: str, user_message: str, history: List) -> Any:
191
+ if not user_message.strip(): return history, *[gr.update()]*4
192
  if not api_key or not state:
193
  msg = "I need a Gemini API key and a dataset to work."; history.append((user_message, msg)); return history, *[gr.update(visible=False)]*4
194
 
195
  history.append((user_message, "Thinking... πŸ€”")); yield history, *[gr.update(visible=False)]*4
196
 
197
  metadata, prompt = state['metadata'], f"""You are 'Chief Data Scientist', an expert AI analyst...
198
+ Respond ONLY with a single JSON object with keys: "plan", "code", "insight".
199
+ Metadata: {metadata['dtypes_head']}
200
+ User Question: "{user_message}"
 
 
 
 
 
201
  """
202
  try:
203
+ genai.configure(api_key=api_key); response_json = json.loads(genai.GenerativeModel('gemini-1.5-flash').generate_content(prompt).text.strip().replace("```json", "").replace("```", ""))
 
204
  plan, code, insight = response_json.get("plan"), response_json.get("code"), response_json.get("insight")
205
  stdout, fig, df_result, error = self._safe_exec(code, {'df': state['df'], 'px': px, 'pd': pd})
 
206
  history[-1] = (user_message, f"**Plan:** {plan}")
207
  explanation = f"**Insight:** {insight}"
208
  if stdout: explanation += f"\n\n**Console Output:**\n```\n{stdout}\n```"
209
  if error: gr.Error(f"AI Code Execution Failed: {error}")
 
210
  yield (history, gr.update(visible=bool(explanation), value=explanation), gr.update(visible=bool(code), value=code),
211
  gr.update(visible=bool(fig), value=fig), gr.update(visible=bool(df_result is not None), value=df_result))
212
  except Exception as e:
 
214
  yield history, *[gr.update(visible=False)]*4
215
 
216
  def _safe_exec(self, code_string: str, local_vars: Dict) -> Tuple[Any, ...]:
 
217
  try:
218
+ output_buffer = io.StringIO()
219
  with redirect_stdout(output_buffer): exec(code_string, globals(), local_vars)
220
  return output_buffer.getvalue(), local_vars.get('fig'), local_vars.get('result_df'), None
221
  except Exception as e: return None, None, None, str(e)
222
 
223
  if __name__ == "__main__":
224
+ # Create dummy files for avatar images if they don't exist
225
+ if not os.path.exists("bot.png"):
226
+ from PIL import Image
227
+ img = Image.new('RGB', (100, 100), color = 'darkblue')
228
+ img.save('bot.png')
229
+ if not os.path.exists("user.png"):
230
+ from PIL import Image
231
+ img = Image.new('RGB', (100, 100), color = 'gray')
232
+ img.save('user.png')
233
+ if not os.path.exists("workflow.png"):
234
+ gr.Warning("workflow.png not found. The welcome screen image will be missing.")
235
+
236
  app = DataExplorerApp()
237
  app.launch()