Update app.py
Browse files
app.py
CHANGED
@@ -13,19 +13,22 @@ from contextlib import redirect_stdout
|
|
13 |
|
14 |
# --- Configuration ---
|
15 |
warnings.filterwarnings('ignore')
|
|
|
|
|
|
|
16 |
CSS = """
|
17 |
-
/* --- Phoenix UI Custom CSS --- */
|
18 |
/* Stat Card Styling */
|
19 |
.stat-card {
|
20 |
border-radius: 12px !important;
|
21 |
padding: 20px !important;
|
22 |
-
background: #
|
23 |
-
border: 1px solid #
|
24 |
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
25 |
text-align: center;
|
26 |
}
|
27 |
-
.stat-card-title { font-size: 16px; font-weight: 500; color: #
|
28 |
-
.stat-card-value { font-size: 32px; font-weight: 700; color: #
|
29 |
|
30 |
/* General Layout & Feel */
|
31 |
.gradio-container { font-family: 'Inter', sans-serif; }
|
@@ -33,9 +36,9 @@ CSS = """
|
|
33 |
|
34 |
/* Sidebar Styling */
|
35 |
.sidebar {
|
36 |
-
background-color: #
|
37 |
padding: 15px;
|
38 |
-
border-right: 1px solid #
|
39 |
min-height: 100vh;
|
40 |
}
|
41 |
.sidebar .gr-button {
|
@@ -44,18 +47,23 @@ CSS = """
|
|
44 |
background: none !important;
|
45 |
border: none !important;
|
46 |
box-shadow: none !important;
|
47 |
-
color: #
|
48 |
font-size: 16px !important;
|
49 |
padding: 12px 10px !important;
|
50 |
margin-bottom: 8px !important;
|
51 |
border-radius: 8px !important;
|
52 |
}
|
53 |
-
.sidebar .gr-button:hover { background-color: #
|
54 |
-
.sidebar .gr-button.selected { background-color: #
|
55 |
|
56 |
/* AI Co-pilot Styling */
|
57 |
-
.code-block { border: 1px solid #
|
58 |
-
.explanation-block {
|
|
|
|
|
|
|
|
|
|
|
59 |
"""
|
60 |
|
61 |
# --- Helper Functions ---
|
@@ -140,8 +148,8 @@ def switch_page(page_name):
|
|
140 |
# --- Page 1: Data Cockpit ---
|
141 |
def get_ai_suggestions(state_dict, api_key):
|
142 |
"""Generates proactive analytical suggestions from the AI."""
|
143 |
-
if not api_key: return "Enter your Gemini API key to get suggestions.", gr.update(visible=False)
|
144 |
-
if not state_dict: return "Upload data first.", gr.update(visible=False)
|
145 |
|
146 |
metadata = state_dict['metadata']
|
147 |
prompt = f"""
|
@@ -159,9 +167,7 @@ def get_ai_suggestions(state_dict, api_key):
|
|
159 |
response = model.generate_content(prompt)
|
160 |
suggestions = json.loads(response.text)
|
161 |
|
162 |
-
# Create a button for each suggestion
|
163 |
buttons = [gr.Button(s, variant="secondary", visible=True) for s in suggestions]
|
164 |
-
# Pad with hidden buttons to always have 5 outputs
|
165 |
buttons += [gr.Button(visible=False)] * (5 - len(buttons))
|
166 |
|
167 |
return gr.update(visible=False), *buttons
|
@@ -197,9 +203,9 @@ def add_plot_to_dashboard(state_dict, x_col, y_col, plot_type):
|
|
197 |
fig.update_xaxes(title=x_col)
|
198 |
|
199 |
if fig:
|
|
|
200 |
state_dict['dashboard_plots'].append(fig)
|
201 |
|
202 |
-
# Rebuild the accordion with all plots
|
203 |
accordion_children = [gr.Plot(fig, visible=True) for fig in state_dict['dashboard_plots']]
|
204 |
return state_dict, gr.Accordion(label="Your Dashboard Plots", children=accordion_children, open=True)
|
205 |
except Exception as e:
|
@@ -232,7 +238,7 @@ def respond_to_chat(user_message, history, state_dict, api_key):
|
|
232 |
2. Formulate a plan (thought process).
|
233 |
3. Write Python code to execute that plan.
|
234 |
4. The code can use pandas (pd), numpy (np), and plotly.express (px).
|
235 |
-
5. **For plots, assign the figure to a variable `fig` (e.g., `fig = px.histogram(...)`).**
|
236 |
6. **For table-like results, assign the final DataFrame to a variable `result_df` (e.g., `result_df = df.describe()`).**
|
237 |
7. Do not modify the original `df`. Use `df.copy()` if needed.
|
238 |
8. Provide a brief, user-friendly explanation of the result.
|
@@ -264,15 +270,13 @@ def respond_to_chat(user_message, history, state_dict, api_key):
|
|
264 |
bot_message = f"π€ **Thought:** *{thought}*"
|
265 |
history[-1] = (user_message, bot_message)
|
266 |
|
267 |
-
|
268 |
-
output_updates = [gr.update(visible=False, value=None)] * 4 # [explanation, code, plot, table]
|
269 |
|
270 |
if explanation: output_updates[0] = gr.update(visible=True, value=f"**Phoenix Co-pilot:** {explanation}")
|
271 |
if code_to_run: output_updates[1] = gr.update(visible=True, value=code_to_run)
|
272 |
if fig_result: output_updates[2] = gr.update(visible=True, value=fig_result)
|
273 |
if df_result is not None: output_updates[3] = gr.update(visible=True, value=df_result)
|
274 |
if stdout:
|
275 |
-
# Append stdout to explanation if it exists
|
276 |
new_explanation = (output_updates[0]['value'] if output_updates[0]['visible'] else "") + f"\n\n**Console Output:**\n```\n{stdout}\n```"
|
277 |
output_updates[0] = gr.update(visible=True, value=new_explanation)
|
278 |
if error:
|
@@ -288,14 +292,14 @@ def respond_to_chat(user_message, history, state_dict, api_key):
|
|
288 |
|
289 |
# --- Gradio UI Definition ---
|
290 |
def create_gradio_interface():
|
291 |
-
|
|
|
292 |
global_state = gr.State({})
|
293 |
|
294 |
with gr.Row():
|
295 |
# --- Sidebar ---
|
296 |
with gr.Column(scale=1, elem_classes="sidebar"):
|
297 |
-
gr.Markdown("
|
298 |
-
gr.Markdown("AI Data Explorer")
|
299 |
|
300 |
# Navigation buttons
|
301 |
cockpit_btn = gr.Button("π Data Cockpit", elem_classes="selected")
|
@@ -316,7 +320,6 @@ def create_gradio_interface():
|
|
316 |
with gr.Column(visible=True) as welcome_page:
|
317 |
gr.Markdown("# Welcome to the AI Data Explorer (Phoenix UI)", elem_id="welcome-header")
|
318 |
gr.Markdown("Please **upload a CSV file** and **enter your Gemini API key** in the sidebar to begin.")
|
319 |
-
# CORRECTED: Uses a local file, 'workflow.png', which must be in the same directory.
|
320 |
gr.Image(value="workflow.png", label="Workflow", show_label=False, show_download_button=False, container=False)
|
321 |
|
322 |
# Page 1: Data Cockpit (Hidden initially)
|
@@ -324,16 +327,16 @@ def create_gradio_interface():
|
|
324 |
gr.Markdown("## π Data Cockpit")
|
325 |
with gr.Row():
|
326 |
with gr.Column(elem_classes="stat-card"):
|
327 |
-
gr.Markdown("<div class='stat-card-title'>Rows</div>"
|
328 |
rows_stat = gr.Textbox("0", show_label=False, elem_classes="stat-card-value")
|
329 |
with gr.Column(elem_classes="stat-card"):
|
330 |
-
gr.Markdown("<div class='stat-card-title'>Columns</div>"
|
331 |
cols_stat = gr.Textbox("0", show_label=False, elem_classes="stat-card-value")
|
332 |
with gr.Column(elem_classes="stat-card"):
|
333 |
-
gr.Markdown("<div class='stat-card-title'>Data Quality</div>"
|
334 |
quality_stat = gr.Textbox("0%", show_label=False, elem_classes="stat-card-value")
|
335 |
with gr.Column(elem_classes="stat-card"):
|
336 |
-
gr.Markdown("<div class='stat-card-title'>Date/Time Cols</div>"
|
337 |
time_cols_stat = gr.Textbox("0", show_label=False, elem_classes="stat-card-value")
|
338 |
|
339 |
suggestion_status = gr.Markdown(visible=True)
|
@@ -374,13 +377,8 @@ def create_gradio_interface():
|
|
374 |
nav_buttons = [cockpit_btn, deep_dive_btn, copilot_btn]
|
375 |
|
376 |
for i, btn in enumerate(nav_buttons):
|
377 |
-
btn.click(
|
378 |
-
|
379 |
-
outputs=pages
|
380 |
-
).then(
|
381 |
-
lambda i=i: [gr.update(elem_classes="selected" if j==i else "") for j in range(len(nav_buttons))],
|
382 |
-
outputs=nav_buttons
|
383 |
-
)
|
384 |
|
385 |
file_input.upload(
|
386 |
fn=load_and_process_file,
|
@@ -388,41 +386,22 @@ def create_gradio_interface():
|
|
388 |
outputs=[global_state, status_output, welcome_page, cockpit_page, deep_dive_page, copilot_page,
|
389 |
rows_stat, cols_stat, quality_stat, time_cols_stat,
|
390 |
x_col_dd, y_col_dd, plot_type_dd]
|
391 |
-
).then(
|
392 |
-
|
393 |
-
).then(
|
394 |
-
lambda: (gr.update(elem_classes="selected"), gr.update(elem_classes=""), gr.update(elem_classes="")), outputs=nav_buttons
|
395 |
-
)
|
396 |
|
397 |
-
suggestion_btn.click(
|
398 |
-
get_ai_suggestions,
|
399 |
-
[global_state, api_key_input],
|
400 |
-
[suggestion_status, *suggestion_buttons]
|
401 |
-
)
|
402 |
|
403 |
for btn in suggestion_buttons:
|
404 |
-
btn.click(
|
405 |
-
|
406 |
-
inputs=[btn],
|
407 |
-
outputs=[cockpit_page, deep_dive_page, copilot_page, chat_input]
|
408 |
-
).then(
|
409 |
-
lambda: (gr.update(elem_classes=""), gr.update(elem_classes=""), gr.update(elem_classes="selected")),
|
410 |
-
outputs=nav_buttons
|
411 |
-
)
|
412 |
|
413 |
add_plot_btn.click(add_plot_to_dashboard, [global_state, x_col_dd, y_col_dd, plot_type_dd], [global_state, dashboard_accordion])
|
414 |
clear_plots_btn.click(clear_dashboard, [global_state], [global_state, dashboard_accordion])
|
415 |
|
416 |
-
chat_submit_btn.click(
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
).then(lambda: "", outputs=[chat_input])
|
421 |
-
chat_input.submit(
|
422 |
-
respond_to_chat,
|
423 |
-
[chat_input, chatbot, global_state, api_key_input],
|
424 |
-
[chatbot, copilot_explanation, copilot_code, copilot_plot, copilot_table]
|
425 |
-
).then(lambda: "", outputs=[chat_input])
|
426 |
|
427 |
return demo
|
428 |
|
|
|
13 |
|
14 |
# --- Configuration ---
|
15 |
warnings.filterwarnings('ignore')
|
16 |
+
|
17 |
+
# --- NEW: Expert-Crafted Dark Theme CSS ---
|
18 |
+
# This CSS is designed to work with a dark theme, ensuring all elements are visible and stylish.
|
19 |
CSS = """
|
20 |
+
/* --- Phoenix UI Custom Dark CSS --- */
|
21 |
/* Stat Card Styling */
|
22 |
.stat-card {
|
23 |
border-radius: 12px !important;
|
24 |
padding: 20px !important;
|
25 |
+
background: #1f2937 !important; /* Dark blue-gray background */
|
26 |
+
border: 1px solid #374151 !important;
|
27 |
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
28 |
text-align: center;
|
29 |
}
|
30 |
+
.stat-card-title { font-size: 16px; font-weight: 500; color: #9ca3af !important; margin-bottom: 8px; }
|
31 |
+
.stat-card-value { font-size: 32px; font-weight: 700; color: #f9fafb !important; }
|
32 |
|
33 |
/* General Layout & Feel */
|
34 |
.gradio-container { font-family: 'Inter', sans-serif; }
|
|
|
36 |
|
37 |
/* Sidebar Styling */
|
38 |
.sidebar {
|
39 |
+
background-color: #111827 !important; /* Very dark blue-gray */
|
40 |
padding: 15px;
|
41 |
+
border-right: 1px solid #374151 !important;
|
42 |
min-height: 100vh;
|
43 |
}
|
44 |
.sidebar .gr-button {
|
|
|
47 |
background: none !important;
|
48 |
border: none !important;
|
49 |
box-shadow: none !important;
|
50 |
+
color: #d1d5db !important; /* Light gray text for readability */
|
51 |
font-size: 16px !important;
|
52 |
padding: 12px 10px !important;
|
53 |
margin-bottom: 8px !important;
|
54 |
border-radius: 8px !important;
|
55 |
}
|
56 |
+
.sidebar .gr-button:hover { background-color: #374151 !important; } /* Hover state */
|
57 |
+
.sidebar .gr-button.selected { background-color: #4f46e5 !important; font-weight: 600 !important; color: white !important; } /* Selected state with primary color */
|
58 |
|
59 |
/* AI Co-pilot Styling */
|
60 |
+
.code-block { border: 1px solid #374151 !important; border-radius: 8px; }
|
61 |
+
.explanation-block {
|
62 |
+
background-color: #1e3a8a !important; /* Dark blue background */
|
63 |
+
border-left: 4px solid #3b82f6 !important; /* Brighter blue border */
|
64 |
+
padding: 12px;
|
65 |
+
color: #e5e7eb !important;
|
66 |
+
}
|
67 |
"""
|
68 |
|
69 |
# --- Helper Functions ---
|
|
|
148 |
# --- Page 1: Data Cockpit ---
|
149 |
def get_ai_suggestions(state_dict, api_key):
|
150 |
"""Generates proactive analytical suggestions from the AI."""
|
151 |
+
if not api_key: return "Enter your Gemini API key to get suggestions.", *[gr.update(visible=False)]*5
|
152 |
+
if not state_dict: return "Upload data first.", *[gr.update(visible=False)]*5
|
153 |
|
154 |
metadata = state_dict['metadata']
|
155 |
prompt = f"""
|
|
|
167 |
response = model.generate_content(prompt)
|
168 |
suggestions = json.loads(response.text)
|
169 |
|
|
|
170 |
buttons = [gr.Button(s, variant="secondary", visible=True) for s in suggestions]
|
|
|
171 |
buttons += [gr.Button(visible=False)] * (5 - len(buttons))
|
172 |
|
173 |
return gr.update(visible=False), *buttons
|
|
|
203 |
fig.update_xaxes(title=x_col)
|
204 |
|
205 |
if fig:
|
206 |
+
fig.update_layout(template="plotly_dark") # Ensure plots match the dark theme
|
207 |
state_dict['dashboard_plots'].append(fig)
|
208 |
|
|
|
209 |
accordion_children = [gr.Plot(fig, visible=True) for fig in state_dict['dashboard_plots']]
|
210 |
return state_dict, gr.Accordion(label="Your Dashboard Plots", children=accordion_children, open=True)
|
211 |
except Exception as e:
|
|
|
238 |
2. Formulate a plan (thought process).
|
239 |
3. Write Python code to execute that plan.
|
240 |
4. The code can use pandas (pd), numpy (np), and plotly.express (px).
|
241 |
+
5. **For plots, assign the figure to a variable `fig` (e.g., `fig = px.histogram(...)`). IMPORTANT: you MUST add `template='plotly_dark'` to all plotly figures to match the UI theme.**
|
242 |
6. **For table-like results, assign the final DataFrame to a variable `result_df` (e.g., `result_df = df.describe()`).**
|
243 |
7. Do not modify the original `df`. Use `df.copy()` if needed.
|
244 |
8. Provide a brief, user-friendly explanation of the result.
|
|
|
270 |
bot_message = f"π€ **Thought:** *{thought}*"
|
271 |
history[-1] = (user_message, bot_message)
|
272 |
|
273 |
+
output_updates = [gr.update(visible=False, value=None)] * 4
|
|
|
274 |
|
275 |
if explanation: output_updates[0] = gr.update(visible=True, value=f"**Phoenix Co-pilot:** {explanation}")
|
276 |
if code_to_run: output_updates[1] = gr.update(visible=True, value=code_to_run)
|
277 |
if fig_result: output_updates[2] = gr.update(visible=True, value=fig_result)
|
278 |
if df_result is not None: output_updates[3] = gr.update(visible=True, value=df_result)
|
279 |
if stdout:
|
|
|
280 |
new_explanation = (output_updates[0]['value'] if output_updates[0]['visible'] else "") + f"\n\n**Console Output:**\n```\n{stdout}\n```"
|
281 |
output_updates[0] = gr.update(visible=True, value=new_explanation)
|
282 |
if error:
|
|
|
292 |
|
293 |
# --- Gradio UI Definition ---
|
294 |
def create_gradio_interface():
|
295 |
+
# --- CORRECTED: Using a standard dark theme + custom dark CSS ---
|
296 |
+
with gr.Blocks(theme=gr.themes.Default(primary_hue="indigo", secondary_hue="blue").dark(), css=CSS, title="Phoenix AI Data Explorer") as demo:
|
297 |
global_state = gr.State({})
|
298 |
|
299 |
with gr.Row():
|
300 |
# --- Sidebar ---
|
301 |
with gr.Column(scale=1, elem_classes="sidebar"):
|
302 |
+
gr.Markdown("## π Phoenix UI")
|
|
|
303 |
|
304 |
# Navigation buttons
|
305 |
cockpit_btn = gr.Button("π Data Cockpit", elem_classes="selected")
|
|
|
320 |
with gr.Column(visible=True) as welcome_page:
|
321 |
gr.Markdown("# Welcome to the AI Data Explorer (Phoenix UI)", elem_id="welcome-header")
|
322 |
gr.Markdown("Please **upload a CSV file** and **enter your Gemini API key** in the sidebar to begin.")
|
|
|
323 |
gr.Image(value="workflow.png", label="Workflow", show_label=False, show_download_button=False, container=False)
|
324 |
|
325 |
# Page 1: Data Cockpit (Hidden initially)
|
|
|
327 |
gr.Markdown("## π Data Cockpit")
|
328 |
with gr.Row():
|
329 |
with gr.Column(elem_classes="stat-card"):
|
330 |
+
gr.Markdown("<div class='stat-card-title'>Rows</div>")
|
331 |
rows_stat = gr.Textbox("0", show_label=False, elem_classes="stat-card-value")
|
332 |
with gr.Column(elem_classes="stat-card"):
|
333 |
+
gr.Markdown("<div class='stat-card-title'>Columns</div>")
|
334 |
cols_stat = gr.Textbox("0", show_label=False, elem_classes="stat-card-value")
|
335 |
with gr.Column(elem_classes="stat-card"):
|
336 |
+
gr.Markdown("<div class='stat-card-title'>Data Quality</div>")
|
337 |
quality_stat = gr.Textbox("0%", show_label=False, elem_classes="stat-card-value")
|
338 |
with gr.Column(elem_classes="stat-card"):
|
339 |
+
gr.Markdown("<div class='stat-card-title'>Date/Time Cols</div>")
|
340 |
time_cols_stat = gr.Textbox("0", show_label=False, elem_classes="stat-card-value")
|
341 |
|
342 |
suggestion_status = gr.Markdown(visible=True)
|
|
|
377 |
nav_buttons = [cockpit_btn, deep_dive_btn, copilot_btn]
|
378 |
|
379 |
for i, btn in enumerate(nav_buttons):
|
380 |
+
btn.click(lambda i=i: (gr.update(visible=i==0), gr.update(visible=i==1), gr.update(visible=i==2)), outputs=pages) \
|
381 |
+
.then(lambda i=i: [gr.update(elem_classes="selected" if j==i else "") for j in range(len(nav_buttons))], outputs=nav_buttons)
|
|
|
|
|
|
|
|
|
|
|
382 |
|
383 |
file_input.upload(
|
384 |
fn=load_and_process_file,
|
|
|
386 |
outputs=[global_state, status_output, welcome_page, cockpit_page, deep_dive_page, copilot_page,
|
387 |
rows_stat, cols_stat, quality_stat, time_cols_stat,
|
388 |
x_col_dd, y_col_dd, plot_type_dd]
|
389 |
+
).then(lambda: (gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)), outputs=pages) \
|
390 |
+
.then(lambda: (gr.update(elem_classes="selected"), gr.update(elem_classes=""), gr.update(elem_classes="")), outputs=nav_buttons)
|
|
|
|
|
|
|
391 |
|
392 |
+
suggestion_btn.click(get_ai_suggestions, [global_state, api_key_input], [suggestion_status, *suggestion_buttons])
|
|
|
|
|
|
|
|
|
393 |
|
394 |
for btn in suggestion_buttons:
|
395 |
+
btn.click(handle_suggestion_click, inputs=[btn], outputs=[cockpit_page, deep_dive_page, copilot_page, chat_input]) \
|
396 |
+
.then(lambda: (gr.update(elem_classes=""), gr.update(elem_classes=""), gr.update(elem_classes="selected")), outputs=nav_buttons)
|
|
|
|
|
|
|
|
|
|
|
|
|
397 |
|
398 |
add_plot_btn.click(add_plot_to_dashboard, [global_state, x_col_dd, y_col_dd, plot_type_dd], [global_state, dashboard_accordion])
|
399 |
clear_plots_btn.click(clear_dashboard, [global_state], [global_state, dashboard_accordion])
|
400 |
|
401 |
+
chat_submit_btn.click(respond_to_chat, [chat_input, chatbot, global_state, api_key_input], [chatbot, copilot_explanation, copilot_code, copilot_plot, copilot_table]) \
|
402 |
+
.then(lambda: "", outputs=[chat_input])
|
403 |
+
chat_input.submit(respond_to_chat, [chat_input, chatbot, global_state, api_key_input], [chatbot, copilot_explanation, copilot_code, copilot_plot, copilot_table]) \
|
404 |
+
.then(lambda: "", outputs=[chat_input])
|
|
|
|
|
|
|
|
|
|
|
|
|
405 |
|
406 |
return demo
|
407 |
|