import gradio as gr
import pandas as pd
import os
import base64
from pathlib import Path
from data_engine import (
clean_numeric, run_analysis, create_visualization, handle_missing_data,
undo_last_change, undo_all_changes, download_dataset,
display_data_format, display_text_format
)
try:
from ai_agent import initialize_llm, analyze_question
except (ImportError, RuntimeError) as e:
print(f"Warning: Full AI agent not available: {e}")
def initialize_llm():
return None
def analyze_question(question, columns, df, llm):
return "AI agent not available. Please check dependencies.", None, None
from prompts import SAMPLE_QUESTIONS
llm = None
uploaded_df = None
original_df = None
dataset_name = None
change_history = []
# Fix: Files are in root directory, not in public/ folder
SCRIPT_DIR = Path(__file__).parent.absolute()
logo_path = SCRIPT_DIR / "main-logo.png" # Changed from public/main-logo.png
css_path = SCRIPT_DIR / "style.css" # Changed from public/style.css
def embed_image_base64(path):
"""Safely embed image as base64 - returns empty string if file not found"""
try:
if Path(path).exists():
with open(path, "rb") as f:
return "data:image/png;base64," + base64.b64encode(f.read()).decode()
else:
print(f"Warning: Logo file not found at {path}")
return ""
except Exception as e:
print(f"Warning: Could not load logo: {e}")
return ""
def load_css(path):
"""Safely load CSS file"""
try:
if Path(path).exists():
with open(path, "r", encoding="utf-8") as f:
return f.read()
else:
print(f"Warning: CSS file not found at {path}")
return ""
except Exception as e:
print(f"Warning: Could not load CSS: {e}")
return ""
logo_b64 = embed_image_base64(logo_path)
css = load_css(css_path)
# Base + custom CSS
custom_css = css + """
.chat-question-input textarea {
min-height: 40px !important;
max-height: 40px !important;
height: 40px !important;
resize: none !important;
}
/* Hide the 'or' text in file upload */
.gr-file span.or,
span[class*="or"],
.upload-text span {
display: none !important;
}
.gr-file,
.gr-file .wrap,
.gr-file .wrap > div {
position: relative !important;
}
.gr-file svg,
.gr-file .wrap svg,
svg.feather-upload {
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
width: 60px !important;
height: 60px !important;
opacity: 0.9 !important;
margin: 0 !important;
z-index: 10 !important;
}
.gr-file .wrap span {
opacity: 0 !important;
}
#analysis-type-box {
padding: 12px !important;
min-height: auto !important;
}
#analysis-type-box h3 {
font-size: 16px !important;
margin: 0 0 8px 0 !important;
}
#visualization-box {
padding: 12px !important;
min-height: auto !important;
}
#visualization-box h3 {
font-size: 16px !important;
margin: 0 0 8px 0 !important;
}
/* Column Selector */
.gr-checkbox-group,
.gr-checkboxgroup {
background: transparent !important;
}
.gr-checkbox-group label,
.gr-checkboxgroup label,
.gr-checkbox-group span,
.gr-checkboxgroup span {
color: #000000 !important;
}
/* Display Format label - white text */
.gradio-container .contain span[class*="svelte"] {
color: rgb(255, 255, 255) !important;
}
.gradio-container.gradio-container-4-20-0 .contain span[class*="svelte"] {
color: rgb(255, 255, 255) !important;
}
/* Force disable text wrapping in all dataframes */
.gradio-container table td,
.gradio-container table th,
.dataframe td,
.dataframe th,
table.dataframe td,
table.dataframe th,
.gr-dataframe td,
.gr-dataframe th {
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
/* Target Gradio's internal dataframe cells */
div[class*="table"] td,
div[class*="table"] th,
div[class*="dataframe"] td,
div[class*="dataframe"] th {
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
/* Disable wrapping in table data elements */
.gradio-container [data-testid="table"] td,
.gradio-container [data-testid="table"] th {
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
/* ====================================================================== */
/* FIX WHITE BORDER AROUND ALL DROPDOWNS — ONLY REQUIRED CHANGES APPLIED */
/* ====================================================================== */
/* Prevent clipping of borders */
.wrap,
.wrap-inner,
.secondary-wrap,
.container,
.input-container {
overflow: visible !important;
}
/* The actual dropdown box — apply the white border here */
.input-container {
border: 2px solid #ffffff !important;
border-radius: 12px !important;
padding: 12px !important;
box-sizing: border-box !important;
background: transparent !important;
min-height: 58px !important;
}
/* Ensure child elements render fully */
.input-container > * {
overflow: visible !important;
}
/* Remove duplicate border on secondary-wrap */
.secondary-wrap,
.secondary-wrap.svelte-vomtxz {
border: none !important;
padding: 0 !important;
background: none !important;
}
/* Clean container without affecting layout */
.svelte-vomtxz.container {
border: none !important;
padding: 0 !important;
margin: 0 !important;
background: none !important;
box-shadow: none !important;
border-radius: 0 !important;
}
/* Remove problematic display: contents */
.wrap.svelte-vomtxz {
display: block !important;
padding: 0 !important;
border: none !important;
background: none !important;
box-shadow: none !important;
}
/* Simple white border around the dropdown wrapper */
.wrap-inner.svelte-vomtxz {
border: 2px solid white !important;
border-radius: 8px !important;
box-sizing: border-box !important;
}
/* Force component-37 to be a single-line input box */
#component-37 {
display: block !important;
visibility: visible !important;
}
#component-37.hide-container {
display: block !important;
visibility: visible !important;
}
#component-37 .block,
#component-37 .svelte-90oupt,
#component-37 .padded {
display: block !important;
visibility: visible !important;
}
#component-37 textarea,
#component-37 input,
#component-37 .gr-textbox textarea,
#component-37 .gr-textbox input {
display: block !important;
visibility: visible !important;
min-height: 45px !important;
max-height: 45px !important;
height: 45px !important;
resize: none !important;
overflow: hidden !important;
line-height: 45px !important;
padding: 0 15px !important;
background: white !important;
border: 1px solid #ccc !important;
border-radius: 8px !important;
color: #333 !important;
}
#component-37 label {
display: none !important;
}
/* Force component-88 to be a single-line input box */
#component-88 {
display: block !important;
visibility: visible !important;
}
#component-88.hide-container {
display: block !important;
visibility: visible !important;
}
#component-88 .block,
#component-88 .svelte-90oupt,
#component-88 .padded {
display: block !important;
visibility: visible !important;
}
#component-88 textarea,
#component-88 input,
#component-88 .gr-textbox textarea,
#component-88 .gr-textbox input {
display: block !important;
visibility: visible !important;
min-height: 45px !important;
max-height: 45px !important;
height: 45px !important;
resize: none !important;
overflow: hidden !important;
line-height: 45px !important;
padding: 0 15px !important;
background: white !important;
border: 1px solid #ccc !important;
border-radius: 8px !important;
color: #333 !important;
}
#component-88 label {
display: none !important;
}
/* Make Sample Questions heading text black */
.sample-header,
.sample-header h4,
.sample-header p {
color: #000000 !important;
}
/* Force Enter Your Question h4 text to black */
.chat-popup-box h4,
.chat-popup-box .gr-markdown h4 {
color: #000000 !important;
}
/* Make entire chat popup box scrollable */
.chat-popup-box {
overflow-y: auto !important;
}
.chat-popup-box > * {
flex-shrink: 0 !important;
}
/* Make all markdown text in chat popup black */
.chat-popup-box .gr-markdown,
.chat-popup-box .gr-markdown h1,
.chat-popup-box .gr-markdown h2,
.chat-popup-box .gr-markdown h3,
.chat-popup-box .gr-markdown h4,
.chat-popup-box .gr-markdown h5,
.chat-popup-box .gr-markdown h6,
.chat-popup-box .gr-markdown p {
color: #000000 !important;
}
/* Force component-96 button text to black */
#component-96-button {
color: #000000 !important;
}
/* Force component-93 button text to black */
#component-93-button {
color: #000000 !important;
}
/* Force component-98 button text to black */
#component-98-button {
color: #000000 !important;
}
/* Fix z-index: chat popup should be above how-to-use */
.chat-popup-box {
z-index: 1001 !important;
}
.how-to-use-sidebar {
z-index: 1000 !important;
}
/* Force all tab buttons text to black */
.chat-popup-box button,
.chat-popup-box .gr-button,
button[id*="component-"] {
color: #000000 !important;
}
/* Force typed text in textbox to black */
#component-88 textarea,
#component-88 input {
color: #000000 !important;
}
/* Force Analysis Output label to white */
#component-19 span.svelte-1gfkn6j {
color: #ffffff !important;
}
/* ====================================================================== */
/* FORCE WHITE BORDERS AROUND ALL DROPDOWNS AND INPUTS */
/* ====================================================================== */
/* Target all Gradio dropdowns */
.gradio-dropdown,
label.block:has(select),
label.block:has(.dropdown),
div:has(> select),
div:has(> .dropdown) {
border: 2px solid white !important;
border-radius: 8px !important;
padding: 8px !important;
box-sizing: border-box !important;
}
/* Force white borders on all input containers */
.gr-box,
.gr-input,
.gr-form,
label.block,
.block.svelte-1t38q2d,
.svelte-1t38q2d {
border: 2px solid rgba(255, 255, 255, 0.8) !important;
border-radius: 8px !important;
box-sizing: border-box !important;
}
/* Target dropdown wrappers specifically */
label:has(select),
div:has(select) {
border: 2px solid white !important;
border-radius: 8px !important;
padding: 4px !important;
}
/* Force borders on File upload component */
.gr-file,
.file-preview {
border: 2px solid white !important;
border-radius: 8px !important;
}
/* Target checkbox group */
.gr-checkbox-group,
.gr-checkboxgroup {
border: 2px solid rgba(255, 255, 255, 0.6) !important;
border-radius: 8px !important;
padding: 12px !important;
}
/* Input element styling with white outline */
/* Target all input elements in Gradio */
.gradio-container input[type="text"],
.gradio-container input[type="number"],
.gradio-container input[type="email"],
.gradio-container input[type="password"],
.gradio-container textarea,
input[class*="svelte-"] {
outline: 1px solid white !important;
outline-offset: 10px !important;
border: none !important;
}
/* Also target specific Svelte class if it exists */
input.svelte-1xfxv4t {
margin: var(--spacing-sm);
outline: 1px solid white !important;
outline-offset: 10px;
border: none;
background: inherit;
width: var(--size-full);
color: var(--body-text-color);
font-size: var(--input-text-size);
height: 100%;
}
/* Chat header row with close button */
.chat-header-row {
position: relative !important;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
padding: 0 !important;
margin: 0 !important;
border-radius: 20px 20px 0 0 !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
}
.chat-header-row .chat-header {
flex: 1 !important;
padding: 20px 24px !important;
}
/* Close button styling */
.chat-close-btn,
button.chat-close-btn {
position: absolute !important;
top: 20px !important;
right: 24px !important;
background: rgba(255, 255, 255, 0.2) !important;
border: 2px solid rgba(255, 255, 255, 0.5) !important;
border-radius: 50% !important;
width: 32px !important;
height: 32px !important;
min-width: 32px !important;
min-height: 32px !important;
max-width: 32px !important;
max-height: 32px !important;
color: white !important;
font-size: 20px !important;
font-weight: bold !important;
cursor: pointer !important;
padding: 0 !important;
margin: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
transition: all 0.2s ease !important;
z-index: 10 !important;
}
.chat-close-btn:hover,
button.chat-close-btn:hover {
background: rgba(255, 255, 255, 0.3) !important;
transform: scale(1.1) !important;
border-color: rgba(255, 255, 255, 0.7) !important;
}
/* Add JavaScript to make close button work */
/* Show warning when no dataset */
.no-dataset-warning {
display: block !important;
background: #fff3cd;
border: 2px solid #ffc107;
border-radius: 8px;
padding: 12px;
margin-bottom: 15px;
}
.no-dataset-warning p {
color: #856404 !important;
margin: 0;
}
"""
def enable_chat_features(file):
"""Enable chat features only after dataset upload"""
if file is None:
# Disable submit button and show warning
return gr.update(interactive=False)
else:
# Enable submit button
return gr.update(interactive=True)
def upload_dataset(file):
global uploaded_df, original_df, dataset_name
if file is None:
return "No file uploaded", gr.update(visible=False), gr.update(choices=[]), gr.update(visible=False)
try:
dataset_name = os.path.basename(file.name)
if file.name.endswith('.csv'):
uploaded_df = pd.read_csv(file.name)
elif file.name.endswith(('.xlsx', '.xls')):
uploaded_df = pd.read_excel(file.name)
else:
return "Unsupported file format. Please upload CSV or Excel files.", gr.update(visible=False), gr.update(choices=[]), gr.update(visible=False)
uploaded_df = clean_numeric(uploaded_df)
original_df = uploaded_df.copy()
info_text = f"✓ Dataset Loaded: {dataset_name} ({uploaded_df.shape[0]} rows × {uploaded_df.shape[1]} columns)"
return info_text, gr.update(visible=False), gr.update(choices=list(uploaded_df.columns), value=[]), gr.update(visible=True)
except Exception as e:
return f"Error loading file: {str(e)}", gr.update(visible=False), gr.update(choices=[]), gr.update(visible=False)
def clear_dataset():
global uploaded_df, original_df, dataset_name, change_history
uploaded_df = None
original_df = None
dataset_name = None
change_history = []
return (
"Dataset cleared. Please upload a new file.",
gr.update(visible=False),
gr.update(choices=[], value=[]),
gr.update(visible=False),
gr.update(interactive=False) # Disable submit button
)
def update_preview(format_type, selected_columns):
if uploaded_df is None or format_type == "None":
return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
elif format_type == "DataFrame":
return gr.update(value=display_data_format(format_type, selected_columns, uploaded_df), visible=True), gr.update(visible=False), gr.update(visible=True)
else:
return gr.update(visible=False), gr.update(value=display_text_format(format_type, selected_columns, uploaded_df), visible=True), gr.update(visible=True)
def handle_analysis_change(analysis_type, selected_columns):
if uploaded_df is None or analysis_type == "None":
return gr.update(value="", visible=False), gr.update(visible=False), gr.update(visible=False)
result_text, data_table = run_analysis(analysis_type, selected_columns, uploaded_df)
if result_text and result_text.strip():
if data_table is not None:
return gr.update(value=result_text, visible=True), gr.update(visible=True), gr.update(value=data_table, visible=True)
else:
return gr.update(value=result_text, visible=True), gr.update(visible=True), gr.update(visible=False)
else:
return gr.update(value="", visible=False), gr.update(visible=False), gr.update(visible=False)
def handle_viz_change(viz_type, selected_columns):
if uploaded_df is None or viz_type == "None":
return None, gr.update(visible=False), "", gr.update(visible=False)
result = create_visualization(viz_type, selected_columns, uploaded_df)
if result and len(result) == 3:
fig, explanation, chart_obj = result
if explanation and fig is not None:
return fig, gr.update(visible=True), explanation, gr.update(visible=True)
else:
return None, gr.update(visible=False), explanation or "Error in visualization", gr.update(visible=False)
else:
return None, gr.update(visible=False), "Error in visualization", gr.update(visible=False)
def handle_question_analysis(question):
global uploaded_df, llm
# CRITICAL CHECK #1: No dataset uploaded
if uploaded_df is None or len(uploaded_df) == 0:
error_msg = "⚠️ **NO DATASET LOADED**\n\nPlease upload a CSV or Excel file before asking questions.\n\nSteps:\n1. Click 'DROP YOUR FILE HERE' on the left\n2. Select your dataset\n3. Wait for confirmation\n4. Then ask your question"
return error_msg, None, None
# CRITICAL CHECK #2: Empty question
if not question or question.strip() == "":
return "⚠️ **EMPTY QUESTION**\n\nPlease type a question about your data.", None, None
# CRITICAL CHECK #3: LLM not initialized
if llm is None:
return "⚠️ **AI ERROR**\n\nAI agent not initialized. Please restart the application or check your GROQ_API_KEY.", None, None
# All checks passed - proceed with analysis
return analyze_question(question, [], uploaded_df, llm)
with gr.Blocks() as demo:
# Only show logo if it exists
logo_html = f'' if logo_b64 else ''
gr.HTML(f"""
SparkNova is a data analysis platform that allows users to upload datasets, explore insights, visualize patterns, and ask questions about their data. It simplifies data analytics by automating cleaning, visualization, and intelligent interpretation for quick decision-making.