<!-- .github/pull_request_template.md --> ## Description <!-- Provide a clear description of the changes in this PR --> ## DCO Affirmation I affirm that all code in every commit of this pull request conforms to the terms of the Topoteretes Developer Certificate of Origin.
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>
|
|
</>
|
|
);
|
|
}
|