Spaces:
Sleeping
Sleeping
| 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 | |
| def toggle_drawer(n_clicks, opened): | |
| """Toggle the info drawer when the info icon is clicked.""" | |
| return not opened | |
| # Callback for file upload | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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) |