Skip to content

Commit 56668d2

Browse files
committed
feat(datasets): support json dataset upload via the ui
1 parent ae02d76 commit 56668d2

File tree

2 files changed

+285
-0
lines changed

2 files changed

+285
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import React, { useCallback, useState } from "react";
2+
import { Controller, useForm } from "react-hook-form";
3+
import { css } from "@emotion/react";
4+
5+
import {
6+
Button,
7+
FieldError,
8+
Flex,
9+
Form,
10+
Input,
11+
Label,
12+
Text,
13+
TextArea,
14+
TextField,
15+
View,
16+
} from "@phoenix/components";
17+
import { JSONBlock } from "@phoenix/components/code";
18+
import { prependBasename } from "@phoenix/utils/routingUtils";
19+
20+
type CreateDatasetFromJSONParams = {
21+
file: FileList;
22+
name: string;
23+
description: string;
24+
};
25+
26+
type JsonData = {
27+
name: string;
28+
description: string;
29+
inputs: Record<string, unknown>[];
30+
outputs: Record<string, unknown>[];
31+
metadata: Record<string, unknown>[];
32+
};
33+
34+
export type CreateDatasetFromJSONFormProps = {
35+
onDatasetCreated: (dataset: { id: string; name: string }) => void;
36+
onDatasetCreateError: (error: Error) => void;
37+
};
38+
39+
export function DatasetFromJSONForm(props: CreateDatasetFromJSONFormProps) {
40+
const { onDatasetCreated, onDatasetCreateError } = props;
41+
const [jsonData, setJsonData] = useState<JsonData | null>(null);
42+
const {
43+
control,
44+
handleSubmit,
45+
formState: { isDirty, isValid },
46+
} = useForm<CreateDatasetFromJSONParams>({
47+
defaultValues: {
48+
name: "Dataset " + new Date().toISOString(),
49+
description: "",
50+
file: undefined,
51+
},
52+
});
53+
54+
const onSubmit = useCallback(
55+
(data: CreateDatasetFromJSONParams) => {
56+
if (!jsonData) {
57+
onDatasetCreateError(new Error("No JSON data available"));
58+
return;
59+
}
60+
61+
return fetch(prependBasename("/v1/datasets/upload?sync=true"), {
62+
method: "POST",
63+
headers: {
64+
"Content-Type": "application/json",
65+
},
66+
body: JSON.stringify({
67+
...jsonData,
68+
name: data.name,
69+
description: data.description,
70+
}),
71+
})
72+
.then((response) => {
73+
if (!response.ok) {
74+
throw new Error(response.statusText || "Failed to create dataset");
75+
}
76+
return response.json();
77+
})
78+
.then((res) => {
79+
onDatasetCreated({
80+
name: data.name,
81+
id: res["data"]["dataset_id"],
82+
});
83+
})
84+
.catch((error) => {
85+
onDatasetCreateError(error);
86+
});
87+
},
88+
[onDatasetCreateError, onDatasetCreated, jsonData]
89+
);
90+
91+
return (
92+
<Form onSubmit={handleSubmit(onSubmit)}>
93+
<div
94+
css={css`
95+
padding: var(--ac-global-dimension-size-200);
96+
`}
97+
>
98+
<Controller
99+
name="name"
100+
control={control}
101+
rules={{
102+
required: "field is required",
103+
}}
104+
render={({
105+
field: { onChange, onBlur, value },
106+
fieldState: { invalid, error },
107+
}) => (
108+
<TextField
109+
isInvalid={invalid}
110+
onChange={onChange}
111+
onBlur={onBlur}
112+
value={value.toString()}
113+
>
114+
<Label>Dataset Name</Label>
115+
<Input placeholder="e.x. Golden Dataset" />
116+
{error?.message ? (
117+
<FieldError>{error.message}</FieldError>
118+
) : (
119+
<Text slot="description">The name of the dataset</Text>
120+
)}
121+
</TextField>
122+
)}
123+
/>
124+
<Controller
125+
name="description"
126+
control={control}
127+
render={({
128+
field: { onChange, onBlur, value },
129+
fieldState: { invalid, error },
130+
}) => (
131+
<TextField
132+
isInvalid={invalid}
133+
onChange={onChange}
134+
onBlur={onBlur}
135+
value={value.toString()}
136+
>
137+
<Label>Description</Label>
138+
<TextArea placeholder="e.x. A dataset for structured data extraction" />
139+
{error?.message ? (
140+
<FieldError>{error.message}</FieldError>
141+
) : (
142+
<Text slot="description">The description of the dataset</Text>
143+
)}
144+
</TextField>
145+
)}
146+
/>
147+
<Controller
148+
control={control}
149+
name="file"
150+
rules={{ required: "JSON file is required" }}
151+
render={({
152+
field: { value: _value, onChange, ...field },
153+
fieldState: { invalid, error },
154+
}) => {
155+
return (
156+
<TextField isInvalid={invalid}>
157+
<Label>JSON file</Label>
158+
<input
159+
{...field}
160+
onChange={(event) => {
161+
onChange(event.target.files);
162+
const file = event.target.files?.[0];
163+
if (file) {
164+
const reader = new FileReader();
165+
reader.onload = function (e) {
166+
if (!e.target) {
167+
return;
168+
}
169+
try {
170+
const parsedData = JSON.parse(
171+
e.target.result as string
172+
);
173+
setJsonData(parsedData);
174+
} catch (error) {
175+
onDatasetCreateError(
176+
new Error(
177+
"Invalid JSON file: " + (error as Error).message
178+
)
179+
);
180+
}
181+
};
182+
reader.readAsText(file);
183+
}
184+
}}
185+
type="file"
186+
id="file"
187+
accept=".json"
188+
/>
189+
{error?.message ? (
190+
<FieldError>{error.message}</FieldError>
191+
) : (
192+
<Text slot="description">Upload a JSON file.</Text>
193+
)}
194+
</TextField>
195+
);
196+
}}
197+
/>
198+
<div
199+
css={css`
200+
margin-top: var(--ac-global-dimension-size-200);
201+
`}
202+
>
203+
<Text>Example JSON file:</Text>
204+
<JSONBlock
205+
value={JSON.stringify(
206+
{
207+
inputs: [
208+
{
209+
question: "What is the format of a JSON dataset file?",
210+
},
211+
],
212+
outputs: [
213+
{
214+
answer: "inputs, outputs, and metadata as lists of objects",
215+
},
216+
],
217+
metadata: [
218+
{
219+
hint: "outputs and metadata are optional",
220+
},
221+
],
222+
},
223+
null,
224+
2
225+
)}
226+
/>
227+
</div>
228+
</div>
229+
<View
230+
paddingEnd="size-200"
231+
paddingTop="size-100"
232+
paddingBottom="size-100"
233+
borderTopColor="light"
234+
borderTopWidth="thin"
235+
>
236+
<Flex direction="row" justifyContent="end">
237+
<Button
238+
type="submit"
239+
isDisabled={!isValid || !jsonData}
240+
variant={isDirty ? "primary" : "default"}
241+
size="S"
242+
>
243+
Create Dataset
244+
</Button>
245+
</Flex>
246+
</View>
247+
</Form>
248+
);
249+
}

app/src/pages/datasets/DatasetsPage.tsx

+36
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { getErrorMessagesFromRelayMutationError } from "@phoenix/utils/errorUtil
1111

1212
import { DatasetsPageQuery } from "./__generated__/DatasetsPageQuery.graphql";
1313
import { DatasetFromCSVForm } from "./DatasetFromCSVForm";
14+
import { DatasetFromJSONForm } from "./DatasetFromJSONForm";
1415
import { DatasetsTable } from "./DatasetsTable";
1516

1617
export function DatasetsPage() {
@@ -64,6 +65,7 @@ type CreateDatasetActionMenu = {
6465
enum CreateDatasetAction {
6566
NEW = "newDataset",
6667
FROM_CSV = "datasetFromCSV",
68+
FROM_JSON = "datasetFromJSON",
6769
}
6870

6971
function CreateDatasetActionMenu({
@@ -133,6 +135,36 @@ function CreateDatasetActionMenu({
133135
</Dialog>
134136
);
135137
};
138+
const onCreateDatasetFromJSON = () => {
139+
setDialog(
140+
<Dialog size="M" title="New Dataset from JSON">
141+
<DatasetFromJSONForm
142+
onDatasetCreated={(newDataset) => {
143+
notifySuccess({
144+
title: "Dataset created",
145+
message: `${newDataset.name} has been successfully created.`,
146+
action: {
147+
text: "Go to Dataset",
148+
onClick: () => {
149+
navigate(`/datasets/${newDataset.id}`);
150+
},
151+
},
152+
});
153+
setDialog(null);
154+
onDatasetCreated();
155+
}}
156+
onDatasetCreateError={(error) => {
157+
const formattedError =
158+
getErrorMessagesFromRelayMutationError(error);
159+
notifyError({
160+
title: "Dataset creation failed",
161+
message: formattedError?.[0] ?? error.message,
162+
});
163+
}}
164+
/>
165+
</Dialog>
166+
);
167+
};
136168
return (
137169
<>
138170
<ActionMenu
@@ -147,11 +179,15 @@ function CreateDatasetActionMenu({
147179
case CreateDatasetAction.FROM_CSV:
148180
onCreateDatasetFromCSV();
149181
break;
182+
case CreateDatasetAction.FROM_JSON:
183+
onCreateDatasetFromJSON();
184+
break;
150185
}
151186
}}
152187
>
153188
<Item key={CreateDatasetAction.NEW}>New Dataset</Item>
154189
<Item key={CreateDatasetAction.FROM_CSV}>Dataset from CSV</Item>
190+
<Item key={CreateDatasetAction.FROM_JSON}>Dataset from JSON</Item>
155191
</ActionMenu>
156192
<DialogContainer
157193
type="modal"

0 commit comments

Comments
 (0)