mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
fix: refactor and enhance
This commit is contained in:
parent
328c5cefb3
commit
717d403532
@ -110,7 +110,7 @@ export const config: Config = {
|
||||
storageMode: 'disk',
|
||||
maxUploadSize: process.env.UPLOAD_MAX_SIZE_IN_BYTES
|
||||
? Number(process.env.UPLOAD_MAX_SIZE_IN_BYTES)
|
||||
: 2000000,
|
||||
: 50 * 1024 * 1024, // 50 MB in bytes
|
||||
appName: 'Hexabot.ai',
|
||||
},
|
||||
pagination: {
|
||||
|
@ -58,8 +58,8 @@
|
||||
"custom_code_is_invalid": "Custom code seems to contain some errors.",
|
||||
"attachment_failure_format": "Attachment has invalid format",
|
||||
"drop_file_here": "Drop file here or click to upload",
|
||||
"file_max_size": "File must have a size less than 25MB",
|
||||
"attachment_failure_size": "Invalid size! File must have a size less than 25MB",
|
||||
"file_max_size": "The file exceeds the maximum allowed size. Please ensure your file is within the size limit and try again.",
|
||||
"attachment_failure_size": "The file exceeds the maximum allowed size. Please ensure your file is within the size limit and try again.",
|
||||
"upload_failed": "Unable to upload the file!",
|
||||
"value_is_required": "NLU Value is required",
|
||||
"nlp_entity_name_is_invalid": "NLU Entity name format is invalid! Only `A-z`, `0-9` and `_` are allowed.",
|
||||
@ -108,7 +108,7 @@
|
||||
"no_label_found": "No label found",
|
||||
"code_is_required": "Language code is required",
|
||||
"text_is_required": "Text is required",
|
||||
"invalid_file_type": "Invalid file type",
|
||||
"invalid_file_type": "Invalid file type. Please select a file in the supported format.",
|
||||
"select_category": "Select a flow"
|
||||
},
|
||||
"menu": {
|
||||
@ -343,6 +343,7 @@
|
||||
"precision": "Precision",
|
||||
"recall": "Recall",
|
||||
"f1score": "F1 Score",
|
||||
"all": "All",
|
||||
"train": "Train",
|
||||
"test": "Test",
|
||||
"inbox": "Inbox",
|
||||
|
@ -58,8 +58,8 @@
|
||||
"custom_code_is_invalid": "Le code personnalisé semble contenir quelques erreurs.",
|
||||
"attachment_failure_format": "La pièce jointe a un format invalide",
|
||||
"drop_file_here": "Déposez le fichier ici ou cliquez pour télécharger",
|
||||
"file_max_size": "Le fichier doit avoir une taille inférieure à 25 Mo",
|
||||
"attachment_failure_size": "Taille invalide! Le fichier doit avoir une taille inférieure à 25 Mo",
|
||||
"file_max_size": "Le fichier dépasse la taille maximale autorisée. Veuillez vérifier que votre fichier respecte la limite de taille et réessayez.",
|
||||
"attachment_failure_size": "Le fichier dépasse la taille maximale autorisée. Veuillez vérifier que votre fichier respecte la limite de taille et réessayez.",
|
||||
"upload_failed": "Impossible d'envoyer le fichier au serveur!",
|
||||
"value_is_required": "La valeur NLU est requise",
|
||||
"nlp_entity_name_is_invalid": "Le nom d'entité NLU n'est pas valide! Seuls `A-z`,` 0-9` et `_` sont autorisés.",
|
||||
@ -108,7 +108,7 @@
|
||||
"no_label_found": "Aucune étiquette trouvée",
|
||||
"code_is_required": "Le code est requis",
|
||||
"text_is_required": "Texte requis",
|
||||
"invalid_file_type": "Type de fichier invalide",
|
||||
"invalid_file_type": "Type de fichier invalide. Veuillez choisir un fichier dans un format pris en charge.",
|
||||
"select_category": "Sélectionner une catégorie"
|
||||
},
|
||||
"menu": {
|
||||
@ -343,6 +343,7 @@
|
||||
"precision": "Précision",
|
||||
"recall": "Rappel",
|
||||
"f1score": "F1-Score",
|
||||
"all": "Tout",
|
||||
"train": "Apprentissage",
|
||||
"test": "Evaluation",
|
||||
"inbox": "Boîte de réception",
|
||||
|
82
frontend/src/app-components/inputs/FileInput.tsx
Normal file
82
frontend/src/app-components/inputs/FileInput.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import UploadIcon from "@mui/icons-material/Upload";
|
||||
import { Button, CircularProgress } from "@mui/material";
|
||||
import { ChangeEvent, forwardRef } from "react";
|
||||
|
||||
import { useConfig } from "@/hooks/useConfig";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
|
||||
import { Input } from "./Input";
|
||||
|
||||
export type FileUploadButtonProps = {
|
||||
label: string;
|
||||
accept?: string;
|
||||
onChange: (file: File) => void;
|
||||
isLoading?: boolean;
|
||||
error?: boolean;
|
||||
helperText?: string;
|
||||
};
|
||||
|
||||
const FileUploadButton = forwardRef<HTMLLabelElement, FileUploadButtonProps>(
|
||||
({ label, accept, isLoading = true, onChange }, ref) => {
|
||||
const config = useConfig();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslate();
|
||||
const handleImportChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files?.length) {
|
||||
const file = event.target.files.item(0);
|
||||
|
||||
if (!file) return false;
|
||||
|
||||
if (accept && !accept.split(",").includes(file.type)) {
|
||||
toast.error(t("message.invalid_file_type"));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.maxUploadSize && file.size > config.maxUploadSize) {
|
||||
toast.error(t("message.file_max_size"));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onChange(file);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
ref={ref}
|
||||
htmlFor="importFile"
|
||||
variant="contained"
|
||||
component="label"
|
||||
startIcon={<UploadIcon />}
|
||||
endIcon={isLoading ? <CircularProgress size="1rem" /> : null}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
<Input
|
||||
id="importFile"
|
||||
type="file"
|
||||
value="" // to trigger an automatic reset to allow the same file to be selected multiple times
|
||||
sx={{ display: "none" }}
|
||||
onChange={handleImportChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
FileUploadButton.displayName = "FileUploadButton";
|
||||
|
||||
export default FileUploadButton;
|
@ -7,26 +7,29 @@
|
||||
*/
|
||||
|
||||
import CircleIcon from "@mui/icons-material/Circle";
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import UploadIcon from "@mui/icons-material/Upload";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Grid,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
MenuItem,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useQueryClient } from "react-query";
|
||||
|
||||
import { DeleteDialog } from "@/app-components/dialogs";
|
||||
import { ChipEntity } from "@/app-components/displays/ChipEntity";
|
||||
import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
|
||||
import FileUploadButton from "@/app-components/inputs/FileInput";
|
||||
import { FilterTextfield } from "@/app-components/inputs/FilterTextfield";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import {
|
||||
@ -35,6 +38,7 @@ import {
|
||||
} from "@/app-components/tables/columns/getColumns";
|
||||
import { renderHeader } from "@/app-components/tables/columns/renderHeader";
|
||||
import { DataGrid } from "@/app-components/tables/DataGrid";
|
||||
import { isSameEntity } from "@/hooks/crud/helpers";
|
||||
import { useDelete } from "@/hooks/crud/useDelete";
|
||||
import { useDeleteMany } from "@/hooks/crud/useDeleteMany";
|
||||
import { useFind } from "@/hooks/crud/useFind";
|
||||
@ -62,6 +66,7 @@ import { NlpImportDialog } from "../NlpImportDialog";
|
||||
import { NlpSampleDialog } from "../NlpSampleDialog";
|
||||
|
||||
const NLP_SAMPLE_TYPE_COLORS = {
|
||||
all: "#fff",
|
||||
test: "#e6a23c",
|
||||
train: "#67c23a",
|
||||
inbox: "#909399",
|
||||
@ -71,7 +76,8 @@ export default function NlpSample() {
|
||||
const { apiUrl } = useConfig();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslate();
|
||||
const [type, setType] = useState<NlpSampleType | undefined>(undefined);
|
||||
const queryClient = useQueryClient();
|
||||
const [type, setType] = useState<NlpSampleType | "all">("all");
|
||||
const [language, setLanguage] = useState<string | undefined>(undefined);
|
||||
const hasPermission = useHasPermission();
|
||||
const getNlpEntityFromCache = useGetFromCache(EntityType.NLP_ENTITY);
|
||||
@ -81,7 +87,10 @@ export default function NlpSample() {
|
||||
);
|
||||
const getLanguageFromCache = useGetFromCache(EntityType.LANGUAGE);
|
||||
const { onSearch, searchPayload } = useSearch<INlpSample>({
|
||||
$eq: [...(type ? [{ type }] : []), ...(language ? [{ language }] : [])],
|
||||
$eq: [
|
||||
...(type !== "all" ? [{ type }] : []),
|
||||
...(language ? [{ language }] : []),
|
||||
],
|
||||
$iLike: ["text"],
|
||||
});
|
||||
const { mutateAsync: deleteNlpSample } = useDelete(EntityType.NLP_SAMPLE, {
|
||||
@ -113,8 +122,20 @@ export default function NlpSample() {
|
||||
toast.error(t("message.import_failed"));
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data.length) toast.success(t("message.success_import"));
|
||||
else {
|
||||
queryClient.removeQueries({
|
||||
predicate: ({ queryKey }) => {
|
||||
const [_qType, qEntity] = queryKey;
|
||||
|
||||
return (
|
||||
isSameEntity(qEntity, EntityType.NLP_SAMPLE_ENTITY) ||
|
||||
isSameEntity(qEntity, EntityType.NLP_ENTITY) ||
|
||||
isSameEntity(qEntity, EntityType.NLP_VALUE)
|
||||
);
|
||||
},
|
||||
});
|
||||
if (data.length) {
|
||||
toast.success(t("message.success_import"));
|
||||
} else {
|
||||
toast.error(t("message.import_duplicated_data"));
|
||||
}
|
||||
},
|
||||
@ -275,14 +296,8 @@ export default function NlpSample() {
|
||||
const handleSelectionChange = (selection: GridRowSelectionModel) => {
|
||||
setSelectedNlpSamples(selection as string[]);
|
||||
};
|
||||
const handleImportChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files?.length) {
|
||||
const file = event.target.files.item(0);
|
||||
|
||||
if (file) {
|
||||
await importDataset(file);
|
||||
}
|
||||
}
|
||||
const handleImportChange = async (file: File) => {
|
||||
await importDataset(file);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -317,7 +332,7 @@ export default function NlpSample() {
|
||||
<AutoCompleteEntitySelect<ILanguage, "title", false>
|
||||
fullWidth={false}
|
||||
sx={{
|
||||
minWidth: "150px",
|
||||
minWidth: "256px",
|
||||
}}
|
||||
autoFocus
|
||||
searchFields={["title", "code"]}
|
||||
@ -332,35 +347,38 @@ export default function NlpSample() {
|
||||
select
|
||||
fullWidth={false}
|
||||
sx={{
|
||||
minWidth: "150px",
|
||||
minWidth: "256px",
|
||||
}}
|
||||
label={t("label.dataset")}
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value as NlpSampleType)}
|
||||
SelectProps={{
|
||||
...(type && {
|
||||
IconComponent: () => (
|
||||
<IconButton size="small" onClick={() => setType(undefined)}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
endAdornment: (
|
||||
<InputAdornment sx={{ marginRight: "1rem" }} position="end">
|
||||
<IconButton size="small" onClick={() => setType("all")}>
|
||||
<ClearIcon sx={{ fontSize: "1.25rem" }} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}),
|
||||
renderValue: (value) => <Box>{t(`label.${value}`)}</Box>,
|
||||
}}
|
||||
>
|
||||
{Object.values(NlpSampleType).map((nlpSampleType, index) => (
|
||||
<MenuItem key={index} value={nlpSampleType}>
|
||||
<Grid container>
|
||||
<Grid item xs={4}>
|
||||
{["all", ...Object.values(NlpSampleType)].map(
|
||||
(nlpSampleType, index) => (
|
||||
<MenuItem key={index} value={nlpSampleType}>
|
||||
<Box display="flex" gap={1}>
|
||||
<CircleIcon
|
||||
fontSize="small"
|
||||
sx={{ color: NLP_SAMPLE_TYPE_COLORS[nlpSampleType] }}
|
||||
sx={{
|
||||
color: NLP_SAMPLE_TYPE_COLORS[nlpSampleType],
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>{nlpSampleType}</Grid>
|
||||
</Grid>
|
||||
</MenuItem>
|
||||
))}
|
||||
<Typography>{t(`label.${nlpSampleType}`)}</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
),
|
||||
)}
|
||||
</Input>
|
||||
<ButtonGroup sx={{ marginLeft: "auto" }}>
|
||||
{hasPermission(EntityType.NLP_SAMPLE, PermissionAction.CREATE) &&
|
||||
@ -368,25 +386,12 @@ export default function NlpSample() {
|
||||
EntityType.NLP_SAMPLE_ENTITY,
|
||||
PermissionAction.CREATE,
|
||||
) ? (
|
||||
<>
|
||||
<Button
|
||||
htmlFor="importFile"
|
||||
variant="contained"
|
||||
component="label"
|
||||
startIcon={<UploadIcon />}
|
||||
endIcon={isLoading ? <CircularProgress size="1rem" /> : null}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{t("button.import")}
|
||||
</Button>
|
||||
<Input
|
||||
id="importFile"
|
||||
type="file"
|
||||
value="" // to trigger an automatic reset to allow the same file to be selected multiple times
|
||||
sx={{ display: "none" }}
|
||||
onChange={handleImportChange}
|
||||
/>
|
||||
</>
|
||||
<FileUploadButton
|
||||
accept="text/csv"
|
||||
label={t("button.import")}
|
||||
onChange={handleImportChange}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
) : null}
|
||||
{hasPermission(EntityType.NLP_SAMPLE, PermissionAction.READ) &&
|
||||
hasPermission(
|
||||
|
@ -13,6 +13,7 @@ export const ConfigContext = createContext<IConfig | null>(null);
|
||||
export interface IConfig {
|
||||
apiUrl: string;
|
||||
ssoEnabled: boolean;
|
||||
maxUploadSize: number;
|
||||
}
|
||||
|
||||
export const ConfigProvider = ({ children }) => {
|
||||
|
@ -11,6 +11,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
type ResponseData = {
|
||||
apiUrl: string;
|
||||
ssoEnabled: boolean;
|
||||
maxUploadSize: number;
|
||||
};
|
||||
|
||||
export default function handler(
|
||||
@ -20,5 +21,8 @@ export default function handler(
|
||||
res.status(200).json({
|
||||
apiUrl: process.env.NEXT_PUBLIC_API_ORIGIN || "http://localhost:4000",
|
||||
ssoEnabled: process.env.NEXT_PUBLIC_SSO_ENABLED === "true" || false,
|
||||
maxUploadSize: process.env.UPLOAD_MAX_SIZE_IN_BYTES
|
||||
? Number(process.env.UPLOAD_MAX_SIZE_IN_BYTES)
|
||||
: 50 * 1024 * 1024, // 50 MB in bytes
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user