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"""
{logo_html}

SparkNova

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.

""") gr.HTML(""" """) with gr.Row(): with gr.Column(scale=1): # How to Use Box at the top gr.HTML("""

How to Use

""") gr.Markdown("### Upload Dataset") file_input = gr.File(label="DROP YOUR FILE HERE", file_types=[".csv", ".xlsx", ".xls"]) dataset_info = gr.Markdown() with gr.Row(): clear_btn = gr.Button("Clear Dataset", variant="secondary", size="sm") format_selector = gr.Dropdown( choices=["None", "DataFrame", "JSON", "Dictionary"], value="None", label="Display Format" ) column_selector = gr.CheckboxGroup( label="Select Columns", choices=[], visible=False ) gr.Markdown("### Choose an Analysis Type") analysis_selector = gr.Dropdown( choices=["None", "Summary", "Describe", "Top 5 Rows", "Bottom 5 Rows", "Missing Values", "Group & Aggregate", "Calculate Expressions", "Highest Correlation"], value="None", label="Analysis Type" ) gr.Markdown("### Visualization Types") viz_selector = gr.Dropdown( choices=["None", "Bar Chart", "Line Chart", "Scatter Plot", "Pie Chart", "Histogram", "Box Plot", "Heat Map"], value="None", label="Chart Type" ) with gr.Column(scale=2): preview_heading = gr.Markdown("### Dataset Preview", visible=False) dataset_preview = gr.Dataframe(wrap=True, visible=False) text_preview = gr.Textbox(label="Text Preview", lines=15, visible=False) analysis_heading = gr.Markdown("### Analysis Results", visible=False) analysis_output = gr.Textbox(label="Analysis Output", lines=10, visible=False, interactive=False) analysis_data_table = gr.Dataframe(label="Data Table", wrap=True, visible=False) chart_output_new = gr.Plot(label="Chart", visible=False) chart_explanation = gr.Textbox(label="Chart Analysis", lines=5, visible=False, interactive=False) # Floating Chat Button chat_toggle_btn = gr.Button("💬", elem_classes=["floating-chat-btn"], size="sm") with gr.Column(visible=False, elem_classes=["chat-popup-box"]) as chat_popup: with gr.Row(elem_classes=["chat-header-row"]): gr.HTML("

Ask SparkNova

") close_chat_btn = gr.Button("✕", elem_classes=["chat-close-btn"], size="sm") gr.Markdown("#### Sample Questions", elem_classes=["sample-header"]) for i in range(min(5, len(SAMPLE_QUESTIONS))): gr.Markdown(f"• {SAMPLE_QUESTIONS[i]}", elem_classes=["sample-q-text"]) gr.Markdown("#### Enter Your Question") user_question = gr.Textbox( label="Your Question", placeholder="Type your question here...", lines=2, max_lines=3, elem_classes=["chat-question-input"], interactive=True, show_label=True ) submit_btn = gr.Button("Analyze", variant="primary", size="lg", elem_classes=["chat-submitbtn"], interactive=False) gr.HTML("

Analysis Results

") with gr.Tabs(): with gr.Tab("Response"): output_text = gr.Textbox(label="", interactive=False, lines=10) with gr.Tab("Visualization"): chart_output = gr.Plot(label="") with gr.Tab("Data"): result_table = gr.Dataframe(label="", wrap=True) chat_visible = [False] def toggle_chat(): chat_visible[0] = not chat_visible[0] return gr.update(visible=chat_visible[0]) def close_chat(): chat_visible[0] = False return gr.update(visible=False) chat_toggle_btn.click(toggle_chat, inputs=None, outputs=chat_popup) close_chat_btn.click(close_chat, inputs=None, outputs=chat_popup) file_input.change( upload_dataset, inputs=file_input, outputs=[dataset_info, dataset_preview, column_selector, column_selector] ).then( enable_chat_features, inputs=file_input, outputs=submit_btn ) clear_btn.click( clear_dataset, outputs=[dataset_info, dataset_preview, column_selector, column_selector, submit_btn] ) format_selector.change(update_preview, inputs=[format_selector, column_selector], outputs=[dataset_preview, text_preview, preview_heading]) column_selector.change(update_preview, inputs=[format_selector, column_selector], outputs=[dataset_preview, text_preview, preview_heading]) analysis_selector.change(handle_analysis_change, inputs=[analysis_selector, column_selector], outputs=[analysis_output, analysis_heading, analysis_data_table]) column_selector.change(handle_analysis_change, inputs=[analysis_selector, column_selector], outputs=[analysis_output, analysis_heading, analysis_data_table]) viz_selector.change(handle_viz_change, inputs=[viz_selector, column_selector], outputs=[chart_output_new, chart_output_new, chart_explanation, chart_explanation]) column_selector.change(handle_viz_change, inputs=[viz_selector, column_selector], outputs=[chart_output_new, chart_output_new, chart_explanation, chart_explanation]) submit_btn.click(handle_question_analysis, inputs=[user_question], outputs=[output_text, chart_output, result_table]) gr.HTML("
Powered by GROQ LLM & Gradio
") if __name__ == "__main__": llm = initialize_llm() if not llm: print("Warning: Failed to initialize GROQ API") demo.launch(show_error=True, share=False, theme=gr.themes.Soft(), css=custom_css)