From 376b515029571b8573d5a26993b905a915e0564a Mon Sep 17 00:00:00 2001
From: Mohamed Marrouchi <marrouchi.mohamed@gmail.com>
Date: Wed, 29 Jan 2025 20:30:15 +0100
Subject: [PATCH] feat: split nlu pages

---
 frontend/public/locales/en/translation.json   |   4 +-
 frontend/public/locales/fr/translation.json   |   5 +-
 .../nlp/{components => }/NlpEntity.tsx        |  99 ++++----
 .../nlp/{components => }/NlpSample.tsx        | 233 ++++++++++--------
 .../src/components/nlp/NlpSampleDialog.tsx    |  51 ++--
 .../nlp/{components => }/NlpValues.tsx        | 192 +++++++--------
 frontend/src/components/nlp/index.tsx         | 131 ----------
 frontend/src/layout/VerticalMenu.tsx          |  24 +-
 .../[id]/nlpValues.tsx => dataset.tsx}        |  18 +-
 .../{index.tsx => entities/[id]/values.tsx}   |  17 +-
 .../nlp/{nlp-entities => entities}/index.tsx  |  18 +-
 11 files changed, 374 insertions(+), 418 deletions(-)
 rename frontend/src/components/nlp/{components => }/NlpEntity.tsx (77%)
 rename frontend/src/components/nlp/{components => }/NlpSample.tsx (68%)
 rename frontend/src/components/nlp/{components => }/NlpValues.tsx (62%)
 delete mode 100644 frontend/src/components/nlp/index.tsx
 rename frontend/src/pages/nlp/{nlp-entities/[id]/nlpValues.tsx => dataset.tsx} (56%)
 rename frontend/src/pages/nlp/{index.tsx => entities/[id]/values.tsx} (65%)
 rename frontend/src/pages/nlp/{nlp-entities => entities}/index.tsx (55%)

diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json
index dd9bad2aa..4feb04cf3 100644
--- a/frontend/public/locales/en/translation.json
+++ b/frontend/public/locales/en/translation.json
@@ -124,6 +124,7 @@
     "visual_editor": "Visual Editor",
     "nlp": "NLU",
     "nlp_entities": "NLU Entities",
+    "nlp_dataset": "NLU Dataset",
     "inbox": "Inbox",
     "categories": "Flows",
     "context_vars": "Context Vars",
@@ -164,7 +165,7 @@
     "regular_blocks": "Regular Blocks",
     "advanced_blocks": "Advanced blocks",
     "custom_blocks": "Custom blocks",
-    "nlp": "NLU Samples",
+    "nlp_datatset": "NLU Dataset",
     "nlp_train": "NLU training",
     "nlp_entities": "NLU Entities",
     "new_nlp_entity": "New NLU Entity",
@@ -172,6 +173,7 @@
     "nlp_entity_values": "NLU Values",
     "new_nlp_entity_value": "New NLU Value",
     "evaluation_report": "Evaluation Report",
+    "add_nlp_sample": "Add NLU Sample",
     "edit_nlp_sample": "Edit NLU Sample",
     "edit_nlp_value": "Edit NLU Value",
     "context_vars": "Context Vars",
diff --git a/frontend/public/locales/fr/translation.json b/frontend/public/locales/fr/translation.json
index 7ce2c0dd1..b9b09a904 100644
--- a/frontend/public/locales/fr/translation.json
+++ b/frontend/public/locales/fr/translation.json
@@ -124,6 +124,7 @@
     "visual_editor": "Editeur Visuel",
     "nlp": "NLU",
     "nlp_entities": "Entités NLU",
+    "nlp_dataset": "Données NLU",
     "inbox": "Boîte de réception",
     "categories": "Catégories",
     "context_vars": "Variables contextuelles",
@@ -164,15 +165,17 @@
     "regular_blocks": "Blocs réguliers",
     "advanced_blocks": "Blocs avancés",
     "custom_blocks": "Blocs spécifiques",
-    "nlp": "Expressions NLU",
+    "nlp": "Données NLU",
     "nlp_train": "Apprentissage NLU",
     "nlp_entities": "Entités NLU",
+    "nlp_dataset": "Données NLU",
     "new_nlp_entity": "Nouvelle entité NLU",
     "edit_nlp_entity": "Modifier l'entité NLU",
     "edit_nlp_value": "Modifier la valeur NLU",
     "nlp_entity_values": "Valeurs NLU",
     "new_nlp_entity_value": "Nouvelle valeur NLU",
     "evaluation_report": "Rapport d'évaluation",
+    "add_nlp_sample": "Ajouter l'expression NLU",
     "edit_nlp_sample": "Modifier l'expression NLU",
     "context_vars": "Variables contextuelles",
     "new_context_var": "Nouvelle variable contextuelle",
diff --git a/frontend/src/components/nlp/components/NlpEntity.tsx b/frontend/src/components/nlp/NlpEntity.tsx
similarity index 77%
rename from frontend/src/components/nlp/components/NlpEntity.tsx
rename to frontend/src/components/nlp/NlpEntity.tsx
index 4ae9c9696..7d1f08779 100644
--- a/frontend/src/components/nlp/components/NlpEntity.tsx
+++ b/frontend/src/components/nlp/NlpEntity.tsx
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2024 Hexastack. All rights reserved.
+ * 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.
@@ -7,8 +7,9 @@
  */
 
 import AddIcon from "@mui/icons-material/Add";
+import DatasetLinkedIcon from "@mui/icons-material/DatasetLinked";
 import DeleteIcon from "@mui/icons-material/Delete";
-import { Button, Chip, Grid } from "@mui/material";
+import { Button, Chip, Grid, Paper } from "@mui/material";
 import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid";
 import { useRouter } from "next/router";
 import { useState } from "react";
@@ -29,12 +30,13 @@ import { useHasPermission } from "@/hooks/useHasPermission";
 import { useSearch } from "@/hooks/useSearch";
 import { useToast } from "@/hooks/useToast";
 import { useTranslate } from "@/hooks/useTranslate";
+import { PageHeader } from "@/layout/content/PageHeader";
 import { EntityType, Format } from "@/services/types";
 import { INlpEntity } from "@/types/nlp-entity.types";
 import { PermissionAction } from "@/types/permission.types";
 import { getDateTimeFormatter } from "@/utils/date";
 
-import { NlpEntityDialog } from "../NlpEntityDialog";
+import { NlpEntityDialog } from "./NlpEntityDialog";
 
 const NlpEntity = () => {
   const router = useRouter();
@@ -87,7 +89,7 @@ const NlpEntity = () => {
         action: (row) =>
           router.push(
             {
-              pathname: "/nlp/nlp-entities/[id]/nlpValues",
+              pathname: "/nlp/entities/[id]/values",
               query: { id: row.id },
             },
             undefined,
@@ -176,7 +178,7 @@ const NlpEntity = () => {
   };
 
   return (
-    <Grid item xs={12}>
+    <Grid container gap={3} flexDirection="column">
       <NlpEntityDialog {...getDisplayDialogs(addDialogCtl)} />
       <NlpEntityDialog {...editEntityDialogCtl} />
       <DeleteDialog
@@ -191,51 +193,54 @@ const NlpEntity = () => {
           }
         }}
       />
-
-      <Grid
-        justifyContent="flex-end"
-        gap={1}
-        container
-        alignItems="center"
-        flexShrink={0}
-      >
-        <Grid item>
-          <FilterTextfield onChange={onSearch} />
-        </Grid>
-
-        {hasPermission(EntityType.NLP_ENTITY, PermissionAction.CREATE) ? (
+      <PageHeader title={t("title.nlp_entities")} icon={DatasetLinkedIcon}>
+        <Grid
+          justifyContent="flex-end"
+          gap={1}
+          container
+          alignItems="center"
+          flexShrink={0}
+          width="max-content"
+        >
           <Grid item>
-            <Button
-              startIcon={<AddIcon />}
-              variant="contained"
-              sx={{ float: "right" }}
-              onClick={() => addDialogCtl.openDialog()}
-            >
-              {t("button.add")}
-            </Button>
+            <FilterTextfield onChange={onSearch} />
           </Grid>
-        ) : null}
-        {selectedNlpEntities.length > 0 && (
-          <Grid item>
-            <Button
-              startIcon={<DeleteIcon />}
-              variant="contained"
-              color="error"
-              onClick={() => deleteEntityDialogCtl.openDialog(undefined)}
-            >
-              {t("button.delete")}
-            </Button>
-          </Grid>
-        )}
-      </Grid>
 
-      <Grid mt={3}>
-        <DataGrid
-          columns={nlpEntityColumns}
-          {...nlpEntityGrid}
-          checkboxSelection
-          onRowSelectionModelChange={handleSelectionChange}
-        />
+          {hasPermission(EntityType.NLP_ENTITY, PermissionAction.CREATE) ? (
+            <Grid item>
+              <Button
+                startIcon={<AddIcon />}
+                variant="contained"
+                sx={{ float: "right" }}
+                onClick={() => addDialogCtl.openDialog()}
+              >
+                {t("button.add")}
+              </Button>
+            </Grid>
+          ) : null}
+          {selectedNlpEntities.length > 0 && (
+            <Grid item>
+              <Button
+                startIcon={<DeleteIcon />}
+                variant="contained"
+                color="error"
+                onClick={() => deleteEntityDialogCtl.openDialog(undefined)}
+              >
+                {t("button.delete")}
+              </Button>
+            </Grid>
+          )}
+        </Grid>
+      </PageHeader>
+      <Grid item xs={12}>
+        <Paper sx={{ padding: 2 }}>
+          <DataGrid
+            columns={nlpEntityColumns}
+            {...nlpEntityGrid}
+            checkboxSelection
+            onRowSelectionModelChange={handleSelectionChange}
+          />
+        </Paper>
       </Grid>
     </Grid>
   );
diff --git a/frontend/src/components/nlp/components/NlpSample.tsx b/frontend/src/components/nlp/NlpSample.tsx
similarity index 68%
rename from frontend/src/components/nlp/components/NlpSample.tsx
rename to frontend/src/components/nlp/NlpSample.tsx
index b29adcc81..30ac7b3ce 100644
--- a/frontend/src/components/nlp/components/NlpSample.tsx
+++ b/frontend/src/components/nlp/NlpSample.tsx
@@ -1,13 +1,15 @@
 /*
- * Copyright © 2024 Hexastack. All rights reserved.
+ * 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 CircleIcon from "@mui/icons-material/Circle";
 import ClearIcon from "@mui/icons-material/Clear";
+import DatasetIcon from "@mui/icons-material/Dataset";
 import DeleteIcon from "@mui/icons-material/Delete";
 import DownloadIcon from "@mui/icons-material/Download";
 import {
@@ -19,6 +21,7 @@ import {
   IconButton,
   InputAdornment,
   MenuItem,
+  Paper,
   Stack,
   Typography,
 } from "@mui/material";
@@ -50,6 +53,7 @@ import { useHasPermission } from "@/hooks/useHasPermission";
 import { useSearch } from "@/hooks/useSearch";
 import { useToast } from "@/hooks/useToast";
 import { useTranslate } from "@/hooks/useTranslate";
+import { PageHeader } from "@/layout/content/PageHeader";
 import { EntityType, Format } from "@/services/types";
 import { ILanguage } from "@/types/language.types";
 import {
@@ -62,7 +66,7 @@ import { PermissionAction } from "@/types/permission.types";
 import { getDateTimeFormatter } from "@/utils/date";
 import { buildURL } from "@/utils/URL";
 
-import { NlpSampleDialog } from "../NlpSampleDialog";
+import { NlpSampleDialog } from "./NlpSampleDialog";
 
 const NLP_SAMPLE_TYPE_COLORS = {
   all: "#fff",
@@ -148,6 +152,7 @@ export default function NlpSample() {
     },
   );
   const deleteDialogCtl = useDialog<string>(false);
+  const addDialogCtl = useDialog<INlpDatasetSample>(false);
   const editDialogCtl = useDialog<INlpDatasetSample>(false);
   const actionColumns = getActionsColumn<INlpSample>(
     [
@@ -299,7 +304,8 @@ export default function NlpSample() {
   };
 
   return (
-    <Grid item xs={12}>
+    <Grid container gap={3} flexDirection="column">
+      <NlpSampleDialog {...getDisplayDialogs(addDialogCtl)} />
       <NlpSampleDialog {...getDisplayDialogs(editDialogCtl)} />
       <DeleteDialog
         {...deleteDialogCtl}
@@ -313,122 +319,149 @@ export default function NlpSample() {
           }
         }}
       />
-      <Grid container alignItems="center">
+      <PageHeader title={t("title.nlp_datatset")} icon={DatasetIcon}>
         <Grid
+          justifyContent="flex-end"
+          gap={1}
           container
-          display="flex"
-          flexDirection="row"
-          gap={2}
-          direction="row"
+          alignItems="center"
+          flexShrink={0}
+          width="max-content"
         >
-          <FilterTextfield
-            onChange={onSearch}
-            fullWidth={false}
-            sx={{ minWidth: "256px" }}
-          />
-          <AutoCompleteEntitySelect<ILanguage, "title", false>
-            fullWidth={false}
-            sx={{
-              minWidth: "256px",
-            }}
-            autoFocus
-            searchFields={["title", "code"]}
-            entity={EntityType.LANGUAGE}
-            format={Format.BASIC}
-            labelKey="title"
-            label={t("label.language")}
-            multiple={false}
-            onChange={(_e, selected) => setLanguage(selected?.id)}
-          />
-          <Input
-            select
-            fullWidth={false}
-            sx={{
-              minWidth: "256px",
-            }}
-            label={t("label.dataset")}
-            value={type}
-            onChange={(e) => setType(e.target.value as NlpSampleType)}
-            SelectProps={{
-              ...(type && {
-                endAdornment: (
-                  <InputAdornment sx={{ marginRight: "1rem" }} position="end">
-                    <IconButton size="small" onClick={() => setType("all")}>
-                      <ClearIcon sx={{ fontSize: "1.25rem" }} />
-                    </IconButton>
-                  </InputAdornment>
+          <Grid item>
+            <FilterTextfield
+              onChange={onSearch}
+              fullWidth={false}
+              sx={{ minWidth: "256px" }}
+            />
+          </Grid>
+          <Grid item>
+            <AutoCompleteEntitySelect<ILanguage, "title", false>
+              fullWidth={false}
+              sx={{
+                minWidth: "256px",
+              }}
+              autoFocus
+              searchFields={["title", "code"]}
+              entity={EntityType.LANGUAGE}
+              format={Format.BASIC}
+              labelKey="title"
+              label={t("label.language")}
+              multiple={false}
+              onChange={(_e, selected) => setLanguage(selected?.id)}
+            />
+          </Grid>
+          <Grid item>
+            <Input
+              select
+              fullWidth={false}
+              sx={{
+                minWidth: "256px",
+              }}
+              label={t("label.dataset")}
+              value={type}
+              onChange={(e) => setType(e.target.value as NlpSampleType)}
+              SelectProps={{
+                ...(type && {
+                  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>,
+              }}
+            >
+              {["all", ...Object.values(NlpSampleType)].map(
+                (nlpSampleType, index) => (
+                  <MenuItem key={index} value={nlpSampleType}>
+                    <Box display="flex" gap={1}>
+                      <CircleIcon
+                        sx={{
+                          color: NLP_SAMPLE_TYPE_COLORS[nlpSampleType],
+                        }}
+                      />
+                      <Typography>{t(`label.${nlpSampleType}`)}</Typography>
+                    </Box>
+                  </MenuItem>
                 ),
-              }),
-              renderValue: (value) => <Box>{t(`label.${value}`)}</Box>,
-            }}
-          >
-            {["all", ...Object.values(NlpSampleType)].map(
-              (nlpSampleType, index) => (
-                <MenuItem key={index} value={nlpSampleType}>
-                  <Box display="flex" gap={1}>
-                    <CircleIcon
-                      sx={{
-                        color: NLP_SAMPLE_TYPE_COLORS[nlpSampleType],
-                      }}
-                    />
-                    <Typography>{t(`label.${nlpSampleType}`)}</Typography>
-                  </Box>
-                </MenuItem>
-              ),
-            )}
-          </Input>
-          <ButtonGroup sx={{ marginLeft: "auto" }}>
+              )}
+            </Input>
+          </Grid>
+          <Grid item>
             {hasPermission(EntityType.NLP_SAMPLE, PermissionAction.CREATE) &&
             hasPermission(
               EntityType.NLP_SAMPLE_ENTITY,
               PermissionAction.CREATE,
-            ) ? (
-              <FileUploadButton
-                accept="text/csv"
-                label={t("button.import")}
-                onChange={handleImportChange}
-                isLoading={isLoading}
-              />
-            ) : null}
-            {hasPermission(EntityType.NLP_SAMPLE, PermissionAction.READ) &&
-            hasPermission(
-              EntityType.NLP_SAMPLE_ENTITY,
-              PermissionAction.READ,
             ) ? (
               <Button
+                startIcon={<AddIcon />}
                 variant="contained"
-                href={buildURL(
-                  apiUrl,
-                  `nlpsample/export${type ? `?type=${type}` : ""}`,
-                )}
-                startIcon={<DownloadIcon />}
+                onClick={() => {
+                  addDialogCtl.openDialog();
+                }}
               >
-                {t("button.export")}
+                {t("button.add")}
               </Button>
             ) : null}
-            {selectedNlpSamples.length > 0 && (
-              <Grid item>
+          </Grid>
+          <Grid item>
+            <ButtonGroup sx={{ marginLeft: "auto" }}>
+              {hasPermission(EntityType.NLP_SAMPLE, PermissionAction.CREATE) &&
+              hasPermission(
+                EntityType.NLP_SAMPLE_ENTITY,
+                PermissionAction.CREATE,
+              ) ? (
+                <FileUploadButton
+                  accept="text/csv"
+                  label={t("button.import")}
+                  onChange={handleImportChange}
+                  isLoading={isLoading}
+                />
+              ) : null}
+              {hasPermission(EntityType.NLP_SAMPLE, PermissionAction.READ) &&
+              hasPermission(
+                EntityType.NLP_SAMPLE_ENTITY,
+                PermissionAction.READ,
+              ) ? (
                 <Button
-                  startIcon={<DeleteIcon />}
                   variant="contained"
-                  color="error"
-                  onClick={() => deleteDialogCtl.openDialog(undefined)}
+                  href={buildURL(
+                    apiUrl,
+                    `nlpsample/export${type ? `?type=${type}` : ""}`,
+                  )}
+                  startIcon={<DownloadIcon />}
                 >
-                  {t("button.delete")}
+                  {t("button.export")}
                 </Button>
-              </Grid>
-            )}
-          </ButtonGroup>
+              ) : null}
+              {selectedNlpSamples.length > 0 && (
+                <Grid item>
+                  <Button
+                    startIcon={<DeleteIcon />}
+                    variant="contained"
+                    color="error"
+                    onClick={() => deleteDialogCtl.openDialog(undefined)}
+                  >
+                    {t("button.delete")}
+                  </Button>
+                </Grid>
+              )}
+            </ButtonGroup>
+          </Grid>
         </Grid>
-      </Grid>
-
-      <Grid mt={3}>
-        <DataGrid
-          columns={columns}
-          {...dataGridProps}
-          checkboxSelection
-          onRowSelectionModelChange={handleSelectionChange}
-        />
+      </PageHeader>
+      <Grid item xs={12}>
+        <Paper sx={{ padding: 2 }}>
+          <DataGrid
+            columns={columns}
+            {...dataGridProps}
+            checkboxSelection
+            onRowSelectionModelChange={handleSelectionChange}
+          />
+        </Paper>
       </Grid>
     </Grid>
   );
diff --git a/frontend/src/components/nlp/NlpSampleDialog.tsx b/frontend/src/components/nlp/NlpSampleDialog.tsx
index 3a14ce11c..0651cb5b4 100644
--- a/frontend/src/components/nlp/NlpSampleDialog.tsx
+++ b/frontend/src/components/nlp/NlpSampleDialog.tsx
@@ -1,15 +1,17 @@
 /*
- * Copyright © 2024 Hexastack. All rights reserved.
+ * 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 { Dialog, DialogContent } from "@mui/material";
 import { FC } from "react";
 
 import { DialogTitle } from "@/app-components/dialogs/DialogTitle";
+import { useCreate } from "@/hooks/crud/useCreate";
 import { useUpdate } from "@/hooks/crud/useUpdate";
 import { DialogControlProps } from "@/hooks/useDialog";
 import { useToast } from "@/hooks/useToast";
@@ -32,7 +34,19 @@ export const NlpSampleDialog: FC<NlpSampleDialogProps> = ({
 }) => {
   const { t } = useTranslate();
   const { toast } = useToast();
-  const { mutateAsync: updateSample } = useUpdate<
+  const { mutate: addSample } = useCreate<
+    EntityType.NLP_SAMPLE,
+    INlpDatasetSampleAttributes
+  >(EntityType.NLP_SAMPLE, {
+    onError: () => {
+      toast.error(t("message.internal_server_error"));
+    },
+    onSuccess: () => {
+      toast.success(t("message.success_save"));
+      closeDialog();
+    },
+  });
+  const { mutate: updateSample } = useUpdate<
     EntityType.NLP_SAMPLE,
     INlpDatasetSampleAttributes
   >(EntityType.NLP_SAMPLE, {
@@ -41,33 +55,34 @@ export const NlpSampleDialog: FC<NlpSampleDialogProps> = ({
     },
     onSuccess: () => {
       toast.success(t("message.success_save"));
+      closeDialog();
     },
   });
   const onSubmitForm = (form: INlpSampleFormAttributes) => {
     if (sample?.id) {
-      updateSample(
-        {
-          id: sample.id,
-          params: {
-            text: form.text,
-            type: form.type,
-            entities: [...form.keywordEntities, ...form.traitEntities],
-            language: form.language,
-          },
-        },
-        {
-          onSuccess: () => {
-            closeDialog();
-          },
+      updateSample({
+        id: sample.id,
+        params: {
+          text: form.text,
+          type: form.type,
+          entities: [...form.keywordEntities, ...form.traitEntities],
+          language: form.language,
         },
-      );
+      });
+    } else {
+      addSample({
+        text: form.text,
+        type: form.type,
+        entities: [...form.keywordEntities, ...form.traitEntities],
+        language: form.language,
+      });
     }
   };
 
   return (
     <Dialog open={open} fullWidth maxWidth="md" onClose={closeDialog} {...rest}>
       <DialogTitle onClose={closeDialog}>
-        {t("title.edit_nlp_sample")}
+        {sample?.id ? t("title.edit_nlp_sample") : t("title.add_nlp_sample")}
       </DialogTitle>
       <DialogContent>
         <NlpDatasetSample sample={sample} submitForm={onSubmitForm} />
diff --git a/frontend/src/components/nlp/components/NlpValues.tsx b/frontend/src/components/nlp/NlpValues.tsx
similarity index 62%
rename from frontend/src/components/nlp/components/NlpValues.tsx
rename to frontend/src/components/nlp/NlpValues.tsx
index 747295785..74a2a27a6 100644
--- a/frontend/src/components/nlp/components/NlpValues.tsx
+++ b/frontend/src/components/nlp/NlpValues.tsx
@@ -1,19 +1,20 @@
 /*
- * Copyright © 2024 Hexastack. All rights reserved.
+ * 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 { faGraduationCap } from "@fortawesome/free-solid-svg-icons";
 import AddIcon from "@mui/icons-material/Add";
 import ArrowBackIcon from "@mui/icons-material/ArrowBack";
 import DeleteIcon from "@mui/icons-material/Delete";
-import { Box, Button, ButtonGroup, Chip, Grid, Slide } from "@mui/material";
+import { Box, Button, ButtonGroup, Chip, Grid, Paper } from "@mui/material";
 import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid";
 import { useRouter } from "next/router";
-import { useEffect, useState } from "react";
+import { useState } from "react";
 
 import { DeleteDialog } from "@/app-components/dialogs";
 import { FilterTextfield } from "@/app-components/inputs/FilterTextfield";
@@ -39,10 +40,9 @@ import { INlpValue } from "@/types/nlp-value.types";
 import { PermissionAction } from "@/types/permission.types";
 import { getDateTimeFormatter } from "@/utils/date";
 
-import { NlpValueDialog } from "../NlpValueDialog";
+import { NlpValueDialog } from "./NlpValueDialog";
 
 export const NlpValues = ({ entityId }: { entityId: string }) => {
-  const [direction, setDirection] = useState<"up" | "down">("up");
   const deleteEntityDialogCtl = useDialog<string>(false);
   const editValueDialogCtl = useDialog<INlpValue>(false);
   const addNlpValueDialogCtl = useDialog<INlpValue>(false);
@@ -144,12 +144,6 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
     },
     actionColumns,
   ];
-
-  useEffect(() => {
-    //TODO: need to be enhanced in a separate issue (for the content page as well)
-    return setDirection("down");
-  }, []);
-
   const canHaveSynonyms = nlpEntity?.lookups?.[0] === NlpLookups.keywords;
   const handleSelectionChange = (selection: GridRowSelectionModel) => {
     setSelectedNlpValues(selection as string[]);
@@ -157,105 +151,105 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
 
   return (
     <Grid container gap={2} flexDirection="column">
-      <Slide direction={direction} in={true} mountOnEnter unmountOnExit>
-        <Grid item xs={12}>
-          <Box sx={{ padding: 1 }}>
-            <Button
-              onClick={() => {
-                router.push("/nlp/nlp-entities", undefined, {
-                  shallow: true,
-                  scroll: false,
-                });
-              }}
-              sx={{ fontWeight: 500 }}
-              variant="text"
-              startIcon={<ArrowBackIcon />}
-            >
-              {t("button.back")}
-            </Button>
-            <PageHeader
-              title={t("title.nlp_entity_values")}
-              icon={faGraduationCap}
-              chip={
-                <Grid>
-                  <Chip label={nlpEntity?.name} variant="title" />
-                </Grid>
-              }
+      <Grid item xs={12}>
+        <Box sx={{ padding: 1 }}>
+          <Button
+            onClick={() => {
+              router.push("/nlp/entities", undefined, {
+                shallow: true,
+                scroll: false,
+              });
+            }}
+            sx={{ fontWeight: 500 }}
+            variant="text"
+            startIcon={<ArrowBackIcon />}
+          >
+            {t("button.back")}
+          </Button>
+          <PageHeader
+            title={t("title.nlp_entity_values")}
+            icon={faGraduationCap}
+            chip={
+              <Grid>
+                <Chip label={nlpEntity?.name} variant="title" />
+              </Grid>
+            }
+          >
+            <Grid
+              container
+              alignItems="center"
+              sx={{ width: "max-content", gap: 1 }}
             >
-              <Grid
-                container
-                alignItems="center"
-                sx={{ width: "max-content", gap: 1 }}
-              >
-                <Grid item>
-                  <FilterTextfield onChange={onSearch} />
-                </Grid>
-                <ButtonGroup sx={{ marginLeft: "auto" }}>
-                  {hasPermission(
-                    EntityType.NLP_VALUE,
-                    PermissionAction.CREATE,
-                  ) ? (
+              <Grid item>
+                <FilterTextfield onChange={onSearch} />
+              </Grid>
+              <ButtonGroup sx={{ marginLeft: "auto" }}>
+                {hasPermission(
+                  EntityType.NLP_VALUE,
+                  PermissionAction.CREATE,
+                ) ? (
+                  <Button
+                    startIcon={<AddIcon />}
+                    variant="contained"
+                    onClick={() => addNlpValueDialogCtl.openDialog()}
+                    sx={{ float: "right" }}
+                  >
+                    {t("button.add")}
+                  </Button>
+                ) : null}
+                {selectedNlpValues.length > 0 && (
+                  <Grid item>
                     <Button
-                      startIcon={<AddIcon />}
+                      startIcon={<DeleteIcon />}
                       variant="contained"
-                      onClick={() => addNlpValueDialogCtl.openDialog()}
-                      sx={{ float: "right" }}
+                      color="error"
+                      onClick={() =>
+                        deleteEntityDialogCtl.openDialog(undefined)
+                      }
                     >
-                      {t("button.add")}
+                      {t("button.delete")}
                     </Button>
-                  ) : null}
-                  {selectedNlpValues.length > 0 && (
-                    <Grid item>
-                      <Button
-                        startIcon={<DeleteIcon />}
-                        variant="contained"
-                        color="error"
-                        onClick={() =>
-                          deleteEntityDialogCtl.openDialog(undefined)
-                        }
-                      >
-                        {t("button.delete")}
-                      </Button>
-                    </Grid>
-                  )}
-                </ButtonGroup>
-              </Grid>
-            </PageHeader>
-            <NlpValueDialog
-              {...addNlpValueDialogCtl}
-              canHaveSynonyms={canHaveSynonyms}
-              callback={() => {
-                refetchEntity();
-              }}
-            />
-            <DeleteDialog
-              {...deleteEntityDialogCtl}
-              callback={() => {
-                if (selectedNlpValues.length > 0) {
-                  deleteNlpValues(selectedNlpValues);
-                  setSelectedNlpValues([]);
-                  deleteEntityDialogCtl.closeDialog();
-                } else if (deleteEntityDialogCtl.data) {
-                  deleteNlpValue(deleteEntityDialogCtl.data);
-                }
-              }}
-            />
-            <NlpValueDialog
-              {...editValueDialogCtl}
-              canHaveSynonyms={canHaveSynonyms}
-              callback={() => {}}
-            />
-            <Grid padding={1} marginTop={2} container>
+                  </Grid>
+                )}
+              </ButtonGroup>
+            </Grid>
+          </PageHeader>
+          <NlpValueDialog
+            {...addNlpValueDialogCtl}
+            canHaveSynonyms={canHaveSynonyms}
+            callback={() => {
+              refetchEntity();
+            }}
+          />
+          <DeleteDialog
+            {...deleteEntityDialogCtl}
+            callback={() => {
+              if (selectedNlpValues.length > 0) {
+                deleteNlpValues(selectedNlpValues);
+                setSelectedNlpValues([]);
+                deleteEntityDialogCtl.closeDialog();
+              } else if (deleteEntityDialogCtl.data) {
+                deleteNlpValue(deleteEntityDialogCtl.data);
+              }
+            }}
+          />
+          <NlpValueDialog
+            {...editValueDialogCtl}
+            canHaveSynonyms={canHaveSynonyms}
+            callback={() => {}}
+          />
+          <Grid item xs={12}>
+            <Paper sx={{ padding: 2 }}>
               <DataGrid
                 columns={columns}
                 {...dataGridProps}
                 checkboxSelection
                 onRowSelectionModelChange={handleSelectionChange}
               />
-            </Grid>
-          </Box>
-        </Grid>
-      </Slide>
+            </Paper>
+          </Grid>
+        </Box>
+      </Grid>
     </Grid>
   );
 };
diff --git a/frontend/src/components/nlp/index.tsx b/frontend/src/components/nlp/index.tsx
deleted file mode 100644
index 96f0f08eb..000000000
--- a/frontend/src/components/nlp/index.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * 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 { faGraduationCap } from "@fortawesome/free-solid-svg-icons";
-import { Grid, Paper, Tab, Tabs } from "@mui/material";
-import dynamic from "next/dynamic";
-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 { NlpValues } from "./components/NlpValues";
-
-const NlpEntity = dynamic(() => import("./components/NlpEntity"));
-
-export const Nlp = ({
-  entityId,
-  selectedTab,
-}: {
-  entityId?: string;
-  selectedTab: "sample" | "entity";
-}) => {
-  useFind(
-    {
-      entity: EntityType.NLP_ENTITY,
-      format: Format.FULL,
-    },
-    {
-      hasCount: false,
-    },
-  );
-  const router = useRouter();
-  const handleChange = (_event: React.SyntheticEvent, newValue: string) => {
-    router.push(
-      `/nlp/${newValue === "sample" ? "" : "nlp-entities"}`,
-      undefined,
-      {
-        shallow: true,
-        scroll: false,
-      },
-    );
-  };
-  const { t } = useTranslate();
-  const { toast } = useToast();
-  const { mutateAsync: 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 (
-    <Grid container gap={2} flexDirection="column">
-      <PageHeader title={t("title.nlp_train")} icon={faGraduationCap} />
-      <Grid item xs={12}>
-        <Grid container flexDirection="row">
-          <Grid item xs={7}>
-            <Paper>
-              <NlpDatasetSample submitForm={onSubmitForm} />
-            </Paper>
-          </Grid>
-          <Grid item xs={5} pl={2}>
-            <Paper>
-              <NlpDatasetCounter />
-            </Paper>
-          </Grid>
-        </Grid>
-      </Grid>
-      <Grid item xs={12}>
-        <Paper sx={{ pb: "20px" }}>
-          <Tabs
-            orientation="horizontal"
-            variant="scrollable"
-            value={selectedTab}
-            onChange={handleChange}
-          >
-            <Tab label={t("title.nlp")} value="sample" />
-            <Tab label={t("title.nlp_entities")} value="entity" />
-          </Tabs>
-
-          {/* NLP SAMPLES */}
-          <Grid sx={{ padding: "20px" }}>
-            <TabPanel value={selectedTab} index="sample">
-              <NlpSample />
-            </TabPanel>
-
-            {/* NLP ENTITIES */}
-            <TabPanel value={selectedTab} index="entity">
-              {entityId ? <NlpValues entityId={entityId} /> : <NlpEntity />}
-            </TabPanel>
-          </Grid>
-        </Paper>
-      </Grid>
-    </Grid>
-  );
-};
diff --git a/frontend/src/layout/VerticalMenu.tsx b/frontend/src/layout/VerticalMenu.tsx
index 857f9dfac..3c41d0fc1 100644
--- a/frontend/src/layout/VerticalMenu.tsx
+++ b/frontend/src/layout/VerticalMenu.tsx
@@ -23,6 +23,8 @@ import {
 import { Flag, Language } from "@mui/icons-material";
 import AppsIcon from "@mui/icons-material/Apps";
 import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
+import DatasetIcon from "@mui/icons-material/Dataset";
+import DatasetLinkedIcon from "@mui/icons-material/DatasetLinked";
 import DriveFolderUploadIcon from "@mui/icons-material/DriveFolderUpload";
 import FolderIcon from "@mui/icons-material/Folder";
 import HomeIcon from "@mui/icons-material/Home";
@@ -124,11 +126,25 @@ const getMenuItems = (ssoEnabled: boolean): MenuItem[] => [
   },
   {
     text: "menu.nlp",
-    href: "/nlp",
     Icon: faGraduationCap,
-    requires: {
-      [EntityType.NLP_SAMPLE]: [PermissionAction.READ],
-    },
+    submenuItems: [
+      {
+        text: "menu.nlp_entities",
+        href: "/nlp/entities",
+        Icon: DatasetLinkedIcon,
+        requires: {
+          [EntityType.NLP_ENTITY]: [PermissionAction.READ],
+        },
+      },
+      {
+        text: "menu.nlp_dataset",
+        href: "/nlp/dataset",
+        Icon: DatasetIcon,
+        requires: {
+          [EntityType.NLP_SAMPLE]: [PermissionAction.READ],
+        },
+      },
+    ],
   },
   {
     text: "menu.inbox",
diff --git a/frontend/src/pages/nlp/nlp-entities/[id]/nlpValues.tsx b/frontend/src/pages/nlp/dataset.tsx
similarity index 56%
rename from frontend/src/pages/nlp/nlp-entities/[id]/nlpValues.tsx
rename to frontend/src/pages/nlp/dataset.tsx
index 55a89b226..8c575d04a 100644
--- a/frontend/src/pages/nlp/nlp-entities/[id]/nlpValues.tsx
+++ b/frontend/src/pages/nlp/dataset.tsx
@@ -1,11 +1,23 @@
 /*
- * Copyright © 2024 Hexastack. All rights reserved.
+ * 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 NlpPage from "../..";
 
-export default NlpPage;
+import { ReactElement } from "react";
+
+import NlpSample from "@/components/nlp/NlpSample";
+import { Layout } from "@/layout";
+
+const DatasetPage = () => {
+  return <NlpSample />;
+};
+
+DatasetPage.getLayout = function getLayout(page: ReactElement) {
+  return <Layout>{page}</Layout>;
+};
+
+export default DatasetPage;
diff --git a/frontend/src/pages/nlp/index.tsx b/frontend/src/pages/nlp/entities/[id]/values.tsx
similarity index 65%
rename from frontend/src/pages/nlp/index.tsx
rename to frontend/src/pages/nlp/entities/[id]/values.tsx
index 3cf24bc9f..a6dc6f4a6 100644
--- a/frontend/src/pages/nlp/index.tsx
+++ b/frontend/src/pages/nlp/entities/[id]/values.tsx
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2024 Hexastack. All rights reserved.
+ * 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.
@@ -9,22 +9,17 @@
 import { useRouter } from "next/router";
 import { ReactElement } from "react";
 
-import { Nlp } from "@/components/nlp";
+import { NlpValues } from "@/components/nlp/NlpValues";
 import { Layout } from "@/layout";
 
-const NlpPage = () => {
+const NlpValuesPage = () => {
   const router = useRouter();
 
-  return (
-    <Nlp
-      entityId={router.query.id as string}
-      selectedTab={router.pathname === "/nlp" ? "sample" : "entity"}
-    />
-  );
+  return <NlpValues entityId={router.query.id as string} />;
 };
 
-NlpPage.getLayout = function getLayout(page: ReactElement) {
+NlpValuesPage.getLayout = function getLayout(page: ReactElement) {
   return <Layout>{page}</Layout>;
 };
 
-export default NlpPage;
+export default NlpValuesPage;
diff --git a/frontend/src/pages/nlp/nlp-entities/index.tsx b/frontend/src/pages/nlp/entities/index.tsx
similarity index 55%
rename from frontend/src/pages/nlp/nlp-entities/index.tsx
rename to frontend/src/pages/nlp/entities/index.tsx
index 68d25538a..516efb963 100644
--- a/frontend/src/pages/nlp/nlp-entities/index.tsx
+++ b/frontend/src/pages/nlp/entities/index.tsx
@@ -1,11 +1,23 @@
 /*
- * Copyright © 2024 Hexastack. All rights reserved.
+ * 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 NlpPage from "../index";
 
-export default NlpPage;
+import { ReactElement } from "react";
+
+import NlpEntity from "@/components/nlp/NlpEntity";
+import { Layout } from "@/layout";
+
+const NlpEntitiesPage = () => {
+  return <NlpEntity />;
+};
+
+NlpEntitiesPage.getLayout = function getLayout(page: ReactElement) {
+  return <Layout>{page}</Layout>;
+};
+
+export default NlpEntitiesPage;