DatasetsAccordion.tsx•11.8 kB
"use client";
import { ChangeEvent, useCallback, useEffect, useState } from "react";
import { useBoolean } from "@/utils";
import { Accordion, CTAButton, GhostButton, IconButton, Input, Modal, PopupMenu } from "@/ui/elements";
import { AccordionProps } from "@/ui/elements/Accordion";
import { CloseIcon, DatasetIcon, MinusIcon, PlusIcon } from "@/ui/Icons";
import useDatasets, { Dataset } from "@/modules/ingestion/useDatasets";
import addData from "@/modules/ingestion/addData";
import cognifyDataset from "@/modules/datasets/cognifyDataset";
import { DataFile } from "@/modules/ingestion/useData";
import { LoadingIndicator } from "@/ui/App";
interface DatasetsChangePayload {
datasets: Dataset[]
refreshDatasets: () => void;
}
export interface DatasetsAccordionProps extends Omit<AccordionProps, "isOpen" | "openAccordion" | "closeAccordion" | "children"> {
onDatasetsChange?: (payload: DatasetsChangePayload) => void;
useCloud?: boolean;
}
export default function DatasetsAccordion({
title,
tools,
switchCaretPosition = false,
className,
contentClassName,
onDatasetsChange,
useCloud = false,
}: DatasetsAccordionProps) {
const {
value: isDatasetsPanelOpen,
setTrue: openDatasetsPanel,
setFalse: closeDatasetsPanel,
} = useBoolean(true);
const {
datasets,
refreshDatasets,
addDataset,
removeDataset,
getDatasetData,
removeDatasetData,
} = useDatasets(useCloud);
useEffect(() => {
if (datasets.length === 0) {
refreshDatasets();
}
}, [datasets.length, refreshDatasets]);
const [openDatasets, openDataset] = useState<Set<string>>(new Set());
const toggleDataset = (id: string) => {
openDataset((prev) => {
const newState = new Set(prev);
if (newState.has(id)) {
newState.delete(id)
} else {
getDatasetData(id)
.then(() => {
newState.add(id);
});
}
return newState;
});
};
const refreshOpenDatasetsData = useCallback(() => {
return Promise.all(
openDatasets.values().map(
(datasetId) => getDatasetData(datasetId)
)
);
}, [getDatasetData, openDatasets]);
const refreshDatasetsAndData = useCallback(() => {
refreshDatasets()
.then(refreshOpenDatasetsData);
}, [refreshDatasets, refreshOpenDatasetsData]);
useEffect(() => {
onDatasetsChange?.({
datasets,
refreshDatasets: refreshDatasetsAndData,
});
}, [datasets, onDatasetsChange, refreshDatasets, refreshDatasetsAndData]);
const {
value: isNewDatasetModalOpen,
setTrue: openNewDatasetModal,
setFalse: closeNewDatasetModal,
} = useBoolean(false);
const handleDatasetAdd = () => {
openNewDatasetModal();
};
const [newDatasetError, setNewDatasetError] = useState("");
const handleNewDatasetSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setNewDatasetError("");
const formElements = event.currentTarget;
const datasetName = formElements.datasetName.value;
if (datasetName.trim().length === 0) {
setNewDatasetError("Dataset name cannot be empty.");
return;
}
if (datasetName.includes(" ") || datasetName.includes(".")) {
setNewDatasetError("Dataset name cannot contain spaces or periods.");
return;
}
addDataset(datasetName)
.then(() => {
closeNewDatasetModal();
refreshDatasetsAndData();
});
};
const {
value: isRemoveDatasetModalOpen,
setTrue: openRemoveDatasetModal,
setFalse: closeRemoveDatasetModal,
} = useBoolean(false);
const [datasetToRemove, setDatasetToRemove] = useState<Dataset | null>(null);
const handleDatasetRemove = (dataset: Dataset) => {
setDatasetToRemove(dataset);
openRemoveDatasetModal();
};
const handleDatasetRemoveCancel = () => {
setDatasetToRemove(null);
closeRemoveDatasetModal();
};
const handleRemoveDatasetConfirm = (event: React.FormEvent<HTMLButtonElement>) => {
event.preventDefault();
if (datasetToRemove) {
removeDataset(datasetToRemove.id)
.then(() => {
closeRemoveDatasetModal();
setDatasetToRemove(null);
refreshDatasetsAndData();
});
}
};
const [datasetInProcessing, setProcessingDataset] = useState<Dataset | null>(null);
const handleAddFiles = (dataset: Dataset, event: ChangeEvent<HTMLInputElement>) => {
event.stopPropagation();
if (datasetInProcessing) {
return;
}
setProcessingDataset(dataset);
if (!event.target.files) {
return;
}
const files: File[] = Array.from(event.target.files);
if (!files.length) {
return;
}
return addData(dataset, files, useCloud)
.then(async () => {
await getDatasetData(dataset.id);
return cognifyDataset(dataset, useCloud)
.finally(() => {
setProcessingDataset(null);
});
});
};
const [dataToRemove, setDataToRemove] = useState<DataFile | null>(null);
const {
value: isRemoveDataModalOpen,
setTrue: openRemoveDataModal,
setFalse: closeRemoveDataModal,
} = useBoolean(false);
const handleDataRemove = (data: DataFile) => {
setDataToRemove(data);
openRemoveDataModal();
};
const handleDataRemoveCancel = () => {
setDataToRemove(null);
closeRemoveDataModal();
};
const handleDataRemoveConfirm = (event: React.FormEvent<HTMLButtonElement>) => {
event.preventDefault();
if (dataToRemove) {
removeDatasetData(dataToRemove.datasetId, dataToRemove.id)
.then(() => {
closeRemoveDataModal();
setDataToRemove(null);
refreshDatasetsAndData();
});
}
}
return (
<>
<Accordion
title={title || <span>Datasets</span>}
isOpen={isDatasetsPanelOpen}
openAccordion={openDatasetsPanel}
closeAccordion={closeDatasetsPanel}
tools={(
<div className="flex flex-row gap-4 items-center">
{tools}
<IconButton onClick={handleDatasetAdd}><PlusIcon /></IconButton>
</div>
)}
switchCaretPosition={switchCaretPosition}
className={className}
contentClassName={contentClassName}
>
<div className="flex flex-col">
{datasets.length === 0 && (
<div className="flex flex-row items-baseline-last text-sm text-gray-400 mt-2 px-2">
<span>No datasets here, add one by clicking +</span>
</div>
)}
{datasets.map((dataset) => {
return (
<Accordion
key={dataset.id}
title={(
<div className="flex flex-row gap-2 items-center py-1.5 cursor-pointer">
{datasetInProcessing?.id == dataset.id ? <LoadingIndicator /> : <DatasetIcon />}
<span className="text-xs">{dataset.name}</span>
</div>
)}
isOpen={openDatasets.has(dataset.id)}
openAccordion={() => toggleDataset(dataset.id)}
closeAccordion={() => toggleDataset(dataset.id)}
tools={(
<IconButton className="relative">
<PopupMenu>
<div className="flex flex-col gap-0.5">
<div className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer relative">
<input tabIndex={-1} type="file" multiple onChange={handleAddFiles.bind(null, dataset)} className="absolute w-full h-full cursor-pointer opacity-0" />
<span>add data</span>
</div>
</div>
<div className="flex flex-col gap-0.5 items-start">
<div onClick={() => handleDatasetRemove(dataset)} className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer">delete</div>
</div>
</PopupMenu>
</IconButton>
)}
className="first:pt-1.5"
switchCaretPosition={true}
>
<>
{dataset.data?.length === 0 && (
<div className="flex flex-row items-baseline-last text-sm text-gray-400 mt-2 px-2">
<span>No data in a dataset, add by clicking "add data" in a dropdown menu</span>
</div>
)}
{dataset.data?.map((data) => (
<div key={data.id} className="flex flex-row gap-2 items-center justify-between py-1.5 pl-6 last:pb-2.5">
<span className="text-xs">{data.name}</span>
<div>
<IconButton onClick={() => handleDataRemove(data)}><MinusIcon /></IconButton>
</div>
</div>
))}
</>
</Accordion>
);
})}
</div>
</Accordion>
<Modal isOpen={isNewDatasetModalOpen}>
<div className="w-full max-w-2xl">
<div className="flex flex-row items-center justify-between">
<span className="text-2xl">Create a new dataset?</span>
<IconButton onClick={closeNewDatasetModal}><CloseIcon /></IconButton>
</div>
<div className="mt-8 mb-6">Please provide a name for the dataset being created.</div>
<form onSubmit={handleNewDatasetSubmit}>
<div className="max-w-md">
<Input name="datasetName" type="text" placeholder="Dataset name" required />
{newDatasetError && <span className="text-sm pl-4 text-gray-400">{newDatasetError}</span>}
</div>
<div className="flex flex-row gap-4 mt-4 justify-end">
<GhostButton type="button" onClick={() => closeNewDatasetModal()}>cancel</GhostButton>
<CTAButton type="submit">create</CTAButton>
</div>
</form>
</div>
</Modal>
<Modal isOpen={isRemoveDatasetModalOpen}>
<div className="w-full max-w-2xl">
<div className="flex flex-row items-center justify-between">
<span className="text-2xl">Delete <span className="text-indigo-600">{datasetToRemove?.name}</span> dataset?</span>
<IconButton onClick={handleDatasetRemoveCancel}><CloseIcon /></IconButton>
</div>
<div className="mt-8 mb-6">Are you sure you want to delete <span className="text-indigo-600">{datasetToRemove?.name}</span>? This action cannot be undone.</div>
<div className="flex flex-row gap-4 mt-4 justify-end">
<GhostButton type="button" onClick={handleDatasetRemoveCancel}>cancel</GhostButton>
<CTAButton onClick={handleRemoveDatasetConfirm} type="submit">delete</CTAButton>
</div>
</div>
</Modal>
<Modal isOpen={isRemoveDataModalOpen}>
<div className="w-full max-w-2xl">
<div className="flex flex-row items-center justify-between">
<span className="text-2xl">Delete <span className="text-indigo-600">{dataToRemove?.name}</span> data?</span>
<IconButton onClick={handleDataRemoveCancel}><CloseIcon /></IconButton>
</div>
<div className="mt-8 mb-6">Are you sure you want to delete <span className="text-indigo-600">{dataToRemove?.name}</span>? This action cannot be undone.</div>
<div className="flex flex-row gap-4 mt-4 justify-end">
<GhostButton type="button" onClick={handleDataRemoveCancel}>cancel</GhostButton>
<CTAButton onClick={handleDataRemoveConfirm} type="submit">delete</CTAButton>
</div>
</div>
</Modal>
</>
);
}