diff --git a/api/src/helper/helper.module.ts b/api/src/helper/helper.module.ts
index f60fd4e3..90f800f2 100644
--- a/api/src/helper/helper.module.ts
+++ b/api/src/helper/helper.module.ts
@@ -10,6 +10,7 @@ import { HttpModule } from '@nestjs/axios';
import { Global, Module } from '@nestjs/common';
import { InjectDynamicProviders } from 'nestjs-dynamic-providers';
+import { ChatModule } from '@/chat/chat.module';
import { CmsModule } from '@/cms/cms.module';
import { NlpModule } from '@/nlp/nlp.module';
@@ -26,7 +27,7 @@ import { HelperService } from './helper.service';
'dist/.hexabot/custom/extensions/helpers/**/*.helper.js',
)
@Module({
- imports: [HttpModule, NlpModule, CmsModule],
+ imports: [HttpModule, NlpModule, CmsModule, ChatModule],
controllers: [HelperController],
providers: [HelperService],
exports: [HelperService],
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/app-components/inputs/Selectable.tsx b/frontend/src/app-components/inputs/Selectable.tsx
index 09ab6197..2eab600c 100644
--- a/frontend/src/app-components/inputs/Selectable.tsx
+++ b/frontend/src/app-components/inputs/Selectable.tsx
@@ -25,18 +25,19 @@ import {
const SelectableBox = styled(Box)({
position: "relative",
- height: "30px",
marginBottom: "1rem",
"& .highlight, & .editable": {
position: "absolute",
top: 0,
display: "block",
width: "100%",
- padding: "4px",
+ padding: "0 4px",
+ lineHeight: 1.5,
+ whiteSpaceCollapse: "preserve",
},
"& .editable": {
+ position: "relative",
backgroundColor: "transparent",
- padding: "0px 4px",
color: "#000",
},
});
@@ -169,10 +170,10 @@ const Selectable: FC = ({
) {
const inputContainer = editableRef.current;
let substring: string = "";
- let input: HTMLInputElement | null = null;
+ let input: HTMLTextAreaElement | null = null;
if (inputContainer) {
- input = inputContainer.querySelector("input");
+ input = inputContainer.querySelector("textarea");
if (
input &&
@@ -267,6 +268,7 @@ const Selectable: FC = ({
/>
))}
> = ({
data: { defaultValues: nlpDatasetSample },
@@ -28,15 +64,134 @@ export const NlpSampleForm: FC> = ({
}) => {
const { t } = useTranslate();
const { toast } = useToast();
- const { mutate: updateSample } = useUpdate(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(
{
@@ -54,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 fb9873de..00000000
--- a/frontend/src/components/nlp/components/NlpTrainForm.tsx
+++ /dev/null
@@ -1,460 +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 = {
diff --git a/widget/src/components/Message.scss b/widget/src/components/Message.scss
index 655638cc..5a5b08bc 100644
--- a/widget/src/components/Message.scss
+++ b/widget/src/components/Message.scss
@@ -55,6 +55,8 @@
.sc-message--wrapper {
display: flex;
flex-direction: column;
+ overflow: hidden;
+ word-break: break-all;
.sc-message--text {
padding: 10px 20px;