diff --git a/frontend/src/app-components/buttons/FormButtons.tsx b/frontend/src/app-components/buttons/FormButtons.tsx
index a4c56429..19b7c3ac 100644
--- a/frontend/src/app-components/buttons/FormButtons.tsx
+++ b/frontend/src/app-components/buttons/FormButtons.tsx
@@ -29,6 +29,7 @@ export const DialogFormButtons = ({
return (
}
{...cancelButtonProps}
>
- {t(cancelButtonTitle)}
+ {cancelButtonProps?.text || t(cancelButtonTitle)}
}
{...confirmButtonProps}
>
- {t(confirmButtonTitle)}
+ {confirmButtonProps?.text || t(confirmButtonTitle)}
);
diff --git a/frontend/src/components/nlp/components/NlpSample.tsx b/frontend/src/components/nlp/components/NlpSample.tsx
index 9cd5de67..2d0ef295 100644
--- a/frontend/src/components/nlp/components/NlpSample.tsx
+++ b/frontend/src/components/nlp/components/NlpSample.tsx
@@ -184,7 +184,6 @@ export default function NlpSample() {
{ defaultValues: data },
{
maxWidth: "md",
- hasButtons: false,
},
);
},
diff --git a/frontend/src/components/nlp/components/NlpSampleForm.tsx b/frontend/src/components/nlp/components/NlpSampleForm.tsx
index 3a839bf9..360d9bf0 100644
--- a/frontend/src/components/nlp/components/NlpSampleForm.tsx
+++ b/frontend/src/components/nlp/components/NlpSampleForm.tsx
@@ -6,20 +6,55 @@
* 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 { FC, Fragment } from "react";
+import AddIcon from "@mui/icons-material/Add";
+import DeleteIcon from "@mui/icons-material/Delete";
+import {
+ Box,
+ Button,
+ Chip,
+ debounce,
+ FormControl,
+ FormControlLabel,
+ FormLabel,
+ IconButton,
+ Radio,
+ RadioGroup,
+ Typography,
+} from "@mui/material";
+import { FC, Fragment, useCallback, useEffect, useMemo, useState } from "react";
+import { Controller, useFieldArray, useForm } from "react-hook-form";
+import { useQuery } from "react-query";
+import { ContentContainer, ContentItem } from "@/app-components/dialogs";
+import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
+import AutoCompleteSelect from "@/app-components/inputs/AutoCompleteSelect";
+import Selectable from "@/app-components/inputs/Selectable";
+import { useCreate } from "@/hooks/crud/useCreate";
+import { useGetFromCache } from "@/hooks/crud/useGet";
import { useUpdate } from "@/hooks/crud/useUpdate";
+import { useApiClient } from "@/hooks/useApiClient";
+import { useNlp } from "@/hooks/useNlp";
import { useToast } from "@/hooks/useToast";
import { useTranslate } from "@/hooks/useTranslate";
-import { EntityType } from "@/services/types";
-import { ComponentFormProps } from "@/types/common/dialogs.types";
+import { EntityType, Format } from "@/services/types";
import {
+ ComponentFormProps,
+ FormButtonsProps,
+} from "@/types/common/dialogs.types";
+import { ILanguage } from "@/types/language.types";
+import { INlpEntity } from "@/types/nlp-entity.types";
+import {
+ INlpDatasetKeywordEntity,
+ INlpDatasetPatternEntity,
INlpDatasetSample,
INlpDatasetSampleAttributes,
+ INlpDatasetTraitEntity,
+ INlpSample,
INlpSampleFormAttributes,
+ INlpSampleFull,
+ NlpSampleType,
} from "@/types/nlp-sample.types";
-
-import NlpDatasetSample from "./NlpTrainForm";
+import { INlpValue } from "@/types/nlp-value.types";
export const NlpSampleForm: FC> = ({
data: { defaultValues: nlpDatasetSample },
@@ -29,18 +64,134 @@ export const NlpSampleForm: FC> = ({
}) => {
const { t } = useTranslate();
const { toast } = useToast();
- const { mutate: updateSample } = useUpdate<
- EntityType.NLP_SAMPLE,
- INlpDatasetSampleAttributes
- >(EntityType.NLP_SAMPLE, {
+ const options = {
onError: () => {
toast.error(t("message.internal_server_error"));
},
onSuccess: () => {
toast.success(t("message.success_save"));
},
+ };
+ const { mutate: createSample } = useCreate<
+ EntityType.NLP_SAMPLE,
+ INlpDatasetSampleAttributes,
+ INlpSample,
+ INlpSampleFull
+ >(EntityType.NLP_SAMPLE, {
+ ...options,
+ onSuccess: () => {
+ options.onSuccess();
+ refetchAllEntities();
+ reset({
+ ...defaultValues,
+ text: "",
+ });
+ },
});
- const onSubmitForm = (form: INlpSampleFormAttributes) => {
+ const { mutate: updateSample } = useUpdate<
+ EntityType.NLP_SAMPLE,
+ INlpDatasetSampleAttributes
+ >(EntityType.NLP_SAMPLE, options);
+ const {
+ allTraitEntities,
+ allKeywordEntities,
+ allPatternEntities,
+ refetchAllEntities,
+ } = useNlp();
+ const getNlpValueFromCache = useGetFromCache(EntityType.NLP_VALUE);
+ const defaultValues: INlpSampleFormAttributes = useMemo(
+ () => ({
+ type: nlpDatasetSample?.type || NlpSampleType.train,
+ text: nlpDatasetSample?.text || "",
+ language: nlpDatasetSample?.language || null,
+ traitEntities: [...allTraitEntities.values()].map((e) => {
+ return {
+ entity: e.name,
+ value:
+ (nlpDatasetSample?.entities || []).find(
+ (se) => se.entity === e.name,
+ )?.value || "",
+ };
+ }) as INlpDatasetTraitEntity[],
+ keywordEntities: (nlpDatasetSample?.entities || []).filter((e) =>
+ allKeywordEntities.has(e.entity),
+ ) as INlpDatasetKeywordEntity[],
+ }),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [allKeywordEntities, allTraitEntities, JSON.stringify(nlpDatasetSample)],
+ );
+ const { handleSubmit, control, register, reset, setValue, watch } =
+ useForm({
+ defaultValues,
+ });
+ const currentText = watch("text");
+ const currentType = watch("type");
+ const { apiClient } = useApiClient();
+ const [patternEntities, setPatternEntities] = useState<
+ INlpDatasetPatternEntity[]
+ >([]);
+ const { fields: traitEntities, update: updateTraitEntity } = useFieldArray({
+ control,
+ name: "traitEntities",
+ });
+ const {
+ fields: keywordEntities,
+ insert: insertKeywordEntity,
+ update: updateKeywordEntity,
+ remove: removeKeywordEntity,
+ } = useFieldArray({
+ control,
+ name: "keywordEntities",
+ });
+ // Auto-predict on text change
+ const debounceSetText = useCallback(
+ debounce((text: string) => {
+ setValue("text", text);
+ }, 400),
+ [setValue],
+ );
+ const { isLoading } = useQuery({
+ queryKey: ["nlp-prediction", currentText],
+ queryFn: async () => {
+ return await apiClient.predictNlp(currentText);
+ },
+ onSuccess: (prediction) => {
+ const predictedTraitEntities: INlpDatasetTraitEntity[] =
+ prediction.entities.filter((e) => allTraitEntities.has(e.entity));
+ const predictedKeywordEntities = prediction.entities.filter((e) =>
+ allKeywordEntities.has(e.entity),
+ ) as INlpDatasetKeywordEntity[];
+ const predictedPatternEntities = prediction.entities.filter((e) =>
+ allPatternEntities.has(e.entity),
+ ) as INlpDatasetKeywordEntity[];
+ const language = prediction.entities.find(
+ ({ entity }) => entity === "language",
+ );
+
+ setValue("language", language?.value || "");
+ setValue("traitEntities", predictedTraitEntities);
+ setValue("keywordEntities", predictedKeywordEntities);
+ setPatternEntities(predictedPatternEntities);
+ },
+ enabled:
+ // Inbox sample update
+ nlpDatasetSample?.type === "inbox" ||
+ // New sample
+ (!nlpDatasetSample && !!currentText),
+ });
+ const findInsertIndex = (newItem: INlpDatasetKeywordEntity): number => {
+ const index = keywordEntities.findIndex(
+ (entity) => entity.start && newItem.start && entity.start > newItem.start,
+ );
+
+ return index === -1 ? keywordEntities.length : index;
+ };
+ const [selection, setSelection] = useState<{
+ value: string;
+ start: number;
+ end: number;
+ } | null>(null);
+ const onSubmitForm = async (form: INlpSampleFormAttributes) => {
if (nlpDatasetSample?.id) {
updateSample(
{
@@ -58,15 +209,328 @@ export const NlpSampleForm: FC> = ({
},
},
);
+ } else {
+ createSample({
+ text: form.text,
+ type: form.type,
+ entities: [...form.traitEntities, ...form.keywordEntities],
+ language: form.language,
+ });
}
};
+ useEffect(() => {
+ reset(defaultValues);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [JSON.stringify(defaultValues)]);
+
+ const cancelButtonProps = {
+ sx: {
+ display: "inline-block",
+ overflow: "hidden",
+ textAlign: "left",
+ whiteSpace: "nowrap",
+ textOverflow: "ellipsis",
+ "& .MuiButton-startIcon": {
+ top: "4px",
+ margin: "auto 4px auto auto",
+ display: "inline-block",
+ position: "relative",
+ },
+ },
+ color: "secondary",
+ variant: "contained",
+ onClick: () => {
+ const newKeywordEntity = {
+ ...selection,
+ entity: "",
+ } as INlpDatasetKeywordEntity;
+ const newIndex = findInsertIndex(newKeywordEntity);
+
+ selection && insertKeywordEntity(newIndex, newKeywordEntity);
+ setSelection(null);
+ },
+ disabled: !selection?.value,
+ startIcon: ,
+ } satisfies FormButtonsProps["cancelButtonProps"];
+ const confirmButtonProps = {
+ sx: { minWidth: "120px" },
+ value: "button.validate",
+ variant: "contained",
+ disabled: !(
+ currentText !== "" &&
+ currentType !== NlpSampleType.inbox &&
+ traitEntities.every((e) => e.value !== "") &&
+ keywordEntities.every((e) => e.value !== "")
+ ),
+ onClick: handleSubmit(onSubmitForm),
+ } satisfies FormButtonsProps["confirmButtonProps"];
+
return (
- {}} {...WrapperProps}>
-
+ {}}
+ {...WrapperProps}
+ cancelButtonProps={{
+ ...WrapperProps?.cancelButtonProps,
+ text: !selection?.value
+ ? t("button.select_some_text")
+ : t("button.add_nlp_entity", { 0: selection.value }),
+ ...cancelButtonProps,
+ }}
+ confirmButtonProps={{
+ ...WrapperProps?.confirmButtonProps,
+ ...confirmButtonProps,
+ }}
+ >
+
);
};
diff --git a/frontend/src/components/nlp/components/NlpTrainForm.tsx b/frontend/src/components/nlp/components/NlpTrainForm.tsx
deleted file mode 100644
index 137f51c0..00000000
--- a/frontend/src/components/nlp/components/NlpTrainForm.tsx
+++ /dev/null
@@ -1,459 +0,0 @@
-/*
- * Copyright © 2025 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 AddIcon from "@mui/icons-material/Add";
-import Check from "@mui/icons-material/Check";
-import DeleteIcon from "@mui/icons-material/Delete";
-import {
- Box,
- Button,
- Chip,
- debounce,
- FormControl,
- FormControlLabel,
- FormLabel,
- IconButton,
- Radio,
- RadioGroup,
- Typography,
-} from "@mui/material";
-import { FC, useCallback, useEffect, useMemo, useState } from "react";
-import { Controller, useFieldArray, useForm } from "react-hook-form";
-import { useQuery } from "react-query";
-
-import { ContentContainer, ContentItem } from "@/app-components/dialogs";
-import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
-import AutoCompleteSelect from "@/app-components/inputs/AutoCompleteSelect";
-import Selectable from "@/app-components/inputs/Selectable";
-import { useGetFromCache } from "@/hooks/crud/useGet";
-import { useApiClient } from "@/hooks/useApiClient";
-import { useNlp } from "@/hooks/useNlp";
-import { useTranslate } from "@/hooks/useTranslate";
-import { EntityType, Format } from "@/services/types";
-import { ILanguage } from "@/types/language.types";
-import { INlpEntity } from "@/types/nlp-entity.types";
-import {
- INlpDatasetKeywordEntity,
- INlpDatasetPatternEntity,
- INlpDatasetSample,
- INlpDatasetTraitEntity,
- INlpSampleFormAttributes,
- NlpSampleType,
-} from "@/types/nlp-sample.types";
-import { INlpValue } from "@/types/nlp-value.types";
-
-type NlpDatasetSampleProps = {
- sample?: INlpDatasetSample;
- submitForm: (params: INlpSampleFormAttributes) => void;
-};
-
-const NlpDatasetSample: FC = ({
- sample,
- submitForm,
-}) => {
- const { t } = useTranslate();
- const {
- allTraitEntities,
- allKeywordEntities,
- allPatternEntities,
- refetchAllEntities,
- } = useNlp();
- const getNlpValueFromCache = useGetFromCache(EntityType.NLP_VALUE);
- const defaultValues: INlpSampleFormAttributes = useMemo(
- () => ({
- type: sample?.type || NlpSampleType.train,
- text: sample?.text || "",
- language: sample?.language || null,
- traitEntities: [...allTraitEntities.values()].map((e) => {
- return {
- entity: e.name,
- value:
- (sample?.entities || []).find((se) => se.entity === e.name)
- ?.value || "",
- };
- }) as INlpDatasetTraitEntity[],
- keywordEntities: (sample?.entities || []).filter((e) =>
- allKeywordEntities.has(e.entity),
- ) as INlpDatasetKeywordEntity[],
- }),
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [allKeywordEntities, allTraitEntities, JSON.stringify(sample)],
- );
- const { handleSubmit, control, register, reset, setValue, watch } =
- useForm({
- defaultValues,
- });
- const currentText = watch("text");
- const currentType = watch("type");
- const { apiClient } = useApiClient();
- const [patternEntities, setPatternEntities] = useState<
- INlpDatasetPatternEntity[]
- >([]);
- const { fields: traitEntities, update: updateTraitEntity } = useFieldArray({
- control,
- name: "traitEntities",
- });
- const {
- fields: keywordEntities,
- insert: insertKeywordEntity,
- update: updateKeywordEntity,
- remove: removeKeywordEntity,
- } = useFieldArray({
- control,
- name: "keywordEntities",
- });
- // Auto-predict on text change
- const debounceSetText = useCallback(
- debounce((text: string) => {
- setValue("text", text);
- }, 400),
- [setValue],
- );
- const { isLoading } = useQuery({
- queryKey: ["nlp-prediction", currentText],
- queryFn: async () => {
- return await apiClient.predictNlp(currentText);
- },
- onSuccess: (prediction) => {
- const predictedTraitEntities: INlpDatasetTraitEntity[] =
- prediction.entities.filter((e) => allTraitEntities.has(e.entity));
- const predictedKeywordEntities = prediction.entities.filter((e) =>
- allKeywordEntities.has(e.entity),
- ) as INlpDatasetKeywordEntity[];
- const predictedPatternEntities = prediction.entities.filter((e) =>
- allPatternEntities.has(e.entity),
- ) as INlpDatasetKeywordEntity[];
- const language = prediction.entities.find(
- ({ entity }) => entity === "language",
- );
-
- setValue("language", language?.value || "");
- setValue("traitEntities", predictedTraitEntities);
- setValue("keywordEntities", predictedKeywordEntities);
- setPatternEntities(predictedPatternEntities);
- },
- enabled:
- // Inbox sample update
- sample?.type === "inbox" ||
- // New sample
- (!sample && !!currentText),
- });
- const findInsertIndex = (newItem: INlpDatasetKeywordEntity): number => {
- const index = keywordEntities.findIndex(
- (entity) => entity.start && newItem.start && entity.start > newItem.start,
- );
-
- return index === -1 ? keywordEntities.length : index;
- };
- const [selection, setSelection] = useState<{
- value: string;
- start: number;
- end: number;
- } | null>(null);
- const onSubmitForm = (form: INlpSampleFormAttributes) => {
- submitForm(form);
- refetchAllEntities();
- reset({
- ...defaultValues,
- text: "",
- });
- };
-
- useEffect(() => {
- reset(defaultValues);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [JSON.stringify(defaultValues)]);
-
- return (
-
-
-
- );
-};
-
-NlpDatasetSample.displayName = "NlpTrain";
-
-export default NlpDatasetSample;
diff --git a/frontend/src/components/nlp/index.tsx b/frontend/src/components/nlp/index.tsx
index b38e372b..642c4972 100644
--- a/frontend/src/components/nlp/index.tsx
+++ b/frontend/src/components/nlp/index.tsx
@@ -13,22 +13,14 @@ import { useRouter } from "next/router";
import React from "react";
import { TabPanel } from "@/app-components/tabs/TabPanel";
-import { useCreate } from "@/hooks/crud/useCreate";
import { useFind } from "@/hooks/crud/useFind";
-import { useToast } from "@/hooks/useToast";
import { useTranslate } from "@/hooks/useTranslate";
import { PageHeader } from "@/layout/content/PageHeader";
import { EntityType, Format } from "@/services/types";
-import {
- INlpDatasetSampleAttributes,
- INlpSample,
- INlpSampleFormAttributes,
- INlpSampleFull,
-} from "@/types/nlp-sample.types";
import NlpDatasetCounter from "./components/NlpDatasetCounter";
import NlpSample from "./components/NlpSample";
-import NlpDatasetSample from "./components/NlpTrainForm";
+import { NlpSampleForm } from "./components/NlpSampleForm";
import { NlpValues } from "./components/NlpValue";
const NlpEntity = dynamic(() => import("./components/NlpEntity"));
@@ -61,28 +53,6 @@ export const Nlp = ({
);
};
const { t } = useTranslate();
- const { toast } = useToast();
- const { mutate: createSample } = useCreate<
- EntityType.NLP_SAMPLE,
- INlpDatasetSampleAttributes,
- INlpSample,
- INlpSampleFull
- >(EntityType.NLP_SAMPLE, {
- onError: () => {
- toast.error(t("message.internal_server_error"));
- },
- onSuccess: () => {
- toast.success(t("message.success_save"));
- },
- });
- const onSubmitForm = (params: INlpSampleFormAttributes) => {
- createSample({
- text: params.text,
- type: params.type,
- entities: [...params.traitEntities, ...params.keywordEntities],
- language: params.language,
- });
- };
return (
@@ -90,8 +60,8 @@ export const Nlp = ({
-
-
+
+
diff --git a/frontend/src/types/common/dialogs.types.ts b/frontend/src/types/common/dialogs.types.ts
index 6dc13258..872721f5 100644
--- a/frontend/src/types/common/dialogs.types.ts
+++ b/frontend/src/types/common/dialogs.types.ts
@@ -162,8 +162,8 @@ export interface FormDialogProps
export interface FormButtonsProps {
onSubmit?: (e: BaseSyntheticEvent) => void;
onCancel?: () => void;
- cancelButtonProps?: ButtonProps;
- confirmButtonProps?: ButtonProps;
+ cancelButtonProps?: ButtonProps & { text?: string };
+ confirmButtonProps?: ButtonProps & { text?: string };
}
export type TPayload = {