Elie Brosset commited on
Commit
4ac77b1
·
0 Parent(s):

First commit

Browse files
Files changed (5) hide show
  1. .gitignore +2 -0
  2. Dockerfile +7 -0
  3. app.py +482 -0
  4. assets/custom.css +3 -0
  5. requirements.txt +2 -0
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ venv/
2
+ .vscode/
Dockerfile ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+ RUN pip install --upgrade pip
5
+ COPY . .
6
+ RUN pip install -r requirements.txt
7
+ EXPOSE 5000
app.py ADDED
@@ -0,0 +1,482 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import io
3
+
4
+ import chartgpt as cg
5
+ import dash
6
+ import dash_ag_grid as dag
7
+ import dash_mantine_components as dmc
8
+ import pandas as pd
9
+ from dash import Input, Output, State, dcc, html, no_update
10
+ from dash_iconify import DashIconify
11
+
12
+ app = dash.Dash(
13
+ __name__,
14
+ external_stylesheets=[
15
+ "https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;900&display=swap",
16
+ ],
17
+ title="ChartGPT",
18
+ update_title="ChartGPT | Loading...",
19
+ assets_folder="assets",
20
+ include_assets_files=True,
21
+ )
22
+ application = app.server
23
+
24
+
25
+ body = dmc.Stack(
26
+ [
27
+ dmc.Stepper(
28
+ id="stepper",
29
+ contentPadding=30,
30
+ active=0,
31
+ size="md",
32
+ breakpoint="sm",
33
+ children=[
34
+ dmc.StepperStep(
35
+ label="Add your OpenAI API key",
36
+ icon=DashIconify(
37
+ icon="material-symbols:lock",
38
+ ),
39
+ progressIcon=DashIconify(
40
+ icon="material-symbols:lock",
41
+ ),
42
+ completedIcon=DashIconify(
43
+ icon="material-symbols:lock-open",
44
+ ),
45
+ children=[
46
+ dmc.Stack(
47
+ [
48
+ dmc.Stack(
49
+ [
50
+ dmc.Blockquote(
51
+ """Welcome to ChartGPT! To get started, fetch your OpenAI API key and paste it below.\
52
+ Then, upload your CSV file and ask ChartGPT to plot your data. Happy charting 🥳""",
53
+ icon=DashIconify(
54
+ icon="line-md:coffee-half-empty-twotone-loop"
55
+ ),
56
+ ),
57
+ dmc.Center(
58
+ dmc.Button(
59
+ dmc.Anchor(
60
+ "Get your API key",
61
+ href="https://platform.openai.com/account/api-keys",
62
+ target="_blank",
63
+ style={
64
+ "textDecoration": "none",
65
+ "color": "white",
66
+ },
67
+ ),
68
+ fullWidth=False,
69
+ rightIcon=DashIconify(
70
+ icon="material-symbols:open-in-new"
71
+ ),
72
+ ),
73
+ ),
74
+ ],
75
+ ),
76
+ dmc.PasswordInput(
77
+ id="input-api-key",
78
+ label="API Key",
79
+ description="Please add your OpenAI API key. It will be used to generate your visualization",
80
+ placeholder="Your OpenAI API Key",
81
+ icon=DashIconify(icon="material-symbols:key"),
82
+ size="sm",
83
+ required=True,
84
+ ),
85
+ ]
86
+ )
87
+ ],
88
+ ),
89
+ dmc.StepperStep(
90
+ label="Upload your CSV file",
91
+ icon=DashIconify(icon="material-symbols:upload"),
92
+ progressIcon=DashIconify(icon="material-symbols:upload"),
93
+ completedIcon=DashIconify(icon="material-symbols:upload"),
94
+ children=[
95
+ dmc.Stack(
96
+ [
97
+ dcc.Upload(
98
+ id="upload-data",
99
+ children=html.Div(
100
+ [
101
+ "Drag and Drop or",
102
+ dmc.Button(
103
+ "Select CSV File",
104
+ ml=10,
105
+ leftIcon=DashIconify(
106
+ icon="material-symbols:upload"
107
+ ),
108
+ ),
109
+ ]
110
+ ),
111
+ max_size=5 * 1024 * 1024, # 5MB
112
+ style={
113
+ "borderWidth": "1px",
114
+ "borderStyle": "dashed",
115
+ "borderRadius": "5px",
116
+ "textAlign": "center",
117
+ "padding": "10px",
118
+ "backgroundColor": "#fafafa",
119
+ },
120
+ style_reject={
121
+ "borderColor": "red",
122
+ },
123
+ multiple=False,
124
+ ),
125
+ dmc.Title("Preview", order=3, color="primary"),
126
+ html.Div(id="output-data-upload"),
127
+ ]
128
+ )
129
+ ],
130
+ ),
131
+ dmc.StepperStep(
132
+ label="Plot your data 🚀",
133
+ icon=DashIconify(icon="bi:bar-chart"),
134
+ progressIcon=DashIconify(icon="bi:bar-chart"),
135
+ completedIcon=DashIconify(icon="bi:bar-chart-fill"),
136
+ children=[
137
+ dmc.Stack(
138
+ [
139
+ dmc.Textarea(
140
+ id="input-text",
141
+ placeholder="Write here",
142
+ autosize=True,
143
+ 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.""",
144
+ maxRows=2,
145
+ ),
146
+ dmc.Title("Preview", order=3, color="primary"),
147
+ html.Div(id="output-data-upload-preview"),
148
+ ]
149
+ )
150
+ ],
151
+ ),
152
+ dmc.StepperCompleted(
153
+ children=[
154
+ dmc.Stack(
155
+ [
156
+ dmc.Textarea(
157
+ id="input-text-retry",
158
+ 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.""",
159
+ placeholder="Write here",
160
+ autosize=True,
161
+ icon=DashIconify(icon="material-symbols:search"),
162
+ maxRows=2,
163
+ ),
164
+ dmc.LoadingOverlay(
165
+ id="output-card",
166
+ mih=300,
167
+ loaderProps={
168
+ "variant": "bars",
169
+ "color": "primary",
170
+ "size": "xl",
171
+ },
172
+ ),
173
+ ]
174
+ )
175
+ ]
176
+ ),
177
+ ],
178
+ ),
179
+ dmc.Group(
180
+ [
181
+ dmc.Button(
182
+ "Back",
183
+ id="stepper-back",
184
+ display="none",
185
+ size="md",
186
+ variant="outline",
187
+ radius="xl",
188
+ leftIcon=DashIconify(icon="ic:round-arrow-back"),
189
+ ),
190
+ dmc.Button(
191
+ "Next",
192
+ id="stepper-next",
193
+ size="md",
194
+ radius="xl",
195
+ rightIcon=DashIconify(
196
+ icon="ic:round-arrow-forward", id="icon-next"
197
+ ),
198
+ ),
199
+ ],
200
+ position="center",
201
+ mb=20,
202
+ ),
203
+ ]
204
+ )
205
+
206
+
207
+ header = dmc.Center(
208
+ html.A(
209
+ dmc.Image(
210
+ src="https://raw.githubusercontent.com/chatgpt/chart/9ff8b9b96f01a5ee7091ee5e69a2795381bf5031/docs/assets/chartgpt_logo.svg",
211
+ alt="ChartGPT Logo",
212
+ width=300,
213
+ m=20,
214
+ caption="Plot your data using GPT",
215
+ ),
216
+ href="https://github.com/chatgpt/chart",
217
+ style={"textDecoration": "none"},
218
+ )
219
+ )
220
+
221
+ socials = dmc.Affix(
222
+ dmc.Stack(
223
+ [
224
+ dmc.ActionIcon(
225
+ html.A(
226
+ DashIconify(icon="mdi:github", width=25),
227
+ href="https://github.com/youplala/chartgpt",
228
+ style={"color": "black"},
229
+ ),
230
+ ),
231
+ dmc.ActionIcon(
232
+ html.A(
233
+ DashIconify(icon="mdi:linkedin", width=25),
234
+ href="https://www.linkedin.com/in/eliebrosset/",
235
+ style={"color": "#0B65C2"},
236
+ ),
237
+ ),
238
+ ],
239
+ spacing="sm",
240
+ mt=5,
241
+ mr=5,
242
+ ),
243
+ position={"top": 10, "left": 10},
244
+ )
245
+
246
+
247
+ def show_graph_card(graph, code):
248
+ return dmc.Card(
249
+ dmc.Stack(
250
+ [
251
+ html.Div(graph),
252
+ dmc.Accordion(
253
+ variant="separated",
254
+ chevronPosition="right",
255
+ radius="md",
256
+ children=[
257
+ dmc.AccordionItem(
258
+ [
259
+ dmc.AccordionControl(
260
+ "Show code",
261
+ icon=DashIconify(icon="solar:code-bold"),
262
+ ),
263
+ dmc.AccordionPanel(
264
+ dmc.Prism(
265
+ code,
266
+ language="python",
267
+ id="output-code",
268
+ withLineNumbers=True,
269
+ ),
270
+ ),
271
+ ],
272
+ value="customization",
273
+ )
274
+ ],
275
+ ),
276
+ ]
277
+ )
278
+ )
279
+
280
+
281
+ page = [
282
+ dcc.Store(id="dataset-store", storage_type="local"),
283
+ dmc.Container(
284
+ [
285
+ dmc.Stack(
286
+ [
287
+ socials,
288
+ header,
289
+ body,
290
+ ]
291
+ ),
292
+ ]
293
+ ),
294
+ ]
295
+
296
+ app.layout = dmc.MantineProvider(
297
+ id="mantine-provider",
298
+ theme={
299
+ "fontFamily": "'Inter', sans-serif",
300
+ "colorScheme": "light",
301
+ "primaryColor": "dark",
302
+ "defaultRadius": "md",
303
+ "white": "#fff",
304
+ "black": "#404040",
305
+ },
306
+ withGlobalStyles=True,
307
+ withNormalizeCSS=True,
308
+ children=page,
309
+ inherit=True,
310
+ )
311
+
312
+
313
+ def parse_contents(contents, filename):
314
+ content_type, content_string = contents.split(",")
315
+ decoded = base64.b64decode(content_string)
316
+ try:
317
+ if "csv" in filename:
318
+ # Assuming the uploaded file is a CSV, parse it
319
+ df = pd.read_csv(io.StringIO(decoded.decode("utf-8")))
320
+ return df
321
+ else:
322
+ return "Invalid file format, please upload a CSV file."
323
+ except Exception as e:
324
+ print(e)
325
+ return "An error occurred while processing the file."
326
+
327
+
328
+ @app.callback(
329
+ Output("dataset-store", "data"),
330
+ Input("upload-data", "contents"),
331
+ State("upload-data", "filename"),
332
+ prevent_initial_call=True,
333
+ )
334
+ def store_data(contents, filename):
335
+ if contents is not None:
336
+ df = parse_contents(contents, filename)
337
+ return df.to_json(orient="split")
338
+
339
+
340
+ @app.callback(
341
+ Output("output-data-upload", "children"),
342
+ Output("output-data-upload-preview", "children"),
343
+ Output("upload-data", "style"),
344
+ Output("upload-data", "children"),
345
+ Input("dataset-store", "data"),
346
+ )
347
+ def load_data(dataset):
348
+ if dataset is not None:
349
+ df = pd.read_json(dataset, orient="split")
350
+ table_preview = dag.AgGrid(
351
+ id="data-preview",
352
+ rowData=df.to_dict("records"),
353
+ style={"height": "275px"},
354
+ columnDefs=[{"field": i} for i in df.columns],
355
+ )
356
+ return (
357
+ table_preview,
358
+ table_preview,
359
+ {
360
+ "borderWidth": "1px",
361
+ "borderStyle": "dashed",
362
+ "borderRadius": "5px",
363
+ "textAlign": "center",
364
+ "padding": "7px",
365
+ "backgroundColor": "#fafafa",
366
+ },
367
+ dmc.Group(
368
+ [
369
+ html.Div(
370
+ [
371
+ "Drag and Drop or",
372
+ dmc.Button(
373
+ "Replace file",
374
+ ml=10,
375
+ leftIcon=DashIconify(icon="mdi:file-replace"),
376
+ ),
377
+ ]
378
+ )
379
+ ],
380
+ position="center",
381
+ align="center",
382
+ spacing="xs",
383
+ ),
384
+ )
385
+ return no_update
386
+
387
+
388
+ @app.callback(
389
+ Output("stepper", "active"),
390
+ Input("stepper-next", "n_clicks"),
391
+ Input("stepper-back", "n_clicks"),
392
+ State("stepper", "active"),
393
+ prevent_initial_call=True,
394
+ )
395
+ def update_stepper(stepper_next, stepper_back, current):
396
+ ctx = dash.callback_context
397
+ id_clicked = ctx.triggered[0]["prop_id"]
398
+ if id_clicked == "stepper-next.n_clicks" and current < 3:
399
+ return current + 1
400
+ elif id_clicked == "stepper-back.n_clicks":
401
+ return current - 1
402
+ return no_update
403
+
404
+
405
+ @app.callback(
406
+ Output("stepper-next", "disabled"),
407
+ Output("stepper-back", "disabled"),
408
+ Output("stepper-next", "display"),
409
+ Output("stepper-back", "display"),
410
+ Output("stepper-next", "children"),
411
+ Output("icon-next", "icon"),
412
+ Input("stepper", "active"),
413
+ Input("input-api-key", "value"),
414
+ Input("dataset-store", "data"),
415
+ )
416
+ def update_stepper_buttons(current, api_key, data):
417
+ if current == 0 and api_key != "":
418
+ return False, True, "block", "none", "Next", "ic:round-arrow-forward"
419
+ elif current == 0 and api_key == "":
420
+ return True, True, "block", "none", "Next", "ic:round-arrow-forward"
421
+ elif current == 1 and data is not None:
422
+ return (
423
+ False,
424
+ False,
425
+ "block",
426
+ "block",
427
+ "Next",
428
+ "ic:round-arrow-forward",
429
+ )
430
+ elif current == 1 and data is None:
431
+ return (
432
+ True,
433
+ False,
434
+ "block",
435
+ "block",
436
+ "Next",
437
+ "ic:round-arrow-forward",
438
+ )
439
+ elif current == 2:
440
+ return (
441
+ False,
442
+ False,
443
+ "block",
444
+ "block",
445
+ "Ask ChartGPT",
446
+ "ph:flask-bold",
447
+ )
448
+ elif current == 3:
449
+ return (False, False, "block", "block", "Ask again", "ic:refresh")
450
+
451
+
452
+ @app.callback(
453
+ Output("input-text-retry", "value"),
454
+ Output("output-card", "children"),
455
+ Input("stepper-next", "n_clicks"),
456
+ State("stepper", "active"),
457
+ State("input-api-key", "value"),
458
+ State("dataset-store", "data"),
459
+ State("input-text", "value"),
460
+ State("input-text-retry", "value"),
461
+ prevent_initial_call=True,
462
+ )
463
+ def update_graph(n_clicks, active, api_key, df, prompt, prompt_retry):
464
+ if n_clicks is not None and active == 2:
465
+ output = predict(api_key, df, prompt)
466
+ return prompt, output
467
+ elif n_clicks is not None and active == 3:
468
+ output = predict(api_key, df, prompt_retry)
469
+ return prompt_retry, output
470
+ return no_update
471
+
472
+
473
+ def predict(api_key, df, prompt):
474
+ df = pd.read_json(df, orient="split")
475
+ chart = cg.Chart(df, api_key=api_key)
476
+ fig = chart.plot(prompt, return_fig=True)
477
+ output = show_graph_card(graph=dcc.Graph(figure=fig), code=chart.last_run_code)
478
+ return output
479
+
480
+
481
+ if __name__ == "__main__":
482
+ app.run_server(debug=True)
assets/custom.css ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ input:invalid {
2
+ outline: none !important;
3
+ }
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ chartgpt>=0.0.5
2
+ dash-ag-grid>=2.2.0