|
|
import base64 |
|
|
import io |
|
|
|
|
|
import chartgpt as cg |
|
|
import dash |
|
|
import dash_ag_grid as dag |
|
|
import dash_mantine_components as dmc |
|
|
import pandas as pd |
|
|
from dash import Input, Output, State, dcc, html, no_update |
|
|
from dash_iconify import DashIconify |
|
|
|
|
|
app = dash.Dash( |
|
|
__name__, |
|
|
external_stylesheets=[ |
|
|
"https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;900&display=swap", |
|
|
], |
|
|
title="ChartGPT", |
|
|
update_title="ChartGPT | Loading...", |
|
|
assets_folder="assets", |
|
|
include_assets_files=True, |
|
|
) |
|
|
server = app.server |
|
|
|
|
|
|
|
|
body = dmc.Stack( |
|
|
[ |
|
|
dmc.Stepper( |
|
|
id="stepper", |
|
|
contentPadding=30, |
|
|
active=0, |
|
|
size="md", |
|
|
breakpoint="sm", |
|
|
children=[ |
|
|
dmc.StepperStep( |
|
|
label="Upload your CSV file", |
|
|
icon=DashIconify(icon="material-symbols:upload"), |
|
|
progressIcon=DashIconify(icon="material-symbols:upload"), |
|
|
completedIcon=DashIconify(icon="material-symbols:upload"), |
|
|
children=[ |
|
|
dmc.Stack( |
|
|
[ |
|
|
dcc.Upload( |
|
|
id="upload-data", |
|
|
children=html.Div( |
|
|
[ |
|
|
"Drag and Drop or", |
|
|
dmc.Button( |
|
|
"Select CSV File", |
|
|
ml=10, |
|
|
leftIcon=DashIconify( |
|
|
icon="material-symbols:upload" |
|
|
), |
|
|
), |
|
|
] |
|
|
), |
|
|
max_size=5 * 1024 * 1024, |
|
|
style={ |
|
|
"borderWidth": "1px", |
|
|
"borderStyle": "dashed", |
|
|
"borderRadius": "5px", |
|
|
"textAlign": "center", |
|
|
"padding": "10px", |
|
|
"backgroundColor": "#fafafa", |
|
|
}, |
|
|
style_reject={ |
|
|
"borderColor": "red", |
|
|
}, |
|
|
multiple=False, |
|
|
), |
|
|
dmc.Title("Preview", order=3, color="primary"), |
|
|
html.Div(id="output-data-upload"), |
|
|
] |
|
|
) |
|
|
], |
|
|
), |
|
|
dmc.StepperStep( |
|
|
label="Plot your data ", |
|
|
icon=DashIconify(icon="bi:bar-chart"), |
|
|
progressIcon=DashIconify(icon="bi:bar-chart"), |
|
|
completedIcon=DashIconify(icon="bi:bar-chart-fill"), |
|
|
children=[ |
|
|
dmc.Stack( |
|
|
[ |
|
|
dmc.Textarea( |
|
|
id="input-text", |
|
|
placeholder="Write here", |
|
|
autosize=True, |
|
|
description="""Type in your questions or requests related to your CSV file. GPT will write the code to visualize the data and find the answers you're looking for.""", |
|
|
maxRows=2, |
|
|
), |
|
|
dmc.Title("Preview", order=3, color="primary"), |
|
|
html.Div(id="output-data-upload-preview"), |
|
|
] |
|
|
) |
|
|
], |
|
|
), |
|
|
dmc.StepperCompleted( |
|
|
children=[ |
|
|
dmc.Stack( |
|
|
[ |
|
|
dmc.Textarea( |
|
|
id="input-text-retry", |
|
|
description="""Type in your questions or requests related to your CSV file. GPT will write the code to visualize the data and find the answers you're looking for.""", |
|
|
placeholder="Write here", |
|
|
autosize=True, |
|
|
icon=DashIconify(icon="material-symbols:search"), |
|
|
maxRows=2, |
|
|
), |
|
|
dmc.LoadingOverlay( |
|
|
id="output-card", |
|
|
mih=300, |
|
|
loaderProps={ |
|
|
"variant": "bars", |
|
|
"color": "primary", |
|
|
"size": "xl", |
|
|
}, |
|
|
), |
|
|
] |
|
|
) |
|
|
] |
|
|
), |
|
|
], |
|
|
), |
|
|
dmc.Group( |
|
|
[ |
|
|
dmc.Button( |
|
|
"Back", |
|
|
id="stepper-back", |
|
|
display="none", |
|
|
size="md", |
|
|
variant="outline", |
|
|
radius="xl", |
|
|
leftIcon=DashIconify(icon="ic:round-arrow-back"), |
|
|
), |
|
|
dmc.Button( |
|
|
"Next", |
|
|
id="stepper-next", |
|
|
size="md", |
|
|
radius="xl", |
|
|
rightIcon=DashIconify( |
|
|
icon="ic:round-arrow-forward", id="icon-next" |
|
|
), |
|
|
), |
|
|
], |
|
|
position="center", |
|
|
mb=20, |
|
|
), |
|
|
] |
|
|
) |
|
|
|
|
|
|
|
|
header = dmc.Center( |
|
|
html.A( |
|
|
dmc.Image( |
|
|
id="logo", |
|
|
src="/assets/logo_light.svg", |
|
|
alt="ChartGPT Logo", |
|
|
width=300, |
|
|
m=20, |
|
|
), |
|
|
href="https://github.com/chatgpt/chart", |
|
|
style={"textDecoration": "none"}, |
|
|
) |
|
|
) |
|
|
|
|
|
theme_toggle = dmc.Switch( |
|
|
id="theme-toggle", |
|
|
size="lg", |
|
|
onLabel="", |
|
|
offLabel="", |
|
|
checked=False, |
|
|
mb=20, |
|
|
style={"position": "absolute", "top": "10px", "right": "10px"}, |
|
|
) |
|
|
|
|
|
|
|
|
socials = dmc.Affix( |
|
|
dmc.Stack( |
|
|
[ |
|
|
dmc.ActionIcon( |
|
|
html.A( |
|
|
DashIconify(icon="mdi:github", width=25), |
|
|
href="https://github.com/chatgpt/chart", |
|
|
style={"color": "black"}, |
|
|
), |
|
|
), |
|
|
dmc.ActionIcon( |
|
|
html.A( |
|
|
DashIconify(icon="mdi:linkedin", width=25), |
|
|
href="https://www.linkedin.com/in/eliebrosset/", |
|
|
style={"color": "#0B65C2"}, |
|
|
), |
|
|
), |
|
|
], |
|
|
spacing="sm", |
|
|
), |
|
|
position={"top": 10, "left": 10}, |
|
|
) |
|
|
|
|
|
|
|
|
def show_graph_card(graph, code): |
|
|
return dmc.Card( |
|
|
dmc.Stack( |
|
|
[ |
|
|
html.Div(graph), |
|
|
dmc.Accordion( |
|
|
variant="separated", |
|
|
chevronPosition="right", |
|
|
radius="md", |
|
|
children=[ |
|
|
dmc.AccordionItem( |
|
|
[ |
|
|
dmc.AccordionControl( |
|
|
"Show code", |
|
|
icon=DashIconify(icon="solar:code-bold"), |
|
|
), |
|
|
dmc.AccordionPanel( |
|
|
dmc.Prism( |
|
|
code, |
|
|
language="python", |
|
|
id="output-code", |
|
|
withLineNumbers=True, |
|
|
), |
|
|
), |
|
|
], |
|
|
value="customization", |
|
|
) |
|
|
], |
|
|
), |
|
|
] |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
page = [ |
|
|
dcc.Store(id="dataset-store", storage_type="local"), |
|
|
dmc.Container( |
|
|
[ |
|
|
theme_toggle, |
|
|
dmc.Stack( |
|
|
[ |
|
|
socials, |
|
|
header, |
|
|
dmc.Alert( |
|
|
"", |
|
|
title="Error", |
|
|
id="alert-error", |
|
|
color="red", |
|
|
withCloseButton=True, |
|
|
hide=True |
|
|
), |
|
|
body, |
|
|
] |
|
|
), |
|
|
] |
|
|
), |
|
|
] |
|
|
|
|
|
|
|
|
CUSTOM_COLORS = { |
|
|
"custom": ["#FFFFFF", "#F2F2F2", "#E5E5E5", "#D9D9D9", "#BFBFBF", "#8C8C8C", "#595959", "#3D3D3D", "#1E1E1E", "#000000"], |
|
|
} |
|
|
|
|
|
LIGHT_THEME = { |
|
|
"colorScheme": "light", |
|
|
"primaryColor": "custom", |
|
|
"colors": CUSTOM_COLORS, |
|
|
"fontFamily": "'Inter', sans-serif", |
|
|
"defaultRadius": "md", |
|
|
"white": "#fff", |
|
|
"black": "#1E1E1E", |
|
|
"primaryShade": 8, |
|
|
} |
|
|
|
|
|
DARK_THEME = { |
|
|
**LIGHT_THEME, |
|
|
"colorScheme": "dark", |
|
|
} |
|
|
|
|
|
app.layout = dmc.MantineProvider( |
|
|
id="mantine-provider", |
|
|
theme=LIGHT_THEME, |
|
|
withGlobalStyles=True, |
|
|
withNormalizeCSS=True, |
|
|
children=page, |
|
|
inherit=True, |
|
|
) |
|
|
|
|
|
|
|
|
def parse_contents(contents, filename): |
|
|
content_type, content_string = contents.split(",") |
|
|
decoded = base64.b64decode(content_string) |
|
|
try: |
|
|
if "csv" in filename: |
|
|
|
|
|
df = pd.read_csv(io.StringIO(decoded.decode("utf-8"))) |
|
|
return df |
|
|
else: |
|
|
return "Invalid file format, please upload a CSV file." |
|
|
except Exception as e: |
|
|
print(e) |
|
|
return "An error occurred while processing the file." |
|
|
|
|
|
|
|
|
@app.callback( |
|
|
Output("dataset-store", "data"), |
|
|
Input("upload-data", "contents"), |
|
|
State("upload-data", "filename"), |
|
|
prevent_initial_call=True, |
|
|
) |
|
|
def store_data(contents, filename): |
|
|
if contents is not None: |
|
|
df = parse_contents(contents, filename) |
|
|
return df.to_json(orient="split") |
|
|
|
|
|
|
|
|
@app.callback( |
|
|
Output("output-data-upload", "children"), |
|
|
Output("output-data-upload-preview", "children"), |
|
|
Output("upload-data", "style"), |
|
|
Output("upload-data", "children"), |
|
|
Input("dataset-store", "data"), |
|
|
) |
|
|
def load_data(dataset): |
|
|
if dataset is not None: |
|
|
df = pd.read_json(io.StringIO(dataset), orient="split") |
|
|
table_preview = dag.AgGrid( |
|
|
id="data-preview", |
|
|
rowData=df.to_dict("records"), |
|
|
style={"height": "275px"}, |
|
|
columnDefs=[{"field": i} for i in df.columns], |
|
|
dashGridOptions={"defaultColDef": {"resizable": True, "sortable": True, "filter": True}}, |
|
|
className="ag-theme-alpine", |
|
|
) |
|
|
return ( |
|
|
table_preview, |
|
|
table_preview, |
|
|
{ |
|
|
"borderWidth": "1px", |
|
|
"borderStyle": "dashed", |
|
|
"borderRadius": "5px", |
|
|
"textAlign": "center", |
|
|
"padding": "7px", |
|
|
"backgroundColor": "#fafafa", |
|
|
}, |
|
|
dmc.Group( |
|
|
[ |
|
|
html.Div( |
|
|
[ |
|
|
"Drag and Drop or", |
|
|
dmc.Button( |
|
|
"Replace file", |
|
|
ml=10, |
|
|
leftIcon=DashIconify(icon="mdi:file-replace"), |
|
|
), |
|
|
] |
|
|
) |
|
|
], |
|
|
position="center", |
|
|
align="center", |
|
|
spacing="xs", |
|
|
), |
|
|
) |
|
|
return no_update |
|
|
|
|
|
|
|
|
@app.callback( |
|
|
Output("stepper", "active"), |
|
|
Input("stepper-next", "n_clicks"), |
|
|
Input("stepper-back", "n_clicks"), |
|
|
State("stepper", "active"), |
|
|
prevent_initial_call=True, |
|
|
) |
|
|
def update_stepper(stepper_next, stepper_back, current): |
|
|
ctx = dash.callback_context |
|
|
id_clicked = ctx.triggered[0]["prop_id"] |
|
|
if id_clicked == "stepper-next.n_clicks" and current < 2: |
|
|
return current + 1 |
|
|
elif id_clicked == "stepper-back.n_clicks": |
|
|
return current - 1 |
|
|
return no_update |
|
|
|
|
|
|
|
|
@app.callback( |
|
|
Output("stepper-next", "disabled"), |
|
|
Output("stepper-back", "disabled"), |
|
|
Output("stepper-next", "display"), |
|
|
Output("stepper-back", "display"), |
|
|
Output("stepper-next", "children"), |
|
|
Output("icon-next", "icon"), |
|
|
Input("stepper", "active"), |
|
|
Input("dataset-store", "data"), |
|
|
) |
|
|
def update_stepper_buttons(current, data): |
|
|
if current == 0 and data is not None: |
|
|
return ( |
|
|
False, |
|
|
False, |
|
|
"block", |
|
|
"block", |
|
|
"Next", |
|
|
"ic:round-arrow-forward", |
|
|
) |
|
|
elif current == 0 and data is None: |
|
|
return ( |
|
|
True, |
|
|
False, |
|
|
"block", |
|
|
"block", |
|
|
"Next", |
|
|
"ic:round-arrow-forward", |
|
|
) |
|
|
elif current == 1: |
|
|
return ( |
|
|
False, |
|
|
False, |
|
|
"block", |
|
|
"block", |
|
|
"Ask ChartGPT", |
|
|
"ph:flask-bold", |
|
|
) |
|
|
elif current == 2: |
|
|
return (False, False, "block", "block", "Ask again", "ic:refresh") |
|
|
|
|
|
|
|
|
@app.callback( |
|
|
[Output("mantine-provider", "theme"), Output("logo", "src"), Output("data-preview", "className")], |
|
|
[Input("theme-toggle", "checked")], |
|
|
) |
|
|
def toggle_theme(is_dark_mode): |
|
|
if is_dark_mode: |
|
|
return DARK_THEME, "/assets/logo_light.svg", "ag-theme-alpine-dark" |
|
|
return LIGHT_THEME, "/assets/logo_dark.svg", "ag-theme-alpine" |
|
|
|
|
|
|
|
|
@app.callback( |
|
|
Output("input-text-retry", "value"), |
|
|
Output("output-card", "children"), |
|
|
Output("alert-error", "hide"), |
|
|
Output("alert-error", "children"), |
|
|
Input("stepper-next", "n_clicks"), |
|
|
State("stepper", "active"), |
|
|
State("dataset-store", "data"), |
|
|
State("input-text", "value"), |
|
|
State("input-text-retry", "value"), |
|
|
prevent_initial_call=True, |
|
|
) |
|
|
def update_graph(n_clicks, active, df, prompt, prompt_retry): |
|
|
if n_clicks is not None and active == 1: |
|
|
try: |
|
|
return prompt, predict(df, prompt), True, "" |
|
|
except Exception as e: |
|
|
return no_update, no_update, False, str(e) |
|
|
elif n_clicks is not None and active == 2: |
|
|
try: |
|
|
return prompt_retry, predict(df, prompt_retry), True, "" |
|
|
except Exception as e: |
|
|
return no_update, no_update, False, str(e) |
|
|
return no_update |
|
|
|
|
|
|
|
|
def predict(df, prompt): |
|
|
df = pd.read_json(df, orient="split") |
|
|
chart = cg.Chart(df, model="huggingface/Qwen/Qwen2.5-Coder-32B-Instruct") |
|
|
fig = chart.plot(prompt, return_fig=True) |
|
|
output = show_graph_card(graph=dcc.Graph(figure=fig), code=chart.last_run_code) |
|
|
return output |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
app.run_server() |
|
|
|