import dash from dash import html, dcc, callback, Output, Input, State import dash_mantine_components as dmc import pandas as pd import os import tempfile from utils import prompt, helpers from gallery_data import GALLERY_DATA # Initialize the Dash app app = dash.Dash(__name__, suppress_callback_exceptions=True) server = app.server # Define the layout matching design.html app.layout = dmc.MantineProvider( html.Div( [ html.Div( [ html.Div(className="ribbon a", **{"aria-hidden": "true"}), html.Div(className="ribbon b", **{"aria-hidden": "true"}), html.Main( [ # Header section html.Header( [ html.Div( [ html.Div(className="mark", **{"aria-hidden": "true"}), html.Div( [ html.H1("ChaRtBot"), html.Div( "AI-assisted data visualization through natural language prompts", className="sub" ) ] ) ], className="brand" ), # Container for tabs and button html.Div( [ html.Button( "New Chart", id="new-chart-button", className="pill", n_clicks=0, style={ "cursor": "pointer", "border": "none", "fontFamily": "inherit" } ), # Tab switcher html.Div( [ html.Button( "CREATE", id="tab-create", className="tab active", type="button", style={ "height": "30px", "padding": "0 16px", }, **{ "role": "tab", "aria-selected": "true" } ), html.Button( "GALLERY", id="tab-gallery", className="tab", type="button", style={ "height": "30px", "padding": "0 16px", }, **{ "role": "tab", "aria-selected": "false" } ) ], className="tabs", role="tablist", **{"aria-label": "ChaRtBot pages"} ) ], style={ "display": "flex", "alignItems": "center", "gap": "12px" } ) ] ), # Visualizer Page (Form section) html.Div( [ # Prompt textarea html.Div( [ dcc.Textarea( id="prompt-textarea", placeholder='Example: "Create a heatmap of weekly sales by month, highlighting high-volume days"', style={ "width": "100%", "height": "120px", "resize": "none", "border": "none", "outline": "none", "background": "transparent", "color": "var(--ink)", "fontSize": "15px", "lineHeight": "1.55" } ) ], className="prompt" ), # How it works section html.Div( [ html.Span("How it works ", style={ "fontSize": "13px", "color": "#667085", "fontWeight": "500" }), html.Span("ⓘ", id="info-icon", style={ "cursor": "pointer", "fontSize": "14px", "color": "#6941C6", "marginLeft": "2px" }) ], style={ "marginTop": "8px", "marginBottom": "16px" } ), # Drawer for How it works dmc.Drawer( title="How it works", id="info-drawer", padding="md", size="400px", position="right", children=[ html.Div([ html.Ol([ html.Li([ html.Strong("Upload and review your data:"), " Choose a CSV file containing your dataset and take a moment to explore it—understand the columns, data types, and any patterns, filters, or transformations relevant to your analysis." ], style={"marginBottom": "16px"}), html.Li([ html.Strong("Describe your analytical goal:"), " Write a natural language prompt that clearly states what you want to analyze and visualize. Reference specific column names and describe how they should be used (e.g., grouping, aggregation, filtering)." ], style={"marginBottom": "16px"}), html.Li([ html.Strong("Refine your prompt if needed:"), " If the output isn’t what you expected or an error occurs, adjust your prompt to be more specific or clarify assumptions. Small changes often lead to better results." ], style={"marginBottom": "16px"}), html.Li([ html.Strong("Review the generated code and chart:"), " Once the visualization is generated, review the underlying code to ensure it accurately reflects your analytical intent before using or sharing the results." ], style={"marginBottom": "16px"}), html.Li([ html.Strong("Share or export your results:"), " Download the visualization as an interactive HTML file to preserve interactivity, or export it as a static image for reports, presentations, or documentation." ], style={"marginBottom": "0px"}), ], style={ "fontSize": "13px", "lineHeight": "1.6", "color": "#344054", "paddingLeft": "20px" }), html.Div([ html.H4("Tips for better results:", style={ "fontSize": "13px", "fontWeight": "600", "color": "#344054", "marginTop": "24px", "marginBottom": "12px" }), html.Ul([ html.Li("Mention specific column names from your dataset"), html.Li("Specify colors, themes, or styling preferences"), html.Li("Include aggregations or transformations you need"), html.Li("Be clear about labels, titles, and legends") ], style={ "fontSize": "12px", "lineHeight": "1.6", "color": "#475467", "paddingLeft": "20px" }) ]) ]) ] ), # File picker and submit button row html.Div( [ html.Div( [ dmc.Tooltip( label="Only CSV files are supported", position="right", withArrow=True, children=[ dcc.Upload( id="upload-data", children=html.Button( "Choose file", className="pickBtn", type="button", style={ "height": "40px", "padding": "0 20px", "minWidth": "120px", "fontFamily": "inherit" } ), accept=".csv", multiple=False ) ] ), html.Div(id="file-name-display", style={"fontSize": "12px", "color": "#475467", "marginTop": "8px"}) ], style={"display": "flex", "alignItems": "center", "gap": "12px"} ), dcc.Loading( id="loading", type="default", children=html.Button( "Visualize", id="submit-button", className="submitBtn", type="button", n_clicks=0, disabled=True, style={ "height": "40px", "padding": "0 20px", "minWidth": "120px", "fontFamily": "inherit" } ) ) ], className="row" ), # Output sections html.A( "Download Chart as HTML", id="download-html", download="chart.html", href="", target="_blank", style={"display": "none", "marginTop": "20px", "textAlign": "right"} ), html.Div(id="dataset-explorer", style={"marginTop": "10px"}), html.Div(id="chartbot-output", style={"marginTop": "10px"}), html.Div(id="python-content-output", style={"marginTop": "10px"}), # Footer section with notes and disclaimers html.Div( [ html.Div( [ html.H3("Notes and Disclaimers", style={ "fontSize": "12px", "fontWeight": "600", "color": "#667085", "marginBottom": "8px", "letterSpacing": "0.02em" }), html.Ul([ html.Li("AI-generated outputs may contain errors. Users should review their data, prompts, and generated code to verify that visualizations accurately reflect their intended analysis. Human oversight is required before use in decision-making."), html.Li("ChaRtBot does not store, log, or retain user-provided datasets, prompts, generated code, or visualization outputs. All processing is performed transiently during the active session.") ], style={ "fontSize": "11px", "color": "#69707D", "lineHeight": "1.6", "margin": "0", "paddingLeft": "16px" }) ], style={"marginBottom": "20px"} ), html.Div( [ html.H3("About this project", style={ "fontSize": "12px", "fontWeight": "600", "color": "#667085", "marginBottom": "8px", "letterSpacing": "0.02em" }), html.P([ "ChaRtBot is a personal project created by Deepa Shalini K to explore AI-assisted data visualization and user-centered analytical workflows." ], style={ "fontSize": "11px", "color": "#69707D", "lineHeight": "1.6", "margin": "0 0 8px 0" }), html.P([ html.Strong("Contact: ", style={"fontWeight": "600"}), html.A("Email", href="mailto:shalini.jul97@gmail.com", target="_blank", style={ "color": "#6941C6", "textDecoration": "none" }), " · ", html.A("LinkedIn", href="https://www.linkedin.com/in/deepa-shalini-273385193/", target="_blank", style={ "color": "#6941C6", "textDecoration": "none" }) ], style={ "fontSize": "11px", "color": "#98A2B3", "lineHeight": "1.6", "margin": "0" }) ] ), html.P("© 2026 Deepa Shalini K. All rights reserved.", style={ "fontSize": "10px", "color": "#434447", "textAlign": "center", "marginTop": "24px", "marginBottom": "0", "paddingTop": "20px", "borderTop": "1px solid #F2F4F7" }) ], style={ "marginTop": "5px", "padding": "10px 0 16px 0" } ), # Hidden stores for data dcc.Store(id="stored-data"), dcc.Store(id="stored-file-name"), dcc.Store(id="html-buffer") ], id="visualizer-page", style={"marginTop": "10px"} ), # Gallery Page html.Div( [ # Gallery page header html.Section( html.Div( [ html.H2("Gallery", style={"margin": "0", "fontSize": "18px", "letterSpacing": "-0.02em"}), html.P( "Browse charts generated by ChaRtBot — each card includes the prompt and the original dataset.", style={"margin": "6px 0 0", "fontSize": "13px", "color": "var(--muted)"} ) ], className="page-title" ), className="page-head" ), # Gallery grid html.Section( [ # Generate cards from GALLERY_DATA *[ html.Article( [ html.Div( [ html.Img( src=item["image"], alt=f"Chart thumbnail {i+1}" ) ], className="thumb" ), html.Div( [ html.P( item["prompt"], className="gallery-prompt" ), html.Div( [ html.Div( [ html.A( "CSV", className="link", href=item["csv_link"], target="_blank" if item["csv_link"] != "#" else "" ), html.Span( item["badge"], className="badge" ) if item.get("badge") else None ], className="links" ) ], className="meta" ) ], className="content" ) ], className="gallery-card" ) for i, item in enumerate(GALLERY_DATA) ] ], className="gallery-grid", **{"aria-label": "Gallery grid"} ), # Copyright footer for gallery page html.Div( html.P("© 2026 Deepa Shalini K. All rights reserved.", style={ "fontSize": "10px", "color": "#434447", "textAlign": "center", "marginTop": "20px", "marginBottom": "0", "paddingTop": "10px", "borderTop": "1px solid #F2F4F7" }), style={ "marginTop": "10px", "padding": "0 0 16px 0" } ) ], id="gallery-page", style={"display": "none"} ) ], className="card", role="main" ) ], className="shell" ) ], className="viewport" ) ) # Add callback for drawer @callback( Output("info-drawer", "opened"), Input("info-icon", "n_clicks"), State("info-drawer", "opened"), prevent_initial_call=True ) def toggle_drawer(n_clicks, opened): """Toggle the info drawer when the info icon is clicked.""" return not opened # Callback for file upload @callback( Output("stored-data", "data"), Output("stored-file-name", "data"), Output("file-name-display", "children"), Output("dataset-explorer", "children"), # Add this output Input("upload-data", "contents"), State("upload-data", "filename") ) def upload_file(contents, filename): """Handle CSV file upload and store the data.""" if contents is None: return None, None, None, None # Add None for dataset-explorer try: # Parse the uploaded file content_type, content_string = contents.split(",") import base64 import io decoded = base64.b64decode(content_string) # Only accept CSV files if not filename.endswith('.csv'): return None, None, html.Div("Only CSV files are allowed", style={"color": "red"}), None # Read CSV file df = pd.read_csv(io.StringIO(decoded.decode('utf-8'))) # Create AG Grid accordion grid_accordion = html.Div([ dmc.Accordion( children=[ dmc.AccordionItem( [ dmc.AccordionControl( html.Div([ html.Span("Explore Dataset", style={"fontWeight": "600", "fontSize": "15px"}), html.Span(f" ({len(df)} rows, {len(df.columns)} columns)", style={"fontSize": "13px", "color": "#667085", "marginLeft": "8px"}) ]) ), dmc.AccordionPanel( helpers.create_ag_grid(df) ) ], value="dataset" ) ], chevronPosition="right", variant="filled", style={ "border": "2px solid #E4E7EC", "borderRadius": "8px", "boxShadow": "0 1px 3px rgba(0, 0, 0, 0.1)" } ) ], style={"marginTop": "20px"}) return df.to_dict("records"), filename, html.Div(f"✓ {filename}", style={"color": "#067A55", "fontWeight": "600"}), grid_accordion except Exception as e: return None, None, html.Div(f"Error: {str(e)}", style={"color": "red"}), None # Callback to enable/disable submit button based on inputs @callback( Output("submit-button", "disabled", allow_duplicate=True), Input("prompt-textarea", "value"), Input("stored-data", "data"), Input("stored-file-name", "data"), prevent_initial_call=True ) def toggle_submit_button(user_prompt, stored_data, filename): """Enable submit button only when both prompt and file are provided.""" # Disable button if prompt is empty or file is not uploaded if not user_prompt or not user_prompt.strip() or not stored_data or not filename: return True return False # Callback for submit button with validation @callback( Output("chartbot-output", "children"), Output("python-content-output", "children"), Output("download-html", "style"), Output("html-buffer", "data"), Output("submit-button", "disabled"), Input("submit-button", "n_clicks"), State("prompt-textarea", "value"), State("stored-data", "data"), State("stored-file-name", "data"), prevent_initial_call=True ) def create_graph(n_clicks, user_prompt, stored_data, filename): """Create visualization based on user prompt and uploaded CSV data.""" if n_clicks == 0: return None, None, {"display": "none"}, None, False try: # Validate inputs if not user_prompt or not user_prompt.strip(): return html.Div([ html.Br(), dmc.Alert("Please enter a prompt for visualization.", title="Missing Prompt", color="red") ]), None, {"display": "none"}, None, False if not stored_data or not filename: return html.Div([ html.Br(), dmc.Alert("Please upload a CSV file before submitting.", title="Missing File", color="red") ]), None, {"display": "none"}, None, False # Convert stored data back to DataFrame df = pd.DataFrame(stored_data) # Save the dataframe temporarily for processing temp_dir = tempfile.gettempdir() temp_file_path = os.path.join(temp_dir, "temp_uploaded_data.csv") df.to_csv(temp_file_path, index=False) # Get first 5 rows as CSV string df_5_rows = df.head(5) data_top5_csv_string = df_5_rows.to_csv(index=False) # Get response from LLM result_output = prompt.get_response(user_prompt, data_top5_csv_string, temp_file_path) # Display the response - returns 5 values graph, code, download_style, html_buffer, _ = helpers.display_response(result_output, temp_file_path) # Extract the Python code from result_output for the accordion import re code_block_match = re.search(r"```(?:[Pp]ython)?(.*?)```", result_output, re.DOTALL) python_code = code_block_match.group(1).strip() if code_block_match else "No code found" # Create accordion with the generated code code_accordion = html.Div([ dmc.Accordion( children=[ dmc.AccordionItem( [ dmc.AccordionControl( html.Div([ html.Span("Code Generated", style={"fontWeight": "600", "fontSize": "15px"}) ]) ), dmc.AccordionPanel( html.Div([ html.Div([ dcc.Clipboard( target_id="code-display", title="Copy code", style={ "position": "absolute", "top": "12px", "right": "12px", "fontSize": "18px", "cursor": "pointer", "padding": "8px", "border": "1px solid #d0d5dd", "borderRadius": "6px", "display": "inline-flex", "alignItems": "center", "justifyContent": "center", "color": "#475467", "transition": "all 0.2s", "zIndex": "10", "width": "32px", "height": "32px" } ) ], style={"position": "relative"}), html.Pre( html.Code( python_code, id="code-display", style={ "fontSize": "13px", "lineHeight": "1.6", "fontFamily": "'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace", "backgroundColor": "#f6f8fa", "padding": "16px", "paddingTop": "48px", "borderRadius": "6px", "display": "block", "overflowX": "auto", "color": "#24292f" } ), style={"margin": "0", "position": "relative"} ) ], style={"position": "relative"}) ) ], value="code" ) ], chevronPosition="right", variant="filled", style={ "border": "2px solid #E4E7EC", "borderRadius": "8px", "boxShadow": "0 1px 3px rgba(0, 0, 0, 0.1)" } ) ]) return graph, code_accordion, {"display": "block", "textAlign": "right", "marginTop": "20px"}, html_buffer, True except Exception as e: error_message = str(e) return html.Div([ html.Br(), dmc.Alert(error_message, title="Error", color="red") ]), None, {"display": "none"}, None, False # Callback for download HTML @callback( Output("download-html", "href"), Input("html-buffer", "data") ) def download_html(encoded): """Generate download link for the chart as HTML.""" if encoded: return f"data:text/html;base64,{encoded}" return "" # Callback for New Chat button to reset everything @callback( Output("prompt-textarea", "value"), Output("stored-data", "data", allow_duplicate=True), Output("stored-file-name", "data", allow_duplicate=True), Output("file-name-display", "children", allow_duplicate=True), Output("dataset-explorer", "children", allow_duplicate=True), # Add this output Output("chartbot-output", "children", allow_duplicate=True), Output("python-content-output", "children", allow_duplicate=True), Output("download-html", "style", allow_duplicate=True), Output("html-buffer", "data", allow_duplicate=True), Output("submit-button", "disabled", allow_duplicate=True), Output("upload-data", "contents"), Input("new-chart-button", "n_clicks"), prevent_initial_call=True ) def reset_chat(n_clicks): """Reset all inputs and outputs to start a new chat.""" if n_clicks > 0: return "", None, None, None, None, None, None, {"display": "none"}, None, True, None # Added None for dataset-explorer return dash.no_update # Callback for tab switching @callback( Output("tab-create", "className"), Output("tab-gallery", "className"), Output("visualizer-page", "style"), Output("gallery-page", "style"), Output("new-chart-button", "style"), Input("tab-create", "n_clicks"), Input("tab-gallery", "n_clicks"), prevent_initial_call=True ) def switch_tabs(visualizer_clicks, gallery_clicks): """Handle tab switching between Visualizer and Gallery.""" ctx = dash.callback_context if not ctx.triggered: return "tab active", "tab", {"marginTop": "10px"}, {"display": "none"}, {"cursor": "pointer", "border": "none", "fontFamily": "inherit"} button_id = ctx.triggered[0]["prop_id"].split(".")[0] if button_id == "tab-create": return "tab active", "tab", {"marginTop": "10px"}, {"display": "none"}, {"cursor": "pointer", "border": "none", "fontFamily": "inherit"} elif button_id == "tab-gallery": return "tab", "tab active", {"display": "none"}, {"marginTop": "10px"}, {"display": "none"} return "tab active", "tab", {"marginTop": "10px"}, {"display": "none"}, {"cursor": "pointer", "border": "none", "fontFamily": "inherit"} if __name__ == "__main__": app.run(debug=False)