346 lines
12 KiB
TypeScript
346 lines
12 KiB
TypeScript
"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>
|
|
</>
|
|
);
|
|
}
|