Skip to content

feat(datasets): support json dataset upload via the ui #7326

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/src/pages/datasets/DatasetFromCSVForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export function DatasetFromCSVForm(props: CreateDatasetFromCSVFormProps) {
value={value.toString()}
>
<Label>Dataset Name</Label>
<Input placeholder="e.x. Golden Dataset" />
<Input placeholder="e.g., Golden Dataset" />
{error?.message ? (
<FieldError>{error.message}</FieldError>
) : (
Expand All @@ -154,7 +154,7 @@ export function DatasetFromCSVForm(props: CreateDatasetFromCSVFormProps) {
value={value.toString()}
>
<Label>Description</Label>
<TextArea placeholder="e.x. A dataset for structured data extraction" />
<TextArea placeholder="e.g., A dataset for structured data extraction" />
{error?.message ? (
<FieldError>{error.message}</FieldError>
) : (
Expand Down
250 changes: 250 additions & 0 deletions app/src/pages/datasets/DatasetFromJSONForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import React, { useCallback, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { css } from "@emotion/react";

import {
Button,
FieldError,
Flex,
Form,
Input,
Label,
Text,
TextArea,
TextField,
View,
} from "@phoenix/components";
import { JSONBlock } from "@phoenix/components/code";
import { prependBasename } from "@phoenix/utils/routingUtils";

type CreateDatasetFromJSONParams = {
file: FileList;
name: string;
description: string;
};

type JsonData = {
name: string;
description: string;
inputs: Record<string, unknown>[];
outputs: Record<string, unknown>[];
metadata: Record<string, unknown>[];
};

export type CreateDatasetFromJSONFormProps = {
onDatasetCreated: (dataset: { id: string; name: string }) => void;
onDatasetCreateError: (error: Error) => void;
};

export function DatasetFromJSONForm(props: CreateDatasetFromJSONFormProps) {
const { onDatasetCreated, onDatasetCreateError } = props;
const [jsonData, setJsonData] = useState<JsonData | null>(null);
const {
control,
handleSubmit,
formState: { isDirty, isValid },
} = useForm<CreateDatasetFromJSONParams>({
defaultValues: {
name: "Dataset " + new Date().toISOString(),
description: "",
file: undefined,
},
});

const onSubmit = useCallback(
(data: CreateDatasetFromJSONParams) => {
if (!jsonData) {
onDatasetCreateError(new Error("No JSON file has been uploaded"));
return;
}

return fetch(prependBasename("/v1/datasets/upload?sync=true"), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...jsonData,
name: data.name,
description: data.description,
}),
})
.then((response) => {
if (!response.ok) {
return response.text().then((errorText) => {
throw onDatasetCreateError(
new Error(errorText || "Failed to create dataset")
);
});
}
return response.json();
})
.then((res) => {
onDatasetCreated({
name: data.name,
id: res["data"]["dataset_id"],
});
});
},
[onDatasetCreateError, onDatasetCreated, jsonData]
);

return (
<Form onSubmit={handleSubmit(onSubmit)}>
<div
css={css`
padding: var(--ac-global-dimension-size-200);
`}
>
<Controller
name="name"
control={control}
rules={{
required: "field is required",
}}
render={({
field: { onChange, onBlur, value },
fieldState: { invalid, error },
}) => (
<TextField
isInvalid={invalid}
onChange={onChange}
onBlur={onBlur}
value={value.toString()}
>
<Label>Dataset Name</Label>
<Input placeholder="e.g., Golden Dataset" />
{error?.message ? (
<FieldError>{error.message}</FieldError>
) : (
<Text slot="description">The name of the dataset</Text>
)}
</TextField>
)}
/>
<Controller
name="description"
control={control}
render={({
field: { onChange, onBlur, value },
fieldState: { invalid, error },
}) => (
<TextField
isInvalid={invalid}
onChange={onChange}
onBlur={onBlur}
value={value.toString()}
>
<Label>Description</Label>
<TextArea placeholder="e.g., A dataset for structured data extraction" />
{error?.message ? (
<FieldError>{error.message}</FieldError>
) : (
<Text slot="description">The description of the dataset</Text>
)}
</TextField>
)}
/>
<Controller
control={control}
name="file"
rules={{ required: "JSON file is required" }}
render={({
field: { value: _value, onChange, ...field },
fieldState: { invalid, error },
}) => {
return (
<TextField isInvalid={invalid}>
<Label>JSON file</Label>
<input
{...field}
onChange={(event) => {
onChange(event.target.files);
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = function (e) {
if (!e.target) {
return;
}
try {
const parsedData = JSON.parse(
e.target.result as string
);
setJsonData(parsedData);
} catch (error) {
onDatasetCreateError(
new Error(
"Invalid JSON file: " + (error as Error).message
)
);
}
};
reader.readAsText(file);
}
}}
type="file"
id="file"
accept=".json"
/>
{error?.message ? (
<FieldError>{error.message}</FieldError>
) : (
<Text slot="description">Upload a JSON file.</Text>
)}
</TextField>
);
}}
/>
<div
css={css`
margin-top: var(--ac-global-dimension-size-200);
`}
>
<Text>Example JSON file:</Text>
<JSONBlock
value={JSON.stringify(
{
inputs: [
{
question: "What is the format of a JSON dataset file?",
},
],
outputs: [
{
answer: "inputs, outputs, and metadata as lists of objects",
},
],
metadata: [
{
hint: "outputs and metadata are optional",
},
],
},
null,
2
)}
/>
</div>
</div>
<View
paddingEnd="size-200"
paddingTop="size-100"
paddingBottom="size-100"
borderTopColor="light"
borderTopWidth="thin"
>
<Flex direction="row" justifyContent="end">
<Button
type="submit"
isDisabled={!isValid || !jsonData}
variant={isDirty ? "primary" : "default"}
size="S"
>
Create Dataset
</Button>
</Flex>
</View>
</Form>
);
}
36 changes: 36 additions & 0 deletions app/src/pages/datasets/DatasetsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getErrorMessagesFromRelayMutationError } from "@phoenix/utils/errorUtil

import { DatasetsPageQuery } from "./__generated__/DatasetsPageQuery.graphql";
import { DatasetFromCSVForm } from "./DatasetFromCSVForm";
import { DatasetFromJSONForm } from "./DatasetFromJSONForm";
import { DatasetsTable } from "./DatasetsTable";

export function DatasetsPage() {
Expand Down Expand Up @@ -64,6 +65,7 @@ type CreateDatasetActionMenu = {
enum CreateDatasetAction {
NEW = "newDataset",
FROM_CSV = "datasetFromCSV",
FROM_JSON = "datasetFromJSON",
}

function CreateDatasetActionMenu({
Expand Down Expand Up @@ -133,6 +135,36 @@ function CreateDatasetActionMenu({
</Dialog>
);
};
const onCreateDatasetFromJSON = () => {
setDialog(
<Dialog size="M" title="New Dataset from JSON">
<DatasetFromJSONForm
onDatasetCreated={(newDataset) => {
notifySuccess({
title: "Dataset created",
message: `${newDataset.name} has been successfully created.`,
action: {
text: "Go to Dataset",
onClick: () => {
navigate(`/datasets/${newDataset.id}`);
},
},
});
setDialog(null);
onDatasetCreated();
}}
onDatasetCreateError={(error) => {
const formattedError =
getErrorMessagesFromRelayMutationError(error);
notifyError({
title: "Dataset creation failed",
message: formattedError?.[0] ?? error.message,
});
}}
/>
</Dialog>
);
};
return (
<>
<ActionMenu
Expand All @@ -147,11 +179,15 @@ function CreateDatasetActionMenu({
case CreateDatasetAction.FROM_CSV:
onCreateDatasetFromCSV();
break;
case CreateDatasetAction.FROM_JSON:
onCreateDatasetFromJSON();
break;
}
}}
>
<Item key={CreateDatasetAction.NEW}>New Dataset</Item>
<Item key={CreateDatasetAction.FROM_CSV}>Dataset from CSV</Item>
<Item key={CreateDatasetAction.FROM_JSON}>Dataset from JSON</Item>
</ActionMenu>
<DialogContainer
type="modal"
Expand Down