import gradio as gr
import os
from analyst import DataAnalystAgent
from analyst_tools import (
copy_uploaded_file,
cleanup_session_sandbox,
normalize_message_text,
collect_charts,
)
# ── Helpers ────────────────────────────────────────────────────────────────────
def _init_agent():
agent = DataAnalystAgent()
agent.setup()
return agent
def _show_status(text: str) -> str:
"""Return a small HTML status badge."""
return (
f'
'
f' {text}
'
)
def _status_done(text: str) -> str:
return f'{text}
'
def process_message(agent, message, success_criteria, dataset_file, history):
"""Run the agent and return updated UI state with HTML report + notebook."""
if not message.strip():
return (
history, agent, gr.update(),
gr.update(visible=False),
gr.update(value="", visible=False),
gr.update(visible=False),
gr.update(value=None, visible=False),
gr.update(value=""),
)
dataset_filename = None
if dataset_file is not None:
original_name = os.path.basename(dataset_file)
copy_uploaded_file(dataset_file, original_name, agent.session_dir)
dataset_filename = original_name
updated_history, evaluator_feedback, _, nb_path, html_report = agent.run(
message, success_criteria, dataset_filename, history,
)
evaluator_feedback = normalize_message_text(evaluator_feedback)
# Charts gallery
charts = collect_charts(agent.session_dir)
chart_update = gr.update(value=charts, visible=True) if charts else gr.update(visible=False)
# HTML report rendered in-browser
report_update = gr.update(value=html_report, visible=True) if html_report else gr.update(visible=False)
# Notebook download
nb_update = gr.update(value=nb_path, visible=True) if nb_path else gr.update(value=None, visible=False)
# Status line
chart_count = len(charts)
status_parts = ["Analysis complete"]
if chart_count:
status_parts.append(f"{chart_count} chart{'s' if chart_count != 1 else ''}")
if nb_path:
status_parts.append("notebook ready")
status_text = _status_done(" | ".join(status_parts))
return (
updated_history,
agent,
gr.update(value=""),
chart_update,
gr.update(value=evaluator_feedback, visible=bool(evaluator_feedback)),
report_update,
nb_update,
gr.update(value=status_text),
)
def reset_agent(agent):
cleanup_session_sandbox(agent.session_dir)
new_agent = agent.reset()
return (
[],
new_agent,
gr.update(value=""),
gr.update(value=""),
gr.update(value=None),
gr.update(visible=False),
gr.update(value="", visible=False),
gr.update(visible=False),
gr.update(value=None, visible=False),
gr.update(value=""),
)
# ── CSS ────────────────────────────────────────────────────────────────────────
CSS = """
/* ── Page background ─────────────────────────────────────────────────────── */
body {
background:
radial-gradient(ellipse at 10% 0%, rgba(59,130,246,.18), transparent 52%),
radial-gradient(ellipse at 90% 0%, rgba(14,165,233,.14), transparent 42%),
linear-gradient(180deg, #f5faff 0%, #eaf4ff 100%);
font-family: "Manrope", system-ui, sans-serif;
}
.gradio-container { max-width: 1200px !important; }
/* ── Header ──────────────────────────────────────────────────────────────── */
#app-header {
text-align: center;
padding: 1.2rem 0 .6rem;
}
#app-header h1 {
font-size: 1.8rem;
font-family: "Space Grotesk", "Manrope", sans-serif;
background: linear-gradient(135deg, #0f3fb8 0%, #2563eb 48%, #0ea5e9 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
letter-spacing: -0.02em;
font-weight: 700;
}
#app-header p {
font-family: "Manrope", system-ui, sans-serif;
color: #64748b;
font-size: .92rem;
margin: .3rem 0 0;
}
/* ── Cards ───────────────────────────────────────────────────────────────── */
.card {
background: rgba(255,255,255,.92);
backdrop-filter: blur(12px);
border: 1px solid rgba(59,130,246,.12);
border-radius: 16px;
box-shadow: 0 8px 28px rgba(37,99,235,.08);
padding: 1rem;
}
/* ── Chatbot ─────────────────────────────────────────────────────────────── */
#chatbot {
border-radius: 16px !important;
border: 1px solid rgba(59,130,246,.14) !important;
box-shadow: 0 10px 34px rgba(37,99,235,.10) !important;
background: linear-gradient(180deg, #ffffff 0%, #f2f8ff 100%) !important;
}
/* ── Input area ──────────────────────────────────────────────────────────── */
#input-group {
background: rgba(255,255,255,.88);
backdrop-filter: blur(10px);
border: 1px solid rgba(59,130,246,.12);
border-radius: 16px;
box-shadow: 0 8px 24px rgba(37,99,235,.07);
padding: .8rem 1rem;
}
/* ── Buttons ─────────────────────────────────────────────────────────────── */
#go-btn {
min-width: 140px;
background: linear-gradient(135deg, #1d4ed8 0%, #2563eb 58%, #0ea5e9 100%) !important;
color: #fff !important;
border: none !important;
border-radius: 12px !important;
font-weight: 600 !important;
font-size: .95rem !important;
box-shadow: 0 8px 20px rgba(37,99,235,.28) !important;
transition: transform .15s, box-shadow .15s;
}
#go-btn:hover {
transform: translateY(-1px);
box-shadow: 0 10px 26px rgba(37,99,235,.34) !important;
}
#reset-btn {
border: 1px solid rgba(59,130,246,.20) !important;
background: rgba(239,246,255,.92) !important;
color: #1d4ed8 !important;
border-radius: 12px !important;
font-weight: 500 !important;
transition: background .15s;
}
#reset-btn:hover { background: rgba(219,234,254,.96) !important; }
/* ── Tabs ────────────────────────────────────────────────────────────────── */
.gr-tab-nav button {
font-family: "Space Grotesk", "Manrope", sans-serif !important;
font-weight: 700 !important;
font-size: .98rem !important;
letter-spacing: .01em !important;
color: #335c8a !important;
background: rgba(255,255,255,.9) !important;
border: 1px solid rgba(59,130,246,.10) !important;
border-radius: 10px 10px 0 0 !important;
padding-top: .55rem !important;
padding-bottom: .55rem !important;
transition: color .15s, transform .15s;
}
.gr-tab-nav button:hover {
color: #1d4ed8 !important;
transform: translateY(-1px);
}
.gr-tab-nav button.selected {
color: #1d4ed8 !important;
background: #ffffff !important;
border-bottom: 2px solid #1d4ed8 !important;
box-shadow: 0 8px 20px rgba(37,99,235,.10) !important;
}
.gr-tab-nav button *, .gr-tab-nav button span, .gr-tab-nav button div {
color: inherit !important;
}
/* ── Sidebar labels ──────────────────────────────────────────────────────── */
.sidebar-label {
font-size: .82rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .06em;
color: #2563eb;
margin: .6rem 0 .3rem;
}
/* ── Status badge ────────────────────────────────────────────────────────── */
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: .82rem;
font-weight: 600;
color: #1d4ed8;
padding: 4px 12px;
border-radius: 999px;
background: rgba(59,130,246,.10);
}
.status-badge.done { color: #0369a1; background: rgba(14,165,233,.12); }
.pulse {
width: 8px; height: 8px;
border-radius: 50%;
background: #2563eb;
animation: pulse-anim 1.4s infinite;
}
@keyframes pulse-anim {
0% { opacity: 1; transform: scale(1); }
50% { opacity: .4; transform: scale(1.4); }
100% { opacity: 1; transform: scale(1); }
}
/* ── Gallery ─────────────────────────────────────────────────────────────── */
.gr-gallery {
border-radius: 12px !important;
border: 1px solid rgba(59,130,246,.10) !important;
}
/* ── File upload / download ──────────────────────────────────────────────── */
.gr-file {
border-radius: 12px !important;
border: 1px dashed rgba(59,130,246,.24) !important;
background: rgba(240,249,255,.95) !important;
}
.gr-file, .gr-file * {
color: #1e293b !important;
}
.gr-button, .gr-button * {
color: inherit !important;
}
.gr-download-button, .gr-download-button * {
color: #1e293b !important;
}
.gr-markdown, .gr-markdown * {
color: #1e293b;
}
/* ── Report embed ────────────────────────────────────────────────────────── */
#tab-report > div {
width: 100% !important;
max-width: 100% !important;
}
#tab-report .prose {
max-width: 100% !important;
}
/* Force dark text inside the HTML report for readability */
#tab-report .rpt,
#tab-report .rpt * {
color: #1e293b;
}
#tab-report .rpt h1, #tab-report .rpt h2 {
color: #1d4ed8;
}
#tab-report .rpt .findings {
background: #f3f8ff;
border: 1px solid #cfe3ff;
}
/* ── Tab panel backgrounds ───────────────────────────────────────────────── */
.gr-tabitem {
background: rgba(255,255,255,.72) !important;
border-radius: 0 0 12px 12px;
}
/* ── Misc ────────────────────────────────────────────────────────────────── */
footer { display: none !important; }
.gr-examples .gr-samples-table { border-radius: 12px !important; }
"""
# ── Build UI ───────────────────────────────────────────────────────────────────
THEME = gr.themes.Soft(
primary_hue=gr.themes.colors.indigo,
secondary_hue=gr.themes.colors.amber,
neutral_hue=gr.themes.colors.slate,
font=[
gr.themes.GoogleFont("Manrope"),
gr.themes.GoogleFont("Space Grotesk"),
"system-ui",
"sans-serif",
],
)
with gr.Blocks(title="Data Analyst Agent") as ui:
# ── Header ─────────────────────────────────────────────────────────────────
gr.HTML(
'"
)
agent_state = gr.State()
# ── Top bar: dataset upload + status ───────────────────────────────────────
with gr.Row(equal_height=True):
with gr.Column(scale=3):
dataset_upload = gr.File(
label="Dataset (CSV)",
file_types=[".csv"],
type="filepath",
elem_classes=["card"],
)
with gr.Column(scale=2):
status_html = gr.HTML(value="", elem_id="status-bar")
# ── Main content: left = chat, right = results tabs ────────────────────────
with gr.Row(equal_height=False):
# ── Left: conversation ─────────────────────────────────────────────────
with gr.Column(scale=5, min_width=440):
chatbot = gr.Chatbot(
label="Conversation",
elem_id="chatbot",
height=500,
avatar_images=(
None,
"https://api.dicebear.com/9.x/bottts/svg?seed=analyst",
),
)
# ── Input area ─────────────────────────────────────────────────────
with gr.Group(elem_id="input-group"):
message = gr.Textbox(
show_label=False,
placeholder="Ask a question about your data ...",
lines=1,
max_lines=4,
scale=4,
)
success_criteria = gr.Textbox(
show_label=False,
placeholder="Success criteria (optional) — e.g. Include correlation matrix and at least one chart",
lines=1,
max_lines=2,
scale=4,
)
with gr.Row():
reset_btn = gr.Button(
"Reset",
variant="stop",
elem_id="reset-btn",
size="sm",
)
go_btn = gr.Button(
"Analyse",
variant="primary",
elem_id="go-btn",
size="lg",
)
# ── Right: results tabs ────────────────────────────────────────────────
with gr.Column(scale=4, min_width=360):
with gr.Tabs():
# Tab 1 — Report
with gr.Tab("Report", id="tab-report"):
report_html = gr.HTML(
label="Analysis Report",
visible=False,
)
gr.Markdown(
""
"Run an analysis to see the report here.
",
elem_id="report-placeholder",
)
# Tab 2 — Charts
with gr.Tab("Charts", id="tab-charts"):
chart_gallery = gr.Gallery(
label="Generated Charts",
columns=2,
rows=2,
visible=False,
height=400,
object_fit="contain",
)
gr.Markdown(
""
"Charts will appear here after analysis.
",
elem_id="charts-placeholder",
)
# Tab 3 — Save / Download
with gr.Tab("Save", id="tab-save"):
gr.HTML(
''
''
"The notebook (.ipynb) bundles your analysis code, "
"charts, and findings in one file.
"
)
notebook_download = gr.File(
label="Notebook (.ipynb)",
visible=False,
interactive=False,
)
# Tab 4 — Evaluator
with gr.Tab("Evaluator", id="tab-eval"):
gr.Markdown(
""
"Internal quality evaluator feedback.
"
)
evaluator_feedback = gr.Markdown(visible=False)
# ── Example prompts ────────────────────────────────────────────────────────
gr.Examples(
examples=[
[
"What are the key trends in this dataset? Detect any outliers.",
"Include at least one chart",
],
[
"Run a correlation analysis and identify the top 3 most correlated pairs.",
"",
],
[
"Forecast the next 3 periods using a simple linear trend.",
"",
],
],
inputs=[message, success_criteria],
label="Quick-start prompts",
)
# ── Events ─────────────────────────────────────────────────────────────────
ui.load(_init_agent, [], [agent_state])
shared_inputs = [agent_state, message, success_criteria, dataset_upload, chatbot]
shared_outputs = [
chatbot, agent_state, message, chart_gallery, evaluator_feedback,
report_html, notebook_download, status_html,
]
go_btn.click(process_message, shared_inputs, shared_outputs)
message.submit(process_message, shared_inputs, shared_outputs)
reset_btn.click(
reset_agent,
[agent_state],
[
chatbot, agent_state, message, success_criteria, dataset_upload,
chart_gallery, evaluator_feedback, report_html, notebook_download,
status_html,
],
)
if __name__ == "__main__":
ui.launch(theme=THEME, css=CSS)